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