Back to Blog
Basics8 min readMay 10, 2026

The Difference Between .env, .env.local, and .env.production

I still see PRs where someone renames .env.local to .env.production and calls it done. The two files load at different times, get treated differently by every framework below, and mixing them up is how test credentials end up live.

The .env File Family

Five files, five jobs. The table below is the version I wish I'd had pinned above my desk during my first few Next.js projects:

FilePurposeCommit to Git?
.envDefault values for all environmentsSometimes (with safe defaults)
.env.localLocal machine overrides with secretsNever
.env.developmentDevelopment-specific valuesSometimes
.env.productionProduction-specific valuesRarely (prefer platform settings)
.env.exampleTemplate with placeholder valuesAlways

Next.js Environment Files

Next.js loads four tiers of .env files in a fixed order, and whichever loads last wins for any variable defined in more than one:

# Loading order (top = loaded first, bottom = highest priority)
1. .env
2. .env.local
3. .env.development / .env.production / .env.test
4. .env.development.local / .env.production.local / .env.test.local

Key Rules for Next.js

  • .env.local is NOT loaded during next build - use .env.production.local instead
  • Test environment uses .env.test (not .env.local to ensure consistent test behavior)
  • Only variables prefixed with NEXT_PUBLIC_ are exposed to the browser
  • Server-side variables are available in API routes, getServerSideProps, etc.

Example Setup

# .env (committed - safe defaults)
NEXT_PUBLIC_SITE_NAME=My App
NEXT_PUBLIC_API_URL=https://api.example.com
# .env.local (never committed - your secrets)
DATABASE_URL=postgres://user:pass@localhost:5432/mydb
AUTH_SECRET=your-super-secret-key
STRIPE_SECRET_KEY=sk_test_xxx
# .env.development (committed - dev-specific non-secrets)
NEXT_PUBLIC_API_URL=http://localhost:3001
LOG_LEVEL=debug

Vite Environment Files

Vite uses a similar system but with the VITE_ prefix instead of NEXT_PUBLIC_:

# Loading order
1. .env
2. .env.local
3. .env.[mode] (e.g., .env.development, .env.production)
4. .env.[mode].local

Accessing Variables in Vite

// In your Vite application
const apiUrl = import.meta.env.VITE_API_URL;
const mode = import.meta.env.MODE; // 'development' or 'production'
const isDev = import.meta.env.DEV; // boolean
const isProd = import.meta.env.PROD; // boolean

Custom modes can be used with vite build --mode stagingwhich will load .env.staging.

Create React App

CRA uses the REACT_APP_ prefix and has a simpler loading order:

# npm start (development)
.env.development.local
.env.local
.env.development
.env
# npm run build (production)
.env.production.local
.env.local
.env.production
.env

Note: CRA embeds environment variables at build time, not runtime. Changing .env files requires a rebuild.

Best Practices

Keep Secrets Out of Version Control

Commit only the files that hold no real secrets. Document what's missing with .env.example instead:

# .gitignore
.env.local
.env.*.local
.env.development
.env.production

Let .env.local Hold Your Personal Setup

Your .env.local can point at your own database, your own test API keys, whatever your machine needs, without touching what teammates have configured on theirs.

Leave .env.production for Non-Secrets

Route real production values through your host's environment variable settings rather than a .env.production file. That gets you:

  • Encryption at rest
  • Access controls and audit logging
  • No risk of accidental commits
  • Easy rotation without code changes

Document Everything in .env.example

Maintain one .env.example that lists every required variable with a comment on where each value comes from:

# .env.example
# Database
# Format: postgres://USER:PASSWORD@HOST:PORT/DATABASE
DATABASE_URL=postgres://localhost:5432/myapp
# Authentication (generate with: openssl rand -base64 32)
AUTH_SECRET=replace-with-random-string
# Stripe (get from https://dashboard.stripe.com/apikeys)
STRIPE_SECRET_KEY=sk_test_xxx
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_xxx

Common Mistakes to Avoid

  • Relying on .env.local in CI/CD: most frameworks skip it in test and build environments, so a pipeline that only works locally is usually missing a .env.production.local or platform variable it never needed on your machine.
  • Forgetting the client prefix: a variable without NEXT_PUBLIC_, VITE_, or REACT_APP_ stays server-only no matter how correct the value is, and the browser never sees it.
  • Expecting a save to update production:most frameworks bake env vars into the build. Edit .env.production after deploying and you're editing a file nobody reads until the next build.
  • Mixing secrets into public files: a server-only secret dropped into the same file as your NEXT_PUBLIC_ variables is one typo away from shipping to the browser.

Quick Reference

When you can't remember which file does what, this is the shortcut version of the table above:

  • .envholds safe defaults you don't mind committing.
  • .env.local holds your secrets and never gets committed.
  • .env.development / .env.production hold values specific to one environment, still no secrets.
  • .env.example documents the shape for everyone else and always gets committed.
  • Real production secrets live in your host's dashboard, not in a file with a dot in front of its name.