fix(env): the documented Compose deploy could not boot
All checks were successful
PR Checks / client-build (pull_request) Successful in 30s
PR Checks / bot-tests (pull_request) Successful in 31s
PR Checks / server-tests (pull_request) Successful in 2m32s

`.env.example` — the file docker-compose.yml actually reads — never listed
SECRET_ENC_KEY. `utils/secretBox.js` resolves the key at require time and throws
`SECRET_ENC_KEY must be set in production`, so following README Option A exactly
produces a container that crash-loops before it ever listens.

It was easy to miss because the variable IS documented in two places that a
Compose operator never opens: `server/.env.example`, which is what local
development copies, and the README's environment-variable reference table. Only
the file the deployment reads was missing it.

Reproduced against the published image with a clean `cp .env.example .env`, then
verified the fix the same way: fill in the values the README names and
`docker compose up -d` reaches `listening on http://0.0.0.0:3000` and
`/api/health` → `{"status":"ok"}`.

- `.env.example` gains SECRET_ENC_KEY, beside JWT_SECRET, with what it encrypts,
  that production refuses to start without it, and that changing it later
  orphans every stored secret rather than re-encrypting them.
- README's Option A "set at least" list gains SECRET_ENC_KEY and
  BOT_INTERNAL_KEY. Both are refused-at-boot in production, and BOT_INTERNAL_KEY
  is required even on a deployment that runs no bot, which is exactly the case
  the list omitted.

Found while writing the runicgateway.com installation docs, whose quickstart is
checked against this file on every build.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-08-24 11:19:49 -05:00
parent 0f96a372cf
commit c43e092248
2 changed files with 20 additions and 0 deletions

View File

@@ -56,6 +56,21 @@ DB_ROOT_PASSWORD=change-me-root-password
# Auth
JWT_SECRET=change-me-to-a-long-random-string
# Encrypts every secret this site stores at rest (AES-256-GCM): OAuth client
# secrets, the Discord bot token, the Gmail refresh token, the uo-link auth
# token. REQUIRED in production — with NODE_ENV=production the app REFUSES TO
# START without it (utils/secretBox.js), so a Compose deployment that leaves it
# blank crash-loops before it ever listens. Development falls back to a key
# derived from JWT_SECRET, with a warning.
#
# Any string; it is hashed to 32 bytes. Generate a long random one and treat it
# like the database password.
#
# Changing it on a live instance does NOT re-encrypt anything: every secret
# already stored becomes unreadable and has to be entered again from the admin
# panel. That is also the reason it is a dedicated key rather than a reuse of
# JWT_SECRET — rotating a session secret must not orphan stored credentials.
SECRET_ENC_KEY=change-me-to-a-different-long-random-string
JWT_EXPIRES_IN=1d
# auto = Secure cookie only when the request arrives over HTTPS (Pangolin).
# Leave as auto so login works both via the LAN IP (HTTP) and the proxy (HTTPS).