How to Compare Development and Production .env Files

2026-04-23

Comparing development and production .env files should answer a configuration question without exposing production secrets. The safest comparison uses key names and classifications, not raw values. Never paste complete environment files into an issue, diff website, chat, or pull request.

The Environment Toolkit can help inspect configuration locally in your browser. Use only tools whose data-handling model you trust, and follow your organization's policy for production material.

Compare key sets locally

This Python script parses simple KEY=value files and reports presence only:

from pathlib import Path

def keys(path):
    result = set()
    for raw in Path(path).read_text().splitlines():
        line = raw.strip()
        if not line or line.startswith("#") or "=" not in line:
            continue
        key = line.removeprefix("export ").split("=", 1)[0].strip()
        result.add(key)
    return result

dev = keys(".env.development")
prod = keys(".env.production")

print("Only development:", sorted(dev - prod))
print("Only production:", sorted(prod - dev))
print("Shared:", len(dev & prod))

Run it on a trusted machine with restrictive file permissions:

umask 077
python3 compare_env_keys.py

This parser intentionally does not attempt shell expansion, multiline values, or every dotenv dialect. For complex files, use the same dotenv parser as the application—but ensure comparison code never executes substitutions or commands.

Compare safe properties instead of values

Presence alone misses type and format drift. Produce a sanitized manifest that records properties such as “set,” “empty,” or “looks like URL”:

from urllib.parse import urlparse

SECRET_WORDS = ("SECRET", "TOKEN", "PASSWORD", "KEY", "CREDENTIAL")

def classify(name, value):
    if not value:
        return "empty"
    if any(word in name.upper() for word in SECRET_WORDS):
        return "sensitive:set"
    parsed = urlparse(value)
    if parsed.scheme and parsed.netloc:
        return f"url:{parsed.scheme}"
    if value.lower() in {"true", "false"}:
        return "boolean"
    return "set"

Do not print lengths, prefixes, hashes, or partial secret values casually. They can leak information or provide stable identifiers that become sensitive themselves. If exact equality must be checked, run that check inside the controlled production environment and emit only the narrow result needed.

The environment comparison recipe offers a structured workflow for inventories and review.

Define an environment contract

Maintain a committed template containing placeholders and documentation:

# Public origin, including scheme; required
APP_BASE_URL=https://example.test

# Secret-manager reference or injected value; required, never commit the value
DATABASE_URL=

# Boolean: true or false
FEATURE_CHECKOUT=false

Validate configuration at application startup with a schema. Mark variables as required, optional, secret, environment-specific, or client-exposed. For frontend builds, remember that many frameworks embed selected environment variables into public JavaScript. A “public” prefix is a disclosure mechanism, not access control.

Review meaningful differences

For every missing or extra key, determine whether it is obsolete, optional, or a deployment defect. Then compare non-secret semantics:

  • Are URLs HTTPS and pointed at the intended environment?
  • Are debug logging and development bypasses disabled?
  • Do timeout and concurrency units match?
  • Are feature flags intentionally different?
  • Are production credentials references, regions, and service names correct?

Avoid copying development values wholesale. Production commonly needs different origins, storage, observability, and resource limits. Conversely, developers should use isolated accounts and fake data, never downloaded production credentials.

Generate the sanitized manifest in each environment and compare those manifests in CI when possible. Fail on missing required keys or invalid types, but report only variable names and validation categories. Assign an owner and reason for every intentional difference. This turns a risky manual inspection into a repeatable configuration check while keeping values inside their existing trust boundaries.

Common mistakes and safer storage

diff .env.development .env.production exposes both values to the terminal, shell scrollback, CI logs, and screen sharing. Committing .env and “removing it later” leaves it in Git history. Add relevant patterns using the Gitignore Builder, but do not rely on ignore rules after a file is already tracked.

Store production secrets in a secret manager or deployment platform, grant least-privilege access, and rotate credentials after suspected exposure. Keep .env.example free of realistic secrets. Delete sanitized working copies when the review is complete according to retention policy.

A good comparison produces an auditable list of configuration drift—not a second, less-protected copy of production secrets.