Compare Config Files Across Environments: Finding Drift Between Dev, Staging, and Prod
"But it works on staging" is rarely a mystery about code — it is usually a mystery about configuration. Environments that began as copies of each other drift apart one hotfix at a time: a timeout raised during an incident and never propagated, a feature flag flipped in dev and forgotten, a cache setting that exists only in production. This guide shows how to diff configs across environments safely (secrets!), accurately (key order!), and automatically (CI!), ending with a worked .env example.
How Environment Drift Happens
- Incident hotfixes — a value tuned directly on production at 2 a.m. and never backported to staging or the repo.
- One-way promotions — a new key added to dev for a feature, promoted to staging, but the production deploy checklist missed it.
- Manual edits — anyone with server access changing a file by hand, outside version control.
- Abandoned experiments — flags and keys from features that shipped, changed, or died, still lingering in one environment.
- Different owners — infra manages prod configs, developers manage dev configs, and nobody owns keeping them aligned.
The symptom is always the same: behaviour differs between environments and the code is identical. The cure is a disciplined diff.
Step 1: Redact Secrets Before Diffing
Config files are where secrets live — database passwords, API keys, signing tokens. Before pasting configs into any tool (or even attaching them to a ticket), replace secret values with placeholders. There is a bonus: secrets are supposed to differ between environments, so redacting them also removes expected noise from the diff.
# Quick redaction with sed (keys containing SECRET/PASSWORD/KEY/TOKEN) sed -E 's/^(.*(SECRET|PASSWORD|KEY|TOKEN)[^=]*)=.*/\1=[REDACTED]/' .env.prod DB_PASSWORD=[REDACTED] STRIPE_API_KEY=[REDACTED] SESSION_TIMEOUT=3600 # non-secret values stay visible
Even with redaction, prefer a client-side diff tool that never uploads your text — the Dev Brains AI Diff Checker runs entirely in your browser.
Step 2: Normalise Structured Configs (Sort the Keys)
A line diff compares text, not meaning. In JSON and most YAML mappings, key order is irrelevant to the application — but if prod lists keys alphabetically and staging lists them chronologically, a naive diff reports everything as changed. Normalise first:
# JSON: sort keys and pretty-print with jq jq --sort-keys . config-staging.json > staging.norm.json jq --sort-keys . config-prod.json > prod.norm.json diff -u staging.norm.json prod.norm.json # YAML: sort keys with yq yq 'sort_keys(..)' config-staging.yml > staging.norm.yml # .env files: plain sort works sort .env.staging > staging.sorted sort .env.prod > prod.sorted
After normalisation, every line the diff flags is a real difference. For JSON-specific techniques see comparing two JSON objects.
Worked Example: Two .env Files
Here are staging and production files after redacting secrets and sorting keys:
# staging.sorted # prod.sorted
CACHE_TTL=300 CACHE_TTL=60
DB_HOST=db.staging.internal DB_HOST=db.prod.internal
DB_PASSWORD=[REDACTED] DB_PASSWORD=[REDACTED]
ENABLE_NEW_CHECKOUT=true ENABLE_NEW_CHECKOUT=false
LOG_LEVEL=debug LOG_LEVEL=warn
MAX_UPLOAD_MB=25 MAX_UPLOAD_MB=10
PAYMENT_RETRIES=3 PAYMENT_RETRIES=3
RATE_LIMIT_PER_MIN=120The diff surfaces four kinds of finding, each needing a different response:
- Expected differences —
DB_HOSTandLOG_LEVELare supposed to differ per environment. Fine. - Suspicious value drift —
CACHE_TTL300 vs 60 andMAX_UPLOAD_MB25 vs 10: are these deliberate tuning or a forgotten hotfix? Someone must decide and document. - Feature flag divergence —
ENABLE_NEW_CHECKOUTis on in staging and off in prod. Staging is not testing what production runs. - Missing key —
RATE_LIMIT_PER_MINexists only in prod. If code reads it with a default fallback, staging silently behaves differently.
Step 3: Automate Drift Detection in CI
A manual diff finds today's drift; automation prevents next month's. The trick is comparing keys and structure on a schedule, while ignoring values that legitimately differ:
#!/bin/sh # ci/check-config-drift.sh — fail if key sets differ cut -d= -f1 .env.staging | sort > /tmp/staging.keys cut -d= -f1 .env.prod | sort > /tmp/prod.keys if ! diff -u /tmp/staging.keys /tmp/prod.keys; then echo "CONFIG DRIFT: key sets differ between staging and prod" exit 1 fi
- Run it on every PR that touches config, plus a nightly schedule to catch manual server edits.
- Maintain a small allowlist of keys whose values may differ (hosts, log levels), and alert when other values diverge.
- Better long-term: generate all environment configs from one template plus per-environment overrides, so drift becomes structurally impossible.
Frequently Asked Questions
Configuration drift is when environments that should be structurally identical — dev, staging, production — slowly diverge: a flag flipped in one place, a timeout tuned during an incident, a key added to staging but never promoted. Drift is a leading cause of works-on-staging, fails-on-prod bugs.
Redact secret values before diffing: replace passwords, API keys, and tokens with a placeholder like [REDACTED]. Since secrets are supposed to differ between environments anyway, redacting removes noise as well as risk. Then use a client-side tool such as the Dev Brains AI Diff Checker so the text never leaves your browser.
Key order carries no meaning in JSON and most YAML mappings, but a line diff compares text, not meaning. If two files list the same keys in different orders, the diff shows dozens of false differences. Normalising with a tool like jq --sort-keys or yq sort_keys makes the diff show only real value differences.