Production Configuration
The config best practices page covered how to consume configuration in code; this page covers how to supply it in production. A .env file on the server is an anti-pattern — insecure, hard to audit, easy to leak. Production config flows from platform-native env var stores (PaaS dashboards, container environment blocks) and dedicated secrets managers (AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager), injected into the process at startup so the running code never needs to know where its secrets came from. This page covers the threat model, the tooling, config promotion across environments, and secrets rotation without downtime.
Why not a .env file on the server
Platform environment variable stores
Most hosting platforms expose a UI / CLI for environment variables:
Render: Dashboard → Environment → Add env var
Railway: Project → Variables
Fly.io: fly secrets set KEY=value
Heroku: heroku config:set KEY=value
AWS ECS: Task definition → Container → Environment variables
(or reference an SSM Parameter Store / Secrets Manager ARN)
K8s: ConfigMap (non-secret) or Secret (base64, RBAC-controlled)
→ Variables are injected into the container/process at startup.
→ Your code reads them from process.env — identical to local development.Dedicated secrets managers
// AWS Secrets Manager — fetch a secret at startup (or use the Lambda extension to inject):
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager'
async function loadSecret(name) {
const client = new SecretsManagerClient({ region: 'us-east-1' })
const { SecretString } = await client.send(
new GetSecretValueCommand({ SecretId: name }),
)
return JSON.parse(SecretString) // secrets are often stored as JSON
}
// Call at startup, before the server binds:
const dbCreds = await loadSecret('prod/app/database')
process.env.DATABASE_URL = `postgres://${dbCreds.user}:${dbCreds.password}@...`Config promotion — dev → staging → production
Environment | Config source | Secret values |
|---|---|---|
Development |
| Weak local credentials, throwaway keys |
CI / Test | CI platform secrets (GitHub Actions secrets) | Test service credentials, isolated databases |
Staging | Platform env store / secrets manager | Production-like but isolated; separate credentials |
Production | Secrets manager + platform env store | Real credentials, fully access-controlled |
Rotating secrets without downtime
Generate the new credential in the upstream system (new DB password, new API key) before removing the old one.
Deploy the new secret to the secrets manager or platform store.
Rolling restart — the platform restarts instances one at a time, each picking up the new value via
process.env; all instances are briefly running both the old (in-memory) and new (on next restart) credential.Verify all instances are running the new secret (watch logs, check the health endpoint).
Revoke the old credential only after confirming zero instances still hold it.
Keep the transition window short; use connection pooling and retry logic to absorb the brief period of mixed credentials.