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)
| File | Purpose |
|---|---|
.env.example | Committed template; no secrets. |
.env.local | Personal overrides; gitignored. |
.env.development | Defaults for dev (sometimes committed if non-secret). |
.env.staging | Staging 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”
| Environment | Source of truth |
|---|---|
| Local dev | Developer machine (vault / .env local only) |
| CI | Provider secret store |
| Production | Cloud 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