diff --git a/changelog.d/fixes/6828-bootstrap-filter-empty-env.md b/changelog.d/fixes/6828-bootstrap-filter-empty-env.md new file mode 100644 index 0000000000..d4a6ca34ad --- /dev/null +++ b/changelog.d/fixes/6828-bootstrap-filter-empty-env.md @@ -0,0 +1 @@ +- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu). diff --git a/scripts/build/bootstrap-env.mjs b/scripts/build/bootstrap-env.mjs index 90ee46ddd6..e3bdd673ec 100644 --- a/scripts/build/bootstrap-env.mjs +++ b/scripts/build/bootstrap-env.mjs @@ -173,7 +173,14 @@ export function bootstrapEnv({ dataDirOverride, quiet = false } = {}) { const preferredEnvFiltered = Object.fromEntries( Object.entries(preferredEnv).filter(([, v]) => typeof v === "string" && v.length > 0) ); - const merged = { ...persisted, ...preferredEnvFiltered, ...process.env }; + // Filter empty strings from process.env so that Docker `-e KEY=` (which sets an + // empty string) does not override real values persisted in server.env or set + // in .env. Only shell/Docker vars that the operator actually set should win. + // Mirrors the filtering already applied to preferredEnv above. (fixes #6824) + const processEnvFiltered = Object.fromEntries( + Object.entries(process.env).filter(([, v]) => typeof v === "string" && v.length > 0) + ); + const merged = { ...persisted, ...preferredEnvFiltered, ...processEnvFiltered }; // ── Auto-generate required secrets ──────────────────────────────────────── let needsPersist = false; diff --git a/tests/unit/bootstrap-env.test.ts b/tests/unit/bootstrap-env.test.ts index 45d2911776..204ceea672 100644 --- a/tests/unit/bootstrap-env.test.ts +++ b/tests/unit/bootstrap-env.test.ts @@ -120,6 +120,30 @@ test("bootstrapEnv fails closed when existing database cannot be inspected", () }); }); +test("bootstrapEnv ignores blank process.env values that would override persisted secrets (#6824)", () => { + withTempEnv(({ dataDir }) => { + process.env.DATA_DIR = dataDir; + fs.mkdirSync(dataDir, { recursive: true }); + + // Persisted secrets in server.env + fs.writeFileSync( + path.join(dataDir, "server.env"), + "STORAGE_ENCRYPTION_KEY=persisted-key\nJWT_SECRET=persisted-jwt\n", + "utf8" + ); + + // Simulate Docker `-e STORAGE_ENCRYPTION_KEY=` — sets an empty string + process.env.STORAGE_ENCRYPTION_KEY = ""; + process.env.JWT_SECRET = ""; + + const env = bootstrapEnv({ quiet: true }); + + // Empty process.env values must NOT override persisted secrets + assert.equal(env.STORAGE_ENCRYPTION_KEY, "persisted-key"); + assert.equal(env.JWT_SECRET, "persisted-jwt"); + }); +}); + test("bootstrapEnv ignores blank dataDirOverride values", () => { withTempEnv(({ dataDir }) => { process.env.DATA_DIR = dataDir;