systemprompt_cli/commands/cloud/dockerfile/
builder.rs1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3
4use systemprompt_cloud::constants::{container, storage};
5use systemprompt_extension::ExtensionRegistry;
6use systemprompt_loader::{ConfigLoader, ExtensionLoader};
7use systemprompt_models::{CliPaths, ServicesConfig};
8
9use super::super::tenant::find_services_config;
10
11#[derive(Debug)]
12pub struct DockerfileBuilder<'a> {
13 project_root: &'a Path,
14 profile_name: Option<&'a str>,
15 services_config: Option<ServicesConfig>,
16}
17
18impl<'a> DockerfileBuilder<'a> {
19 pub fn new(project_root: &'a Path) -> Self {
20 let services_config = find_services_config(project_root)
21 .map_err(|e| {
22 tracing::debug!(error = %e, "No services config found for dockerfile generation");
23 e
24 })
25 .ok()
26 .and_then(|path| {
27 ConfigLoader::load_from_path(&path)
28 .map_err(|e| {
29 tracing::warn!(error = %e, "Failed to load services config");
30 e
31 })
32 .ok()
33 });
34 Self {
35 project_root,
36 profile_name: None,
37 services_config,
38 }
39 }
40
41 pub const fn with_profile(mut self, name: &'a str) -> Self {
42 self.profile_name = Some(name);
43 self
44 }
45
46 pub fn build(&self) -> String {
47 let mcp_section = self.mcp_copy_section();
48 let env_section = self.env_section();
49 let extension_dirs = Self::extension_storage_dirs();
50 let extension_assets_section = self.extension_asset_copy_section();
51
52 format!(
53 r#"# systemprompt.io Application Dockerfile
54# Built by: systemprompt cloud profile create
55# Used by: systemprompt cloud deploy
56
57FROM debian:bookworm-slim
58
59# Install runtime dependencies
60RUN apt-get update && apt-get install -y \
61 ca-certificates \
62 curl \
63 libssl3 \
64 libpq5 \
65 lsof \
66 && rm -rf /var/lib/apt/lists/*
67
68RUN useradd -m -u 1000 app
69WORKDIR {app}
70
71RUN mkdir -p {bin} {logs} {storage}/{images} {storage}/{generated} {storage}/{logos} {storage}/{audio} {storage}/{video} {storage}/{documents} {storage}/{uploads} {web}{extension_dirs}
72
73# Copy pre-built binaries
74COPY target/release/systemprompt {bin}/
75{mcp_section}
76# Copy storage assets (images, etc.)
77COPY storage {storage}
78
79# Copy web dist (generated HTML, CSS, JS)
80COPY web/dist {web_dist}
81{extension_assets_section}
82# Copy services configuration
83COPY services {services_path}
84
85# Copy profiles
86COPY .systemprompt/profiles {profiles}
87RUN chmod +x {bin}/* && chown -R app:app {app}
88
89USER app
90EXPOSE 8080
91
92# Environment configuration
93{env_section}
94
95HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
96 CMD curl -f http://localhost:8080/api/v1/health || exit 1
97
98CMD ["{bin}/systemprompt", "{cmd_infra}", "{cmd_services}", "{cmd_serve}", "--foreground"]
99"#,
100 app = container::APP,
101 bin = container::BIN,
102 logs = container::LOGS,
103 storage = container::STORAGE,
104 web = container::WEB,
105 web_dist = container::WEB_DIST,
106 services_path = container::SERVICES,
107 profiles = container::PROFILES,
108 images = storage::IMAGES,
109 generated = storage::GENERATED,
110 logos = storage::LOGOS,
111 audio = storage::AUDIO,
112 video = storage::VIDEO,
113 documents = storage::DOCUMENTS,
114 uploads = storage::UPLOADS,
115 extension_dirs = extension_dirs,
116 mcp_section = mcp_section,
117 env_section = env_section,
118 extension_assets_section = extension_assets_section,
119 cmd_infra = CliPaths::INFRA,
120 cmd_services = CliPaths::SERVICES,
121 cmd_serve = CliPaths::SERVE,
122 )
123 }
124
125 fn extension_storage_dirs() -> String {
126 let registry = ExtensionRegistry::discover().unwrap_or_else(|e| {
127 tracing::error!(error = %e, "extension dependency cycle; using empty registry");
128 ExtensionRegistry::new()
129 });
130 let paths = registry.all_required_storage_paths();
131 if paths.is_empty() {
132 return String::new();
133 }
134
135 let mut result = String::new();
136 for path in paths {
137 result.push(' ');
138 result.push_str(container::STORAGE);
139 result.push('/');
140 result.push_str(path);
141 }
142 result
143 }
144
145 fn extension_asset_copy_section(&self) -> String {
146 let discovered = ExtensionLoader::discover(self.project_root);
147
148 if discovered.is_empty() {
149 return String::new();
150 }
151
152 let ext_dirs: HashSet<PathBuf> = discovered
153 .iter()
154 .filter_map(|ext| ext.path.strip_prefix(self.project_root).ok())
155 .map(Path::to_path_buf)
156 .collect();
157
158 if ext_dirs.is_empty() {
159 return String::new();
160 }
161
162 let mut sorted_dirs: Vec<_> = ext_dirs.into_iter().collect();
163 sorted_dirs.sort();
164
165 let copy_lines: Vec<_> = sorted_dirs
166 .iter()
167 .map(|dir| {
168 format!(
169 "COPY {} {}/{}",
170 dir.display(),
171 container::APP,
172 dir.display()
173 )
174 })
175 .collect();
176
177 format!("\n# Copy extension assets\n{}\n", copy_lines.join("\n"))
178 }
179
180 fn mcp_copy_section(&self) -> String {
181 let binaries = self.services_config.as_ref().map_or_else(
182 || ExtensionLoader::get_mcp_binary_names(self.project_root),
183 |config| ExtensionLoader::get_production_mcp_binary_names(self.project_root, config),
184 );
185
186 if binaries.is_empty() {
187 return String::new();
188 }
189
190 let lines: Vec<String> = binaries
191 .iter()
192 .map(|bin| format!("COPY target/release/{} {}/", bin, container::BIN))
193 .collect();
194
195 format!("\n# Copy MCP server binaries\n{}\n", lines.join("\n"))
196 }
197
198 fn env_section(&self) -> String {
199 let profile_env = self.profile_name.map_or_else(String::new, |name| {
200 format!(
201 " SYSTEMPROMPT_PROFILE={}/{}/profile.yaml \\",
202 container::PROFILES,
203 name
204 )
205 });
206
207 if profile_env.is_empty() {
208 format!(
209 r#"ENV HOST=0.0.0.0 \
210 PORT=8080 \
211 RUST_LOG=info \
212 PATH="{}:$PATH" \
213 SYSTEMPROMPT_SERVICES_PATH={} \
214 SYSTEMPROMPT_TEMPLATES_PATH={} \
215 SYSTEMPROMPT_ASSETS_PATH={}"#,
216 container::BIN,
217 container::SERVICES,
218 container::TEMPLATES,
219 container::ASSETS
220 )
221 } else {
222 format!(
223 r#"ENV HOST=0.0.0.0 \
224 PORT=8080 \
225 RUST_LOG=info \
226 PATH="{}:$PATH" \
227{}
228 SYSTEMPROMPT_SERVICES_PATH={} \
229 SYSTEMPROMPT_TEMPLATES_PATH={} \
230 SYSTEMPROMPT_ASSETS_PATH={}"#,
231 container::BIN,
232 profile_env,
233 container::SERVICES,
234 container::TEMPLATES,
235 container::ASSETS
236 )
237 }
238 }
239}