infrastructure/docker-compose.yml had \`\${VAR:-default}\` fallbacks for
JWT_SECRET, SESSION_SECRET, and POSTGRES_PASSWORD that silently
substituted known-weak values (\`change-me-in-production\`, \`trails\`)
when the env wasn't set. A misconfigured prod deploy would happily come
up with these defaults — JWT/session forgery + a guessable Postgres
password.
Switched the four critical substitutions to \`\${VAR:?message}\` so
\`docker compose up\` fails loud instead. Mirrors the existing pattern
already used for BROUTER_URL/BROUTER_AUTH_TOKEN and (in staging) for
JWT_SECRET/SESSION_SECRET.
Sites fixed:
- journal: DATABASE_URL (POSTGRES_PASSWORD), JWT_SECRET, SESSION_SECRET
- planner: DATABASE_URL (POSTGRES_PASSWORD)
- postgres: POSTGRES_PASSWORD (container itself)
- exporter: DATA_SOURCE_NAME (POSTGRES_PASSWORD)
- staging: both DATABASE_URLs
Production already provides all three via SOPS-encrypted
secrets.app.env, so this is a defense-in-depth change with no behavior
change on a properly-configured deploy.
Updated infrastructure/.env.example to make SESSION_SECRET explicit
(was missing) and call out the trio as required.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bugs in the staging-environments rollout:
1. Staging containers published on 127.0.0.1 are unreachable from the
production Caddy container, which connects via the Docker bridge IP
(host.docker.internal:host-gateway resolves to the bridge, not
loopback). Bind to 0.0.0.0 instead — Hetzner Cloud firewall blocks
ports 3000+ from the public internet, so it stays internal-only.
2. The trailing 'docker compose ps' diagnostic in deploy-staging /
deploy-preview was missing --env-file staging.env, so compose
failed env interpolation and the job exited non-zero even when the
actual deploy succeeded.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Implements the staging-environments OpenSpec change. Persistent staging at
staging.trails.cool / planner.staging.trails.cool deploys from main; PR
opens get a journal-only preview at pr-<N>.staging.trails.cool that shares
the persistent planner. cd-staging.yml builds tagged images, manages
per-PR Postgres databases and Caddyfile snippets, evicts the oldest
preview at the cap of 3, and tears everything down on PR close.
staging-cleanup.yml runs weekly to sweep orphaned previews.
DNS records (staging + *.staging A/AAAA) already applied to production
via tofu.
Caddy approach: per-PR Caddyfile snippets imported from /etc/caddy/sites/
and reloaded on each PR event — no wildcard / on-demand TLS, no router
service. Production compose gains a trails-shared network for the staging
project to reach Postgres, and host.docker.internal on Caddy so it can
reverse-proxy staging containers published on the host loopback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>