Skip to main content

systemprompt_cli/commands/admin/setup/
docker_compose.rs

1//! docker-compose file generation for the local database.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use anyhow::{Context, Result};
7use std::path::Path;
8use std::process::Command;
9use systemprompt_logging::CliService;
10
11use super::common::PostgresConfig;
12
13pub fn is_docker_available() -> bool {
14    Command::new("docker").arg("--version").output().is_ok()
15}
16
17pub fn is_compose_available() -> bool {
18    Command::new("docker")
19        .args(["compose", "version"])
20        .output()
21        .is_ok_and(|o| o.status.success())
22}
23
24pub fn is_container_running(container_name: &str) -> bool {
25    Command::new("docker")
26        .args([
27            "ps",
28            "--filter",
29            &format!("name=^{}$", container_name),
30            "--format",
31            "{{.Names}}",
32        ])
33        .output()
34        .is_ok_and(|o| !String::from_utf8_lossy(&o.stdout).trim().is_empty())
35}
36
37pub fn create_compose_files_if_missing(
38    compose_dir: &Path,
39    container_name: &str,
40    port: u16,
41) -> Result<()> {
42    const INIT_SCRIPT: &str = r#"CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
43CREATE EXTENSION IF NOT EXISTS "pgcrypto";
44"#;
45
46    let compose_file = compose_dir.join("docker-compose.yaml");
47
48    std::fs::create_dir_all(compose_dir).context("Failed to create infrastructure/docker")?;
49
50    let init_scripts_dir = compose_dir.join("init-scripts");
51    std::fs::create_dir_all(&init_scripts_dir).context("Failed to create init-scripts")?;
52
53    let compose_content = format!(
54        r#"services:
55  postgres:
56    image: postgres:18-alpine
57    container_name: {container_name}
58    environment:
59      POSTGRES_USER: ${{POSTGRES_USER:-systemprompt}}
60      POSTGRES_PASSWORD: ${{POSTGRES_PASSWORD}}
61      POSTGRES_DB: ${{POSTGRES_DB:-systemprompt}}
62    ports:
63      - "{port}:5432"
64    volumes:
65      - {container_name}_data:/var/lib/postgresql
66      - ./init-scripts:/docker-entrypoint-initdb.d
67    healthcheck:
68      test: ["CMD-SHELL", "pg_isready -U ${{POSTGRES_USER:-systemprompt}}"]
69      interval: 5s
70      timeout: 5s
71      retries: 5
72    networks:
73      - {container_name}_network
74
75volumes:
76  {container_name}_data:
77
78networks:
79  {container_name}_network:
80    driver: bridge
81"#
82    );
83
84    std::fs::write(&compose_file, compose_content)
85        .context("Failed to write docker-compose.yaml")?;
86
87    std::fs::write(init_scripts_dir.join("01-extensions.sql"), INIT_SCRIPT)
88        .context("Failed to write init script")?;
89
90    CliService::success(&format!("Created {}", compose_file.display()));
91
92    Ok(())
93}
94
95pub fn start_compose(
96    config: &PostgresConfig,
97    compose_dir: &Path,
98    container_name: &str,
99) -> Result<()> {
100    CliService::info("Starting PostgreSQL...");
101
102    let result = Command::new("docker")
103        .args(["compose", "up", "-d"])
104        .current_dir(compose_dir)
105        .env("POSTGRES_USER", &config.user)
106        .env("POSTGRES_PASSWORD", &config.password)
107        .env("POSTGRES_DB", &config.database)
108        .env("CONTAINER_NAME", container_name)
109        .output()
110        .context("Failed to run docker compose")?;
111
112    if !result.status.success() {
113        let stderr = String::from_utf8_lossy(&result.stderr);
114        anyhow::bail!("Failed to start PostgreSQL: {}", stderr);
115    }
116
117    CliService::success("PostgreSQL container started");
118    Ok(())
119}
120
121pub fn wait_for_postgres_ready(config: &PostgresConfig, container_name: &str) {
122    CliService::info("Waiting for PostgreSQL to be ready...");
123
124    for _ in 0..30 {
125        std::thread::sleep(std::time::Duration::from_secs(1));
126
127        let health = Command::new("docker")
128            .args([
129                "exec",
130                container_name,
131                "pg_isready",
132                "-U",
133                &config.user,
134                "-d",
135                &config.database,
136            ])
137            .output();
138
139        if health.is_ok_and(|o| o.status.success()) {
140            CliService::success("PostgreSQL is ready");
141            return;
142        }
143    }
144
145    CliService::warning("PostgreSQL started but health check timed out");
146}