@smbcloud / cli / commits / 364c8d4

fix(deploy): stop nextjs-ssr deploys shipping local .env files

Next.js copies the project's .env* files into .next/standalone/, so the standalone upload sent the developer's local env (dev OAuth credentials and redirect URLs) to the production runtime directory, and the upload's rsync --delete removed any server-managed .env on each deploy. Exclude .env* from the standalone transfer, anchored at the app root and the runtime subdir so bundled node_modules content is untouched. The same excludes keep --delete away from server-side .env files. A notice is printed when local env files were skipped so operators know runtime env must live on the server (ecosystem config or a server-side .env). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

Seto Elkahfi committed Jul 8, 2026 at 06:56 UTC 364c8d4e0656b635101b2338a012ba024169ee24
1 file changed +37
crates/cli/src/deploy/process_deploy_nextjs_ssr.rs
+37
@@ -235,6 +235,40 @@ pub async fn process_deploy_nextjs_ssr(env: Environment, config: Config) -> Resu
235 .map(|path| format!("{}/", path))
236 .unwrap_or_default();
237
238 + // Next.js copies the project's .env* files into .next/standalone/, so an
239 + // unfiltered upload would ship the developer's local (often development)
240 + // env straight into the production runtime directory — and `--delete`
241 + // would remove any operator-managed .env on the server. Env files are
242 + // excluded on both sides: local ones never upload, server ones survive
243 + // (rsync does not delete excluded destination files without
244 + // --delete-excluded). Runtime env is server-managed — ecosystem config or
245 + // a server-side .env. Anchored to the app roots so bundled node_modules
246 + // content is not affected.
247 + let env_file_excludes: Vec<String> = {
248 + let mut patterns = vec!["/.env*".to_string()];
249 + if !runtime_prefix.is_empty() {
250 + patterns.push(format!("/{}.env*", runtime_prefix));
251 + }
252 + patterns
253 + };
254 +
255 + let local_env_file = [
256 + format!("{}/.env", standalone_dir),
257 + format!("{}/{}.env", standalone_dir, runtime_prefix),
258 + ]
259 + .iter()
260 + .any(|path| std::path::Path::new(path).exists());
261 + if local_env_file {
262 + println!(
263 + "{} {}",
264 + succeed_symbol(),
265 + succeed_message(
266 + "Local .env* files found in the standalone build — not uploaded. \
267 + Runtime env comes from the server (ecosystem config or server-side .env).",
268 + )
269 + );
270 + }
271 +
272 // (local_source, remote_destination)
273 // .next/standalone contents go to the root of remote_path.
274 // .next/static and public go into the runtime directory that contains
@@ -299,6 +333,9 @@ pub async fn process_deploy_nextjs_ssr(env: Environment, config: Config) -> Resu
333 "--exclude".to_string(),
334 "logs/".to_string(),
335 ]);
336 + for pattern in &env_file_excludes {
337 + rsync_args.extend(["--exclude".to_string(), pattern.clone()]);
338 + }
339 }
340
341 rsync_args.extend([