Env backup and secrets
Production secrets live in 1Password (or Vault, Doppler, Infisical, SOPS - same shape). Your VPS keeps a compose/.env.production file with only op:// references; op run expands them at process start. No raw values touch disk, no rotating via vi, no leaks via cat.
For the reasoning behind this pattern, see Secrets management.
Steady-state flow
Section titled “Steady-state flow”After provisioning, the file on the VPS is:
/opt/boringstack/infra/compose/compose/.env.productionPermissions 0600. Contents are just references:
POSTGRES_PASSWORD=op://Production/Postgres/passwordJWT_SECRET=op://Production/Auth/jwt_secretGOOGLE_OAUTH_CLIENT_SECRET=op://Production/OAuth/google_client_secretSTRIPE_SECRET_KEY=op://Production/Stripe/secret_keyWUD_GHCR_TOKEN=op://Production/GHCR/tokenRESEND_API_KEY=op://Production/Email/resend_api_keyThis file contains zero secrets. You can share it on Slack, commit it to a private repo, or print it. All actual values live in the Production 1Password vault, accessed by a service-account token.
Boot the stack
Section titled “Boot the stack”export OP_SERVICE_ACCOUNT_TOKEN=... # from systemd or ~/.bashrccd /opt/boringstack/infra/compose/composeop run --env-file=.env.production -- docker compose --profile prod up -dThe op CLI resolves each op:// reference, expands the env, and passes it to docker compose. The resolved values never touch disk or shell history. The compose-up.sh wrapper detects OP_SERVICE_ACCOUNT_TOKEN and prefixes this automatically.
What backups don’t cover
Section titled “What backups don’t cover”Postgres backups (see Backups) capture database rows only. They do not include the 1Password vault. Secrets and data live in separate stores, restored independently.
If you restore Postgres without the 1Password vault, the database comes back but the app can’t sign cookies or call external APIs. If you lose the vault, the data is still safe in Postgres.
Back up 1Password itself per their recommendations: emergency kit printed and stored offline, multiple vault administrators, account recovery enabled.
Rotating secrets
Section titled “Rotating secrets”To rotate any secret:
- Edit the item in 1Password.
- On the VPS:
op run --env-file=.env.production -- docker compose --profile prod up -d. - The container restarts with the new value.
Rotation cadence:
| Secret | Cadence | Notes |
|---|---|---|
| JWT_SECRET | On compromise only | Invalidates all sessions |
| POSTGRES_PASSWORD | Annual | Update vault + ALTER USER together |
| OAuth secrets | When provider prompts | Rotate at provider first, then vault |
| GHCR_TOKEN | 90 days | PAT with read:packages scope |
| Email API tokens | Per provider policy | Cloudflare/Resend/SendGrid dashboards |
Always provision the new value at the provider before revoking the old one so mid-rotation traffic doesn’t fail.
Adding an operator
Section titled “Adding an operator”When someone joins your team:
- Add them to the
Production1Password vault (read-only, or read-write if they rotate secrets). - Grant SSH access separately (see Firewall & TLS).
- Share the boot command:
op run --env-file=.env.production -- docker compose --profile prod up -d.
When someone leaves:
- Remove them from the 1Password vault.
- The service-account token on the VPS stays valid; rotate only secrets they could have read locally (which should be none if they only had vault access).
Break-glass: exporting secrets to disk
Section titled “Break-glass: exporting secrets to disk”Only for rare situations: auditing a historical deploy, or needing to recover without 1Password access.
Generate a one-time resolved copy:
# On the VPSop run --env-file=.env.production -- env | grep -v '^OP_' > /tmp/resolved.envchmod 600 /tmp/resolved.env
# Encrypt with ageage -r <your-age-public-key> /tmp/resolved.env > resolved.env.age
# Delete the plaintextshred -u /tmp/resolved.envTreat the .age file like a backup tape: encrypted, off-host, delete the plaintext immediately.
Can I use Bitwarden / Vault / Doppler / SOPS instead?
Yes. The pattern is identical: references on disk, CLI resolves at boot. See Secrets management.
What if 1Password is down?
The op CLI caches vault contents briefly after a successful run. For longer outages, use the break-glass backup above. Plan and drill it like you drill database restores.
Can I commit .env.production to git?
Yes, it’s just references. Many teams version it alongside code. Treat git as the truth-of-references; 1Password as the truth-of-values.
Do I need this for local dev?
Optional. See Secrets management → Local dev. Solo developers can use op run --env-file=.env.local or fall back to a raw .env - never in production.
Related
Section titled “Related”- Secrets management - the full pattern and alternatives.
- Backups - Postgres to rclone offsite.
- Provisioning with OpenTofu - how cloud-init renders
.env.production.