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=120

The diff surfaces four kinds of finding, each needing a different response:

  • Expected differencesDB_HOST and LOG_LEVEL are supposed to differ per environment. Fine.
  • Suspicious value driftCACHE_TTL 300 vs 60 and MAX_UPLOAD_MB 25 vs 10: are these deliberate tuning or a forgotten hotfix? Someone must decide and document.
  • Feature flag divergenceENABLE_NEW_CHECKOUT is on in staging and off in prod. Staging is not testing what production runs.
  • Missing keyRATE_LIMIT_PER_MIN exists 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

What is configuration drift?

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.

How do I diff config files that contain secrets safely?

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.

Why should I sort keys before comparing JSON or YAML configs?

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.

Try the Free Diff Checker

Paste two configs and compare them line by line in your browser — your data never leaves your machine. No signup, no cost.

Related articles