Back to Developer's Study Materials

How to Manage Multiple .env Files for Development, Staging, and Production in Node.js

Stop using one .env for everything — here's the right way (2026)

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

FileLocal devCI/CD testStagingProduction
.env
.env.local✓ (highest priority)✗ (test)Not presentNot 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.ts

Method 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-long

Method 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=1025

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

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

You make the difference

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.