Definition: Why One .env Is Not Enough
Real applications run in at least three environments: local development, staging (pre-production testing), and production. Each environment needs different configuration — different database URLs, different API keys, different log levels, different feature flags.
Using a single .env file for everything means either hardcoding environment-specific values (breaking other environments) or manually swapping files before each deployment (error-prone and slow).
The goal: Each environment automatically loads the right configuration without manual intervention. Developers pull the repo and start working. CI/CD deploys without touching .env files. Production secrets never touch developer machines.
What: The Standard .env File Hierarchy
The Node.js ecosystem (and Next.js in particular) has converged on a standard set of .env file names that serve distinct purposes:
.envCommit to git
Base defaults for all environments. Contains non-sensitive, non-secret values that work as sensible defaults everywhere. Every developer gets these values automatically when they clone the repo. Example: LOG_LEVEL=info, PORT=3000
.env.localNEVER commit
Machine-specific overrides and secrets. Highest priority — overrides everything. Contains real API keys, local database credentials, personal config. Each developer has their own. Gitignored by default in Next.js projects. Example: DATABASE_URL=postgresql://localhost/mydb_dev
.env.developmentCan commit (no secrets)
Development-specific defaults. Only loaded when NODE_ENV=development. Contains non-secret dev-mode config. Example: NEXT_PUBLIC_API_URL=http://localhost:4000, DEBUG=true
.env.productionCan commit (no secrets)
Production-specific non-secret defaults. Only loaded when NODE_ENV=production. Real production secrets go in the hosting platform dashboard, NOT here. Example: NEXT_PUBLIC_API_URL=https://api.myapp.com
.env.exampleCommit — document required vars
Documents all required environment variables with placeholder values and comments. Developers copy this to .env.local and fill in real values. This is the onboarding guide for your environment configuration. Always keep it up to date.
When: Which File Loads in Which Environment
| File | Local dev | CI/CD test | Staging | Production |
|---|---|---|---|---|
| .env | ✓ | ✓ | ✓ | ✓ |
| .env.local | ✓ (highest priority) | ✗ (test) | Not present | Not present |
| .env.development | ✓ | ✗ | ✗ | ✗ |
| .env.test | ✗ | ✓ | ✗ | ✗ |
| .env.production | ✗ | ✗ | ✓ (if NODE_ENV=production) | ✓ |
How To: Set Up Multiple .env Files
Method 1: Plain dotenv with NODE_ENV
In a plain Node.js app, load the environment-specific file based on NODE_ENV:
// src/env.ts or server.ts — top of your entry file
import dotenv from 'dotenv'
import path from 'path'
// Load base defaults
dotenv.config({ path: path.resolve(process.cwd(), '.env') })
// Load environment-specific overrides
// Values here override the base .env
dotenv.config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV}`),
override: true,
})
// Load local overrides (gitignored, highest priority)
dotenv.config({
path: path.resolve(process.cwd(), `.env.${process.env.NODE_ENV}.local`),
override: true,
})
dotenv.config({
path: path.resolve(process.cwd(), '.env.local'),
override: true,
})Method 2: dotenv-flow (Automatic Cascading)
Install dotenv-flow for automatic multi-file cascading:
npm install dotenv-flow// server.ts
import 'dotenv-flow/config'
// That's it! Automatically loads in order:
// .env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local
// Each file overrides the previous
// Start with NODE_ENV:
// NODE_ENV=development node server.ts
// NODE_ENV=production node server.ts
// NODE_ENV=test node server.tsMethod 3: Next.js (Built-In — No Package Needed)
Next.js handles multi-env loading automatically. Just create the files:
# Project structure
myapp/
├── .env # Base defaults (commit)
├── .env.local # Local secrets (gitignore)
├── .env.development # Dev non-secrets (commit)
├── .env.production # Prod non-secrets (commit)
├── .env.example # Template (commit)
└── package.json
# .env (base — committed)
NEXT_PUBLIC_APP_NAME=MyApp
LOG_LEVEL=info
PORT=3000
# .env.development (committed — no secrets)
NEXT_PUBLIC_API_URL=http://localhost:4000
NEXT_PUBLIC_ENVIRONMENT=development
DEBUG=true
# .env.production (committed — no secrets)
NEXT_PUBLIC_API_URL=https://api.myapp.com
NEXT_PUBLIC_ENVIRONMENT=production
DEBUG=false
# .env.local (gitignored — real secrets)
DATABASE_URL=postgresql://localhost:5432/myapp_dev
STRIPE_SECRET_KEY=sk_test_xxxxx
AUTH_SECRET=my-dev-secret-at-least-32-chars-longMethod 4: .env.example — Onboarding Template
# .env.example — commit this, never .env.local!
# Copy this file to .env.local and fill in real values
# ── Required ──────────────────────────────────────────
# PostgreSQL connection string
DATABASE_URL=postgresql://user:password@localhost:5432/myapp
# Auth secret (minimum 32 characters, generate with: openssl rand -base64 32)
AUTH_SECRET=
# Stripe
STRIPE_SECRET_KEY=sk_test_...
# ── Optional ──────────────────────────────────────────
# Redis (leave empty to disable caching)
REDIS_URL=
# Email provider
SMTP_HOST=localhost
SMTP_PORT=1025Method 5: GitHub Actions CI/CD
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Uses production secrets
steps:
- uses: actions/checkout@v4
- name: Install dependencies
run: npm ci
- name: Build
env:
# Secrets from GitHub repository Settings → Secrets
DATABASE_URL: ${{ secrets.DATABASE_URL }}
STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
AUTH_SECRET: ${{ secrets.AUTH_SECRET }}
# Non-secret env vars inline
NODE_ENV: production
NEXT_PUBLIC_API_URL: https://api.myapp.com
run: npm run build
- name: Deploy
run: npm run deploy.gitignore — What to Always Exclude
# .gitignore
# Environment variables — NEVER commit these
.env.local
.env.*.local
# These are safe to commit (no secrets):
# .env
# .env.development
# .env.production
# .env.test
# .env.exampleBest Practice: Store real production secrets in your hosting platform dashboard (Vercel, Railway, Render) — not in any committed file. Use .env.local for local dev secrets. Commit .env, .env.development, .env.production only with non-sensitive defaults. Always maintain an up-to-date .env.example.
Why: Real Risks of One .env for Everything
Production DB in local .env
Developer accidentally runs a migration script against the production database URL that was copied into their .env. Data loss. No rollback.
Secrets committed to git
A single .env with real secrets gets committed when a developer forgets it's not gitignored. Even a quick delete and push leaves the secret in git history.
"Works on my machine"
Each developer has different local values. Without a consistent structure, onboarding requires manual instructions and debugging instead of a documented .env.example.
Staging pointing at prod DB
Manual .env swapping before deploy is forgotten. Staging tests run against the production database. Real user data mutated by test scripts.
Frequently Asked Questions
What is the difference between .env.local and .env in Next.js?
.env is committed to git with non-secret defaults. .env.local is gitignored and holds machine-specific secrets with highest priority. Never put real secrets in .env.
How do I use different .env files for development and production?
In Node.js: use dotenv with path: `.env.${NODE_ENV}`. In Next.js: create .env.development and .env.production — the framework loads them automatically based on NODE_ENV.
What is dotenv-flow?
dotenv-flow automatically loads cascading .env files in order: .env → .env.local → .env.{NODE_ENV} → .env.{NODE_ENV}.local. Mimics Next.js built-in behavior for plain Node.js apps.
Should I commit .env files to git?
Only commit .env files with NO secrets: .env, .env.development, .env.production, .env.example. Never commit: .env.local, .env.*.local, or any file with real credentials.
How do I set environment variables in GitHub Actions?
Add secrets in repo Settings → Secrets and variables → Actions. Reference as $${{ secrets.MY_SECRET }} in workflow env blocks. Use GitHub Environments for separate secrets per environment (staging vs production).
Related guides & tools
Share this article with Your Friends, Collegue and Team mates
Stay Updated
Get the latest tool updates, new features, and developer tips delivered to your inbox.
Occasional useful updates only. Unsubscribe in one click — we never sell your email.
Feedback for Multiple .env Files Guide
Tell us what's working, what's broken, or what you wish we built next — it directly shapes our roadmap.
Good feedback is gold — a rough edge you hit today could be smoother for everyone tomorrow.
- Feature ideas often jump the queue when lots of you ask.
- Bug reports with steps get fixed faster — paste URLs or examples if you can.
- Name and email are optional; we won't use them for anything except replying if needed.