The app works on your machine. CI fails because API_URL is missing. Staging points at production Stripe because .env was copied. A secret is in the repo from 2021 and nobody rotated it. Config looks like a beginner topic. It is a common outage class.
This is how to treat environment variables and secrets like part of the architecture, not like a junk drawer.
Config vs secret vs feature flag
Config: non-sensitive settings that change by environment: log level, API_URL, pool size.
Secrets: passwords, tokens, private keys. They go in a secret manager or a sealed env, not in the frontend bundle, not in git, not in Slack.
Flags: product behavior you want to toggle without a deploy. Not everything is a flag. If it needs a restart, it might just be config.
Mixing them in one .env without naming is how a “config change” becomes a leaked token in a screenshot.
Twelve-factor, in practice
Read config from the environment at process start. Fail if required values are missing. Do not silently default DATABASE_URL to production. Do not default secrets to "changeme" in production code paths.
A typed getConfig() that parses env once is better than process.env.FOO in 40 files. You can unit test it with a fake env. You can print a redacted config at startup (DATABASE_URL host only, no password).
.env files
.env is a local convenience. .env.example is the contract: keys, dummy values, comments. CI and prod should not depend on a developer’s .env. They should inject env from the platform.
Never commit .env. If you did, rotate everything in it. git rm --cached does not unpublish history.
env.local vs env.production in Next.js: know which is public. NEXT_PUBLIC_* goes to the browser. A secret with that prefix is a public secret. That bug ships constantly.
Frontend vs backend
The browser cannot keep a server secret. Anything in JS is public. Map APIs, “secret” analytics write keys (often designed to be public), and third-party keys with referrer restrictions. Database URLs do not belong in the client.
If the frontend needs a key, it needs a backend proxy or a key that is safe to expose.
Naming and environments
DATABASE_URL not db. Use the same names in every environment. STAGING_DATABASE_URL in prod is a smell.
Separate APP_ENV=production from NODE_ENV=production. Libraries treat NODE_ENV as “build optimizations.” Your “are we in prod” checks should use an explicit app env so tests can run with NODE_ENV=test and still hit a staging API if you made that mistake—which you should not.
Injection in Docker and Kubernetes
Compose environment and env_file. Kubernetes Secrets vs ConfigMaps. Do not put secrets in ConfigMaps that get dumped in tickets. Mount secrets as env or files. Rotate by deploying new values.
Interpolation in Compose: ${FOO} empty vs unset. Quote values with $ in them.
Validation at boot
If Redis is required, ping it at startup or fail. Lazy failure on the first request at 2 a.m. is worse. Health checks should not need secrets to say “process is up,” but readiness can need the DB.
Log “config loaded, missing optional FOO, using default X” for optional values. Silent defaults are mysterious.
Rotation
Assume leak. Have a way to rotate: new token, deploy, disable old. If a secret is in an old Docker image tag, treat the image as compromised. If it is in CI logs, rotate.
Personal access tokens in git config or in a script in the repo: delete, rotate, add pre-commit secret scanning if the team will tolerate it.
A checklist for a new service
- Required env listed in
.env.example - Boot fails on missing required secrets
- No
NEXT_PUBLICsecrets - Prod values from the platform, not a copied file
- Redacted config dump at info on startup
- Rotation notes in the runbook
Config in the database vs env
Some teams store settings in a config table so product can change them. That is a flag store. Cache it. Audit who changed it. Do not put the database password in the database. Env for secrets, DB for product knobs, files for things that must ship with the version (feature behavior that is code).
If you have all three, document which wins when they conflict. Conflicts are outages.
Whitespace and quoting
.env parsers disagree on quotes, export prefixes, and multiline values. A private key in an env var is a misery; use a file mount. If you must use env, watch \n escaping. A truncated key fails at boot if you validate. If you do not validate, it fails on the first request to the vendor.
Windows vs Linux line endings in .env can introduce a \r on the last value. Trim. This bug is real and insulting.
Public config endpoints
An endpoint that returns public config (feature names, mapbox public token) is fine. An endpoint that returns the server’s entire env is a security incident. Do not build a debug route that prints process.env “only on staging” and then deploy the same binary.
Feature branches and env drift
A preview deploy that does not get the same secrets as staging will fail in a way that looks like your code. Confirm which env the preview uses. If it has no database, you cannot test the migration there. Do not conclude the migration is fine because the preview UI loaded.
Local .env that points at staging databases is convenient and dangerous. You can wipe staging data. Prefer a local DB. If you must use staging, use a role that cannot DROP.
Default values that only exist in one environment
If Kubernetes sets TIMEOUT=30 and local defaults to unlimited, you will not see the timeout until production. Mirror timeouts locally. Put the default in code next to the env read, the same number, so local and prod match unless overridden.
Secret scanning in CI
If the company will allow it, gitleaks or trufflehog on PRs catch keys before they merge. It is not perfect. It is cheaper than a rotation after a public repo. If a scan flags a test fixture JWT, use a clearly fake token. Do not disable the scanner globally for one false positive; exclude the path.
Changing a secret without downtime
If you can accept two secrets for a while, deploy the new one as secondary, switch, then remove the old. If the vendor allows only one key, you will have a blip. Do it in a window. Do not rotate the database password in the middle of a migration.
README vs the platform UI
If production values live only in a cloud console, take a screenshot for the runbook or export a template. A new teammate cannot guess the console. Env is only half of config. The other half is “where do we click.”
Different values per replica
If one pod has an old secret because the rollout stuck, you get flaky auth. Check that all replicas have the same env after a rotate. Kubernetes secrets need a restart of the pods if they do not reload. Know whether your app reads env only at boot.
Artikals is for the config mess because it is unglamorous and it pages people. Treat env as an API. Document it. Fail loud. Keep secrets out of git and out of the bundle. The feature can be clever. The config should be dull.