Generate a session secret instead of refusing to start
Build and Push Docker Image / build-and-push (push) Failing after 20s

setup.sh starts the production server without SESSION_SECRET, so the new
"refuse to start" check stopped the app after an update. When the variable
is unset in production the server now makes a random secret once and keeps
it in DATA_DIR/.session-secret (mode 600, git-ignored).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-06 08:18:02 -04:00
1 parent f274bc9c8a
commit 31fb3f958d
3 files changed
+24 -7

No files matched your search

+2
View File
@@ -11,3 +11,5 @@ coverage/
.api.log .api.log
.DS_Store .DS_Store
.env .env
.session-secret
server/data/backups/
+1 -1
View File
@@ -46,7 +46,7 @@ http://localhost:5173. Dev mode is much slower, so don't use it for real users.
| Variable | What it turns on | | Variable | What it turns on |
| --- | --- | | --- | --- |
| `SESSION_SECRET` | **Required in production.** A long random string; the server refuses to start without it. | | `SESSION_SECRET` | A long random string that signs login cookies. Optional: if unset in production, the server makes one and keeps it in `DATA_DIR/.session-secret`. |
| `ADMIN_EMAIL` | The one admin account. | | `ADMIN_EMAIL` | The one admin account. |
| `ESV_API_KEY`, `API_BIBLE_KEY` | ESV, NLT, CSB and NASB in the Reader. | | `ESV_API_KEY`, `API_BIBLE_KEY` | ESV, NLT, CSB and NASB in the Reader. |
| `IQBIBLE_API_KEY` | IQ Bible: more commentaries, lexicons (BDB, LSJ, Abbott-Smith), places, topics, people, textual notes, Gospel parallels, daily verse and Spurgeon devotional, plan calendar export, more audio narrations, and background for podcast prompts. Hidden when unset. | | `IQBIBLE_API_KEY` | IQ Bible: more commentaries, lexicons (BDB, LSJ, Abbott-Smith), places, topics, people, textual notes, Gospel parallels, daily verse and Spurgeon devotional, plan calendar export, more audio narrations, and background for podcast prompts. Hidden when unset. |
+21 -6
View File
@@ -2,11 +2,12 @@ import express from 'express';
import compression from 'compression'; import compression from 'compression';
import session from 'express-session'; import session from 'express-session';
import QRCode from 'qrcode'; import QRCode from 'qrcode';
import { randomUUID } from 'crypto'; import { randomUUID, randomBytes } from 'crypto';
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { dirname, join } from 'path'; import { dirname, join } from 'path';
import { import {
initDb, getAllProjects, getProject, getProjectEdited, upsertProject, deleteProject, initDb, dataDir, getAllProjects, getProject, getProjectEdited, upsertProject, deleteProject,
countUsers, createUser, getUserByEmail, getUserById, claimOrphanProjects, countUsers, createUser, getUserByEmail, getUserById, claimOrphanProjects,
enableTotp, disableTotp, setBackupCodeHashes, setPodcastName, enableTotp, disableTotp, setBackupCodeHashes, setPodcastName,
getShareToken, setShareToken, clearShareToken, getProjectByShareToken, getShareToken, setShareToken, clearShareToken, getProjectByShareToken,
@@ -50,9 +51,23 @@ const app = express();
const PORT = process.env.PORT || 3001; const PORT = process.env.PORT || 3001;
const isProd = process.env.NODE_ENV === 'production'; const isProd = process.env.NODE_ENV === 'production';
if (isProd && !process.env.SESSION_SECRET) { // Session cookies are signed with SESSION_SECRET. When it isn't set (setup.sh doesn't set one), a random secret
console.error('SESSION_SECRET is not set. Set it to a long random string before running in production.'); // is made once and kept in DATA_DIR, so it survives restarts and nobody has to configure anything.
process.exit(1); function sessionSecret() {
if (process.env.SESSION_SECRET) return process.env.SESSION_SECRET;
if (!isProd) return 'dev-only-secret-change-me';
const file = join(dataDir(), '.session-secret');
try {
if (existsSync(file)) return readFileSync(file, 'utf8').trim();
mkdirSync(dataDir(), { recursive: true });
const made = randomBytes(48).toString('hex');
writeFileSync(file, made, { mode: 0o600 });
console.log(`SESSION_SECRET is not set; generated one and saved it to ${file}.`);
return made;
} catch (err) {
console.error('Could not read or save a session secret; set SESSION_SECRET instead.', err.message);
process.exit(1);
}
} }
// Trust the reverse proxy (needed for secure cookies to work behind nginx/etc). // Trust the reverse proxy (needed for secure cookies to work behind nginx/etc).
@@ -98,7 +113,7 @@ app.use(compression());
app.use(express.json({ limit: '10mb' })); app.use(express.json({ limit: '10mb' }));
app.use(session({ app.use(session({
store: new SqliteSessionStore(), store: new SqliteSessionStore(),
secret: process.env.SESSION_SECRET || 'dev-only-secret-change-me', secret: sessionSecret(),
resave: false, resave: false,
saveUninitialized: false, saveUninitialized: false,
rolling: true, rolling: true,