.env files are useful because they make local setup boring. They become dangerous when a production secret survives in a commit, a Docker layer, a support archive, or a copied sample file for years.
Short answer: keep a committed .env.example with names and safe placeholders, keep real .env files ignored, inject production secrets from the deployment platform or a secret manager, and rotate anything that was committed—even if the repository is private.
Give every value a home
| Value | Good home | Why |
|---|---|---|
LOG_LEVEL=debug | Versioned config or .env.example | Not sensitive; reviewable |
| Local database password | Ignored local .env | Convenient for a developer machine |
| Production API key | Secret manager / platform secret | Audited access and rotation |
| Browser-exposed public identifier | Build-time public config | It is not a secret—name it honestly |
# .env.example — commit this
DATABASE_URL=postgres://app:password@localhost:5432/app
STRIPE_SECRET_KEY=replace-me
The example must be usable documentation, not a museum of variables no code reads. Validate startup configuration and fail with the variable name—not its value—when something is missing.
Base64 is not secret storage. Neither are encrypted files if the decryption key lives beside them in the same CI environment.
The incident response nobody enjoys
If a credential reaches Git, deleting the line is not remediation: clones, forks, caches, and prior commits may retain it. Revoke or rotate first, then remove it from current config, audit its use, and consider history cleanup as damage reduction rather than the fix.
For local hygiene, run a secret scanner in pre-commit and CI. For production, prefer short-lived workload identities over static keys when the platform supports them. The important property is not where a string is stored; it is who can retrieve it, how long it works, and how quickly you can revoke it.
Cover photo by Nathan Thomas on Pexels.
