Back to Next.js
Next.js
medium
mid

How does Next.js handle environment variables across .env.local and .env.production?

Next.js loads `.env` files at build time per environment: `.env` (all), `.env.local` (all, gitignored), `.env.development` / `.env.production` (per NODE_ENV). `NEXT_PUBLIC_*` are inlined into the client bundle; everything else is server-only. `.env.local` overrides — for secrets and local config. Build-time inlining means changing public vars requires a rebuild.

3 min read·~5 min to think through

File precedence

Loaded in order; later overrides earlier (for the same key):

ts
.env                  ← shared defaults
.env.development      ← when NODE_ENV=development (dev / next dev)
.env.production       ← when NODE_ENV=production (next build / next start)
.env.test             ← when NODE_ENV=test
.env.local            ← always loaded, overrides above (except during NODE_ENV=test)
.env.development.local
.env.production.local

Convention: commit .env, .env.development, .env.production. Gitignore everything ending in .local — those hold secrets and per-developer overrides.

Public vs server-only

bash
# .env.production
DATABASE_URL=postgres://...     # server-only
NEXT_PUBLIC_API_URL=https://api.example.com   # inlined into client bundle
  • **NEXT_PUBLIC_* — inlined into the JS bundle at build time. Visible to every visitor. Don't put secrets here.
  • Everything else — only available on the server (API routes, getServerSideProps, server components).

Access in code

tsx
// Server-side (route handler, server component)
const dbUrl = process.env.DATABASE_URL;

// Client-side (any "use client" component)
const apiUrl = process.env.NEXT_PUBLIC_API_URL;

Trying to read process.env.DATABASE_URL in a client component returns undefined — Next.js strips it.

Build-time vs runtime

process.env.NEXT_PUBLIC_* is inlined at build time. You can't change the public vars without rebuilding the app. For runtime config on Vercel / serverless, use:

  • A runtime config endpoint the client fetches on mount.
  • The NEXT_PUBLIC_* vars at deploy time (rebuild per env).

Production deployment

On Vercel / Netlify / similar:

  • Set env vars in the dashboard (per environment).
  • They're injected during build (next build).
  • For server-only vars, they're also available at runtime (the Node process reads them).

Edge / runtime variants

For edge runtime or runtime config:

tsx
export const runtime = "edge";
// process.env still works for vars set in deploy config

Security

  • Never commit secrets to .env or .env.production. Use .env.local (gitignored) for local dev; deploy dashboard for prod.
  • Don't prefix secrets with NEXT_PUBLIC_ — they'll end up in the client bundle.
  • Audit the client bundle for accidentally-public vars: grep NEXT_PUBLIC .next/static -r.

Test mode

.env.test loads on NODE_ENV=test. .env.local is skipped under test (so CI doesn't pull dev secrets).

Common pitfalls

  • Putting secrets in NEXT_PUBLIC_* by accident.
  • Expecting client-side changes to env without rebuild.
  • .env.local not gitignored — committed secrets.
  • Trying to use process.env for runtime config that should be fetched.

Interview framing

"Next.js layers .env files: .env (shared) → .env.development / .env.production (per NODE_ENV) → .env.local (always, overrides, gitignored). NEXT_PUBLIC_* vars get inlined into the client bundle at build time — anyone can see them. Everything else is server-only. So secrets live in .env.local for dev and in the deployment dashboard for prod. The big mistake is shipping a secret with NEXT_PUBLIC_ prefix and never noticing. Public vars require rebuild to change — for true runtime config, fetch from a config endpoint."

Follow-up questions

  • Why isn't .env.local used in test mode?
  • How would you do runtime config?
  • What does NEXT_PUBLIC_ actually do?

Common mistakes

  • Secrets in NEXT_PUBLIC_*.
  • .env.local committed.
  • Expecting hot-reload of env without restart.

Performance considerations

  • Public vars inlined → no runtime cost. Server-only stay in the Node process.

Edge cases

  • Edge runtime env access.
  • Build-time vs runtime mismatch on Vercel preview deployments.
  • Vars referenced in static export pages.

Real-world examples

  • Vercel deployment dashboards, env var migrations, Next.js docs.

Senior engineer discussion

Seniors audit the client bundle for leaks, separate build-time from runtime config, and have a CI check that NEXT_PUBLIC_ vars don't contain credentials.

Related questions