NodeJSProduction Configuration

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
A `.env` file on a production server is a secret stored in plaintext on disk — any code execution or path traversal exposes it
It's tempting to simply `scp .env` to the production server and call it done, but a `.env` file on a server is a **plaintext file of secrets sitting on disk**, readable by anything that can execute code on that machine or traverse the filesystem — a compromised library, a path traversal bug in your app, a backup that gets over-shared, a log that captures `cat .env`. There's also no audit trail (who changed it? when?), no rotation history, and no way to limit access. Platform environment variable stores and secrets managers solve all of these: secrets are encrypted at rest and in transit, access is access-controlled (IAM), every read and write is logged, and rotation can be automated. The operational cost is minimal; the security improvement is substantial. `.env` files belong in local development only.
Platform environment variable stores

Text
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.
Every platform has a built-in env var store — prefer it over files; your code always reads `process.env` regardless of how vars were injected
The simplest production config path is the **platform's own environment variable store**: a UI or CLI where you set `KEY=value` pairs, and the platform injects them into every container/dyno/instance of your app at startup. Your code is unchanged — it still reads `process.env.DATABASE_URL` — but the value comes from the platform rather than a file. This is enough for most teams: access is limited by your account permissions, values are encrypted at rest, and they aren't stored in your repo or on disk in plaintext. The main limitation is granularity — there's often no per-secret access control or rotation automation. For that, use a dedicated secrets manager and reference it from the platform store.
Dedicated secrets managers

JS
// 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}@...`
Secrets managers (AWS SM, Vault, GCP SM) encrypt, version, audit, and automate rotation — the right layer for high-value credentials
For high-value secrets — production database passwords, signing keys, third-party API keys — a **dedicated secrets manager** provides what platform env var stores don't: **encryption with a managed key**, **fine-grained IAM access control** (only this Lambda role can read this secret), **audit logs** of every access, **versioning** of secret values, and **automated rotation** (AWS Secrets Manager can rotate an RDS password on a schedule without you writing any code). The cost is a call to the secrets manager API at startup (or using the Lambda/ECS Secrets Manager agent to inject them as environment variables automatically). Most production systems that handle sensitive data end up here — it's the operationally correct baseline for anything you'd be embarrassed to see in a breach report.
Config promotion — dev → staging → production

Environment

Config source

Secret values

Development

.env file (git-ignored)

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

Each environment has its own separate credentials — staging and production are never the same database, never the same keys
A **config promotion model** treats each environment as fully isolated. Dev and CI use throwaway credentials that can be committed to your config templates or CI secrets without consequence. **Staging should mirror production in structure** — the same services, the same secrets-manager integration, the same `NODE_ENV=production` — but with *separate credentials*. The staging database is not the production database; the staging Stripe key is the test-mode key. This means a misconfiguration or a staging incident can't corrupt production data, and you can safely test config changes in staging before they go live. The test for good config promotion: could you accidentally run staging code against production data? If yes, the environments aren't isolated enough. Keep credentials per-environment and never share them across the boundary.
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.

Rotate secrets with a generate→deploy→verify→revoke sequence — never revoke the old credential before the new one is live everywhere
Credential rotation is a normal operational task, not an emergency. The safe sequence — **generate, deploy, verify, revoke** — keeps the service live throughout. Create the new credential in the upstream system first (it now has two valid credentials), update the secret in the secrets manager, do a **rolling restart** so instances pick up the new value one at a time without a full outage, confirm all instances are healthy and using the new credential (check logs for auth errors), then revoke the old one. The danger is rushing to revoke before the rollout completes: an instance still using the old credential against a revoked secret causes auth failures. Automate rotation on a schedule (AWS Secrets Manager and Vault both support this natively for common databases) so it happens regularly without a human having to remember to do it.
Next
Now structure the code itself well: [Project Structure](/nodejs/project-structure).