Skip to main content
Security

Env files and secrets management: `.env` is configuration, not a vault

Keep local configuration convenient without turning Git history, CI logs, and container images into a long-lived archive of production credentials.

Thien Nguyen
By Thien Nguyen
Updated July 21, 2026 · 2 min read

.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.

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 ever committed—even if the repository is private.

Give every value a home

ValueGood homeWhy
LOG_LEVEL=debugVersioned config or .env.exampleNot sensitive; reviewable
Local database passwordIgnored local .envConvenient for a developer machine
Production API keySecret manager / platform secretAudited access and rotation
Browser-exposed public identifierBuild-time public configIt 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.

Configuration needs validation too

Parse and validate configuration once at startup. A missing integer, malformed URL, or production default that silently falls back to development is an outage waiting for traffic. Report the variable name and expected shape, never the secret value.

if (!process.env.DATABASE_URL?.startsWith('postgres://')) {
  throw new Error('DATABASE_URL must be a PostgreSQL URL');
}

Keep runtime secrets out of container image layers and build logs. A multi-stage Dockerfile does not help if a build argument or copied .env file leaves the secret in image history.

Cover photo by Nathan Thomas on Pexels.

References

Primary documentation and specifications checked when this article was last updated.

SecurityConfigurationDevOps

Related articles

All articles