How to manage multiple .env files across dev, staging, and prod

“Just use .env” breaks down the moment you have three environments, six microservices, and a teammate who asks which file was last updated. You need naming, loading order, and boundaries so nobody runs staging migrations against production by accident.

This guide covers patterns, not dogma — pick what matches your stack.


1. Name things so humans cannot confuse them

File naming conventions (common)

FilePurpose
.env.exampleCommitted template; no secrets.
.env.localPersonal overrides; gitignored.
.env.developmentDefaults for dev (sometimes committed if non-secret).
.env.stagingStaging placeholders only if non-secret; secrets via vault/CI.

Rule: if a file can contain a secret, assume it must be gitignored and per-machine or injected.


2. Twelve-factor style: config in the environment

The Twelve-Factor App “config” factor recommends strict separation of code and config that varies between deploys. In practice:

  • Code is in Git.
  • Secrets arrive via environment at runtime (container, PaaS, systemd, CI).

That does not mean “use .env files in production servers” — many platforms inject env without a file.


3. Local development loading order (Node example)

Tools like dotenv often support multiple files with precedence. Typical pattern:

.env                # default local (gitignored)
.env.local          # overrides (gitignored)

In Next.js, documented env file precedence is built-in — see Next.js docs for Environment Variables (behavior evolves; always check your installed version).

Tip: print names of loaded files in debug mode, never values:

echo "Using env files: .env, .env.local"

4. Monorepo layout

Option A: per-package .env

apps/
  web/.env.example
  api/.env.example
packages/
  shared/   # no secrets here

Each app documents its own variables. Root .env rarely scales.

Option B: root .env + app-specific overrides

Works for small teams; becomes political at scale. Prefer explicit per-app config.

Option C: centralized developer vault + export

Store canonical values in a local vault (PassStore), copy into the correct file per session or use small wrapper scripts that export variables for a subshell only.


5. Docker and Compose

docker-compose.yml often uses:

env_file:
  - .env

Keep compose files committed; keep .env gitignored. For teams, provide .env.example with dummy values.

For production, prefer orchestrator secrets (Kubernetes Secret, ECS task secrets, etc.) — not a .env file baked into an image layer.


6. Make targets and shell discipline

Instead of everyone inventing commands, standardize:

.PHONY: dev-api
dev-api:
	set -a && source .env && set +a && npm run dev --workspace api

Caveat: source .env keeps secrets in that shell’s memory — still better than pasting into Slack, but close the terminal when done.


7. Keep production secrets off laptops

Strong recommendation:

  • Developers use scoped dev/staging credentials.
  • Production rotation and access live in cloud IAM / secret manager / break-glass procedures.

If someone must debug prod, use temporary credentials with audit trails and automatic expiry.


8. Mental model: “sources of truth”

EnvironmentSource of truth
Local devDeveloper machine (vault / .env local only)
CIProvider secret store
ProductionCloud secret manager + IAM

When those blur — e.g. prod .env on a laptop — incidents follow.


9. Where PassStore helps

PassStore is built around workspaces and grouped material so your Mac can hold dev-oriented secrets in one encrypted place instead of a fractal tree of .env.staging.bak.final2 files. Pair it with the Git hygiene in Keep secrets out of Git.

Download for macOS · Security overview


Related