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();
127 let paths = registry.all_required_storage_paths();
128 if paths.is_empty() {
129 return String::new();
130 }
131
132 let mut result = String::new();
133 for path in paths {
134 result.push(' ');
135 result.push_str(container::STORAGE);
136 result.push('/');
137 result.push_str(path);
138 }
139 result
140 }
141
142 fn extension_asset_copy_section(&self) -> String {
143 let discovered = ExtensionLoader::discover(self.project_root);
144
145 if discovered.is_empty() {
146 return String::new();
147 }
148
149 let ext_dirs: HashSet<PathBuf> = discovered
150 .iter()
151 .filter_map(|ext| ext.path.strip_prefix(self.project_root).ok())
152 .map(Path::to_path_buf)
153 .collect();
154
155 if ext_dirs.is_empty() {
156 return String::new();
157 }
158
159 let mut sorted_dirs: Vec<_> = ext_dirs.into_iter().collect();
160 sorted_dirs.sort();
161
162 let copy_lines: Vec<_> = sorted_dirs
163 .iter()
164 .map(|dir| {
165 format!(
166 "COPY {} {}/{}",
167 dir.display(),
168 container::APP,
169 dir.display()
170 )
171 })
172 .collect();
173
174 format!("\n# Copy extension assets\n{}\n", copy_lines.join("\n"))
175 }
176
177 fn mcp_copy_section(&self) -> String {
178 let binaries = self.services_config.as_ref().map_or_else(
179 || ExtensionLoader::get_mcp_binary_names(self.project_root),
180 |config| ExtensionLoader::get_production_mcp_binary_names(self.project_root, config),
181 );
182
183 if binaries.is_empty() {
184 return String::new();
185 }
186
187 let lines: Vec<String> = binaries
188 .iter()
189 .map(|bin| format!("COPY target/release/{} {}/", bin, container::BIN))
190 .collect();
191
192 format!("\n# Copy MCP server binaries\n{}\n", lines.join("\n"))
193 }
194
195 fn env_section(&self) -> String {
196 let profile_env = self.profile_name.map_or_else(String::new, |name| {
197 format!(
198 " SYSTEMPROMPT_PROFILE={}/{}/profile.yaml \\",
199 container::PROFILES,
200 name
201 )
202 });
203
204 if profile_env.is_empty() {
205 format!(
206 r#"ENV HOST=0.0.0.0 \
207 PORT=8080 \
208 RUST_LOG=info \
209 PATH="{}:$PATH" \
210 SYSTEMPROMPT_SERVICES_PATH={} \
211 SYSTEMPROMPT_TEMPLATES_PATH={} \
212 SYSTEMPROMPT_ASSETS_PATH={}"#,
213 container::BIN,
214 container::SERVICES,
215 container::TEMPLATES,
216 container::ASSETS
217 )
218 } else {
219 format!(
220 r#"ENV HOST=0.0.0.0 \
221 PORT=8080 \
222 RUST_LOG=info \
223 PATH="{}:$PATH" \
224{}
225 SYSTEMPROMPT_SERVICES_PATH={} \
226 SYSTEMPROMPT_TEMPLATES_PATH={} \
227 SYSTEMPROMPT_ASSETS_PATH={}"#,
228 container::BIN,
229 profile_env,
230 container::SERVICES,
231 container::TEMPLATES,
232 container::ASSETS
233 )
234 }
235 }
236}