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([