Environment Variables
Next.js loads variables from .env* files into process.env. A variable is
available only on the server unless its name starts with NEXT_PUBLIC_:
then next build inlines its value into the JS bundle as a literal, so the
browser gets it, and it is frozen at build time. Server variables are read
at runtime, so the same build can run with different values. Nothing in
NEXT_PUBLIC_ is secret.
Applies to: Next.js 16.4.
Details
Two Kinds of Variables
NEXT_PUBLIC_API_URL | DATABASE_URL | |
|---|---|---|
| Where available | Server and client code | Server only: Server Components, Route Handlers, Server Actions, Proxy |
| When the value is set | At next build: replaced with a literal | At runtime, when the server code reads it |
| Change it | Rebuild | Restart with a new value |
| Secret | Never: it is in the bundle | Can be: it never reaches the browser |
- In client code a server variable is
undefined, not an error. - Only a static reference is inlined:
process.env.NEXT_PUBLIC_API_URL.process.env[name]and destructuring (const { NEXT_PUBLIC_X } = process.env) stay unreplaced and areundefinedin the browser. - A server variable read in a page that is prerendered at build time is also
frozen into the HTML. To read the runtime value, the code must run on request
(after
await connection(),cookies()and similar). - The
envkey innext.configis legacy: its values are always inlined into the bundle.
.env Files
Lookup order; the first file that defines a variable wins:
process.env: variables already set in the shell, CI or hosting..env.$(NODE_ENV).local:.env.development.local,.env.production.local..env.local: not loaded whenNODE_ENV=test, so tests are reproducible..env.$(NODE_ENV)..env.
- If
NODE_ENVis not set, it isdevelopmentfornext devandproductionfor other commands. .env*.localfiles hold local overrides and secrets and are not committed;.env,.env.developmentand.env.productionhold defaults and can be committed if they contain no secrets.- Files are read from the project root, not from
src/. $OTHER_VARinside a value is expanded;\$is a literal dollar.- Outside Next.js (a test runner, an ORM script) the same files are loaded with
loadEnvConfig()from@next/env.
One Build, Several Environments
The usual Docker setup builds one image and promotes it from staging to
production. NEXT_PUBLIC_ values are already in that image, so staging and
production would get the same API URL. Options:
- Keep the value on the server: read it in a Server Component and pass it to the client as a prop, or expose it through a Route Handler.
- Build a separate image per environment.
- Generate a config file at container start and load it in the browser
(
window.__ENV), at the cost of an extra request or script.
Example
.env
NEXT_PUBLIC_API_URL=https://api.acme.com
DATABASE_URL=postgres://localhost/acme
app/page.tsx
import { connection } from 'next/server';
import { Checkout } from './checkout';
export default async function Page() {
await connection(); // render on request: read the runtime value
const region = process.env.PAYMENT_REGION; // server only, set per environment
return <Checkout region={region} />;
}
app/checkout.tsx
'use client';
export function Checkout({ region }: { region?: string }) {
// Inlined at build time: the same in every environment that uses this build
const apiUrl = process.env.NEXT_PUBLIC_API_URL;
const dbUrl = process.env.DATABASE_URL; // set during SSR, undefined in the browser: leaks into the HTML, then a hydration mismatch
return <p>{`${apiUrl} · ${region} · ${dbUrl}`}</p>;
}
Follow-up Questions
- Why is a
NEXT_PUBLIC_variable changed on the server not visible in the browser? It was replaced with a literal at build time; the bundle does not readprocess.envat runtime. - How do you keep a server variable from leaking? Read it only in modules marked
import 'server-only'; passing it as a prop to a Client Component sends it to the browser. - How is this different from React Native? The idea is the same (
EXPO_PUBLIC_is inlined), but a mobile app has no server part: every value in it is public, see Environment Configuration. - Why is
.env.localignored in tests? Tests should give the same results on every machine, so personal overrides are not loaded; use.env.test. - How do you fail fast on a missing variable? Validate
process.envwith a schema (Zod,@t3-oss/env-nextjs) at build or startup, not at the first request that needs it. - Can there be a
.env.staging? Not throughNODE_ENV: it allows onlydevelopment,productionandtest. Use your own variable (APP_ENV) or set the values in the staging environment.
Common Mistakes
- Putting a secret key in a
NEXT_PUBLIC_variable "because the client needs it": call your server instead, which holds the secret. - Changing a
NEXT_PUBLIC_value in the hosting settings and expecting it to apply without a rebuild. - Reading variables dynamically with
process.env[key]in client code. - Committing
.env.localwith secrets.