Format .env Files — Duplicate Keys and Silent Overrides
Two API_KEY= lines in .env. Staging worked for months on the first. Someone appended a second “temporary” key at the bottom. Last wins. Deploy. Auth dies. Nobody “changed” the app — the file lied by repetition.
Last-wins is the footgun
API_KEY=sk_old_still_in_docs
# temporary rotation test
API_KEY=sk_new_actually_used
You get the newer value; the first line becomes dead weight. Runbooks say “update API_KEY” and people edit the stale assignment. Months later someone deletes the “temporary” bottom line and production flips back to the ancient key.
| Symptom | Likely cause |
|---|---|
| Local works, CI fails | Different files / missing keys |
| “I updated the key” no effect | Edited the earlier duplicate |
| Random env in containers | Orchestrator injects after file load |
| Staging and prod disagree | Dashboard shadows the file on one side |
Format, sort, and spot duplicates with an env file formatter before you diff against .env.example.
Formatting habits that scale
Sort keys within groups (app, data, third parties) so diffs stay readable. One key, one line — no duplicates across groups. Keep a final newline. When a key is retired, delete it from local files and .env.example in the same change. Leaving commented-out live secrets “just in case” is how yesterday’s rotation becomes tomorrow’s leak.
Quotes, spaces, and multiline values
PORT=3000
DISPLAY_NAME="Ada Lovelace"
# API_KEY= sk_live ← leading space may become part of the value
Trim blindly only when values should not contain intentional whitespace. Multiline PEM material belongs in a secret manager when possible; if it must live in dotenv, test the exact loader your runtime uses. After formatting, boot once against a throwaway key and log secret length (never the value) to confirm stray quotes did not attach.
Some loaders expand $VAR inside values; others treat dollars literally. Know which behavior you rely on before “cleaning up” quotes.
Comments, examples, and safety
Comments that describe a key are fine. Comments that paste the live key are leaks. Slack pastes of a full .env mean rotate everything in that paste. .env.example should list every required key with dummy values. Add a lightweight CI check that example keys match what the app expects, so juniors do not invent duplicates “just to be safe.”
Loader precedence and multi-file layouts
| Runtime | Notes |
|---|---|
Node dotenv | Last wins; often does not override existing process.env |
| Docker Compose | env_file + shell env merge rules |
| Vercel / CF / Heroku | Dashboard vars often override files |
systemd EnvironmentFile | Stricter quote rules |
Document merge order for .env + .env.local + .env.production. Duplicates across files are as dangerous as duplicates inside one file.
Pre-deploy checklist
- No duplicate keys (
cut -d= -f1 .env | grep -v '^#' | sort | uniq -d) -
.envnot staged -
.env.exampleupdated for new keys - Quotes correct for spaces/multiline
- Diff against previous known-good export
- Secret scanning on +
.envin.gitignore
Onboarding should generate .env from .env.example with a script — not by copying a teammate’s laptop file. Local .env is for DX; production should inject from a vault. Make the file boring: sorted, unique, quoted on purpose, never committed with live credentials. Silent overrides are worse than loud parse errors.
CI check
Add a job that fails when .env.example is unsorted or contains duplicate keys. Humans will not remember; automation will. Keep production secrets out of that file forever.