Do not expose the app until production posture passes locally on the target server.
This runbook turns a disposable/private VPS-like rehearsal host into a repeatable, non-destructive gate: load a production-shaped environment, run the bootstrap bundle, validate compose shape, run local diagnostics, then connect the HTTPS reverse proxy only after blocking checks are clean. Passing this rehearsal proves a transplantable container/edge shape; it is not the later official long-lived VPS cutover.
Files in this slice
| File | Purpose | Exposure rule |
|---|---|---|
.env.production.example | Copy-safe production variable template with placeholders. | Commit template only; never commit .env.production. |
docker-compose.vps.yml | VPS-oriented app/Postgres/Redis skeleton with no app host port published and named volumes for Postgres, private media, backups, backup manifests, and custody ledgers. | Use first on a disposable test-VPS rehearsal host, then transplant the proven shape to the official VPS with fresh private secrets/evidence; do not use the dev compose file for production. |
deploy/reverse-proxy/ | Copy-safe Caddy/nginx edge templates for the public TLS handoff after preflight. | Use one template as a starting point; keep live configs and TLS secrets off-repo. |
vps-bootstrap-plan | Non-destructive CLI bundle that checks the private env file, compose skeleton, proxy templates, gitignored runtime ledger paths, and safe operator order. | Run before starting VPS services; it does not start compose, create directories, connect to databases, or touch backup contents. |
deployment-rehearsal-plan | Secret-free operator checklist that connects bootstrap, preflight, external smokes, evidence records, backup metadata, restore-drill planning, and private readiness. | Run before copying commands into a VPS session; it prints commands only and performs no deployment actions. |
production-preflight | CLI gate that blocks local profile, placeholder domains, insecure cookies, missing owner gate, local-only backups, and long owner sessions. | Run on the VPS before public proxy exposure. |
Docker HEALTHCHECK | Container-level shallow /healthz liveness check. | Public health remains shallow; deep diagnostics stay local. |
Create the private production environment
cp .env.production.example .env.production
chmod 600 .env.production
# Edit every CHANGE_ME value before starting services.
NOCTURNE_PUBLIC_ORIGINmust be the finalhttps://origin, not an example,.test, localhost, or temporary placeholder.NOCTURNE_SECURE_COOKIES=trueis required for production.- Use
NOCTURNE_OWNER_KEY_SHA256rather than storing the plaintext owner key in the environment when possible. - Keep
NOCTURNE_OWNER_SESSION_MAX_AGE_SECONDSat or below 3600 before exposure. - Use
NOCTURNE_BACKUP_POSTURE=configuredonly afterNOCTURNE_BACKUP_DIRexists and is writable, orexternalwhen provider/offsite backups are documented outside the app.
Run the bootstrap bundle before services start
cargo run -- vps-bootstrap-plan --env-file .env.production
The bootstrap bundle reads only local files and secret-free metadata. It verifies that the live env file is not the committed example, no placeholder variables remain, the env file is private, backup manifest/custody ledgers are git-ignored, the VPS compose file persists backup manifest/custody ledgers in named volumes, the compose file does not publish host ports, and the Caddy/nginx templates preserve body caps, access-log minimization, and client-IP header stripping. It also verifies that the stock-Caddy rate-limit boundary remains explicit and that nginx retains its memory-only sensitive-POST throttle markers; it does not prove live throttle behavior or WAF/DDoS resilience.
For the full private VPS operator sequence, generate deployment-rehearsal-plan with the selected HTTPS rehearsal origin and backup ID before starting the live rehearsal.
Validate compose shape without starting public exposure
NOCTURNE_ENV_FILE=.env.production docker compose \
-f docker-compose.vps.yml \
--env-file .env.production \
config
The VPS compose skeleton intentionally uses expose, not ports, for the application. That keeps the Rust app private to the Docker network until you explicitly attach a reverse proxy. It also keeps backup artifacts, manifest ledgers, and custody ledgers in separate named volumes so container restarts do not erase the metadata required by private-VPS readiness. The development docker-compose.yml publishes app, Postgres, and Redis host ports for local testing and should not be the production exposure file.
Run production preflight on the target server
# If running from the host binary:
set -a; . ./.env.production; set +a
cargo run -- production-preflight
# If running through the VPS compose file:
NOCTURNE_ENV_FILE=.env.production docker compose \
-f docker-compose.vps.yml \
--env-file .env.production \
run --rm app nocturne-platform production-preflight
The preflight command runs strict production config checks plus the deeper local doctor. It returns non-zero for blocking failures. Warnings are still operator review items; do not ignore a bind/network warning unless the app is definitely reachable only through a private Docker network or host-level HTTPS proxy.
Only then connect the HTTPS reverse proxy
- Terminate public TLS at the reverse proxy and forward to the private app service or loopback app port. Start from the reverse proxy edge runbook when possible.
- Set proxy body limits equal to or stricter than the app: 16 KiB default forms and 8 KiB public report posts.
- Keep proxy logs purpose-bound and short-retention; avoid persistent raw IP/user-agent ledgers unless an incident override is explicitly needed.
- Forward only the paths intended for public/member traffic. Keep shell, database, backups, and private media root inaccessible from the public web server.
- After proxy attachment, run external edge smoke from outside the VPS network; create a disposable member with
smoke-member-provision --recovery-key-file ..., run external member smoke, clean up withsmoke-member-cleanup --confirm-smoke-member-cleanup, record each pass withdeployment-evidence-record, record sealed dump/media hashes withbackup-manifest-record, record encrypted/offsite custody withbackup-custody-record, rundeployment-evidence-check, then runprivate-vps-readinessbefore widening private testing.
Do not expose when any of these are true
vps-bootstrap-planordeployment-rehearsal-planhas any failed checks.production-preflighthas any failed checks.- The public origin is still a placeholder, temporary domain, localhost, or non-HTTPS.
- The owner dashboard gate is unset or owner sessions exceed one hour.
- Backups are still
localposture or configured backup target is unwritable. - The app/Postgres/Redis ports are directly published to the internet.
- No trusted rollback path exists for the current image, database, and media root.