Skip to content
BoringStack
Star

Env backup and secrets

4 min read

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.

After provisioning, the file on the VPS is:

Terminal window
/opt/boringstack/infra/compose/compose/.env.production

Permissions 0600. Contents are just references:

Terminal window
POSTGRES_PASSWORD=op://Production/Postgres/password
JWT_SECRET=op://Production/Auth/jwt_secret
GOOGLE_OAUTH_CLIENT_SECRET=op://Production/OAuth/google_client_secret
STRIPE_SECRET_KEY=op://Production/Stripe/secret_key
WUD_GHCR_TOKEN=op://Production/GHCR/token
RESEND_API_KEY=op://Production/Email/resend_api_key

This 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.

Terminal window
export OP_SERVICE_ACCOUNT_TOKEN=... # from systemd or ~/.bashrc
cd /opt/boringstack/infra/compose/compose
op run --env-file=.env.production -- docker compose --profile prod up -d

The 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.

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.

To rotate any secret:

  1. Edit the item in 1Password.
  2. On the VPS: op run --env-file=.env.production -- docker compose --profile prod up -d.
  3. The container restarts with the new value.

Rotation cadence:

SecretCadenceNotes
JWT_SECRETOn compromise onlyInvalidates all sessions
POSTGRES_PASSWORDAnnualUpdate vault + ALTER USER together
OAuth secretsWhen provider promptsRotate at provider first, then vault
GHCR_TOKEN90 daysPAT with read:packages scope
Email API tokensPer provider policyCloudflare/Resend/SendGrid dashboards

Always provision the new value at the provider before revoking the old one so mid-rotation traffic doesn’t fail.

When someone joins your team:

  1. Add them to the Production 1Password vault (read-only, or read-write if they rotate secrets).
  2. Grant SSH access separately (see Firewall & TLS).
  3. Share the boot command: op run --env-file=.env.production -- docker compose --profile prod up -d.

When someone leaves:

  1. Remove them from the 1Password vault.
  2. 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).

Only for rare situations: auditing a historical deploy, or needing to recover without 1Password access.

Generate a one-time resolved copy:

Terminal window
# On the VPS
op run --env-file=.env.production -- env | grep -v '^OP_' > /tmp/resolved.env
chmod 600 /tmp/resolved.env
# Encrypt with age
age -r <your-age-public-key> /tmp/resolved.env > resolved.env.age
# Delete the plaintext
shred -u /tmp/resolved.env

Treat 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.