From c43e09224865d55a920e7f883e3dace10ef479cf Mon Sep 17 00:00:00 2001 From: wtclaude Date: Mon, 24 Aug 2026 11:19:49 -0500 Subject: [PATCH] fix(env): the documented Compose deploy could not boot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `.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 --- .env.example | 15 +++++++++++++++ README.md | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/.env.example b/.env.example index 520c30f..22abc57 100644 --- a/.env.example +++ b/.env.example @@ -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). diff --git a/README.md b/README.md index 5b15305..097b90b 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,12 @@ cp .env.example .env # Edit .env and set at least: # DB_PASSWORD, DB_ROOT_PASSWORD (any strong values) # JWT_SECRET (a long random string) +# SECRET_ENC_KEY (a different long random string) +# BOT_INTERNAL_KEY (a third one, 16+ chars — even with no bot) # ADMIN_USERNAME, ADMIN_PASSWORD (your first admin login) +# +# SECRET_ENC_KEY and BOT_INTERNAL_KEY are not optional in production: the app +# refuses to start without them, so the container crash-loops before it listens. docker compose pull && docker compose up -d # IMAGE_TAG defaults to `latest` # pin a specific build (reproducible deploy / rollback):