Skip to content
FrameworkJavaScript

Next.js environment variables: NEXT_PUBLIC_ and .env

How Next.js picks values from process.env and four .env files, when NEXT_PUBLIC_ values are frozen into the bundle, and how to run Next.js with Crunu.

Next.jsAt a glance

Env file that wins
.env.$(NODE_ENV).local

First of 4 .env files, highest priority first. See the load order

Public prefix
NEXT_PUBLIC_

Only names with this prefix reach browser code, and anyone can read them there.

Values applied
Build and runtime

Some values are fixed at build time, the rest are read when the app runs.

Built-in secret store
None built in: process.env from the host

nextjs.org/docs/app/guides/environment-variables

Crunu command
crunu run --env web-app/development -- npm run devSee the Vercel integration

Common pitfalls

  1. NEXT_PUBLIC_ values are frozen into the JavaScript bundle at next build, so one Docker image keeps the same public values in every environment.
  2. Dynamic lookups such as process.env[name] are not inlined, so a NEXT_PUBLIC_ value read that way is undefined in the browser.
  3. When NODE_ENV is test, Next.js skips .env.local and reads .env.test and .env instead.
  4. With a src folder, .env files must stay in the project root. Next.js does not look for them inside src.
  5. A $ inside a .env value starts a variable reference. Write it as \$ when the dollar sign is part of the value.
On this page

Next.js reads environment variables from process.env first and then from up to four .env files in the project root, stopping at the first match. Server code can read any variable. Browser code sees only variables whose names start with NEXT_PUBLIC_, and those are copied into the JavaScript bundle when next build runs, so their values are fixed until the next build. Everything below is checked against the Next.js environment variables guide as of October 30, 2026.

The lookup order

Next.js looks a variable up in five places, top to bottom, and takes the first value it finds:

OrderSourceLoaded when
1process.envalways
2.env.$(NODE_ENV).localalways
3.env.localnot when NODE_ENV is test
4.env.$(NODE_ENV)always
5.envalways

NODE_ENV may be production, development or test. If you don't set it, next dev uses development and every other command uses production. So a value in .env.development.local beats the same name in .env while you develop, and .env.production applies to next build and next start.

The first row matters most when you inject values from outside. Anything already in the process environment wins over every file, which is why a variable exported in your shell, set by your host, or passed by a tool like crunu run replaces the value in .env.local without any configuration.

Build time versus request time

Next.js has two kinds of environment variables, and they behave very differently.

NEXT_PUBLIC_ variables are inlined. During next build, every reference written as process.env.NEXT_PUBLIC_ANALYTICS_ID is replaced by the literal value from the build environment. The value then sits in the JavaScript that every visitor downloads. Two consequences follow:

  • Anything with the prefix is public. A key that grants write access, a database URL or a signing secret must never get it.
  • The value is frozen. If you build one Docker image and promote it from staging to production, the browser code keeps the staging value. The Next.js guide says this directly: after the build, the app no longer responds to changes in these variables.

Inlining also only works for the literal form. process.env[varName] and const env = process.env; env.NEXT_PUBLIC_X are left alone, so in the browser they come out undefined.

Other variables stay on the server and can be read at request time. By default they exist only in the Node.js process. When a page renders dynamically, for example after await connection() or a call to cookies() or headers(), process.env.MY_VALUE is evaluated per request. That lets one image run in several environments with different server-side values. A page that Next.js renders statically during the build reads its variables during next build instead, so check which rendering mode a page uses before you rely on a runtime value.

For code that must run once when the server starts, the guide points to the register function in instrumentation.

Docker images and promotion between stages

The build-time rule shapes how you ship containers. Teams that build one image and promote it through staging to production have two options, and both are in the Next.js guide:

  1. Keep browser-facing settings out of NEXT_PUBLIC_. Read them on the server at request time and hand them to the client through your own API route or as props from a dynamically rendered layout. The image stays the same in every stage, and only the runtime environment differs.
  2. Build once per stage. Run next build with the staging values for the staging image and again with the production values for production. This is simpler, but staging and production then run slightly different artifacts.

Whichever you pick, audit the names. List every NEXT_PUBLIC_ variable in the project and ask of each one: would I paste this value into a public web page? Analytics IDs, a public site URL and keys designed to be public pass. Anything that lets the holder write, pay, send mail or read private data fails, and belongs in a server-only variable.

Smaller rules that cause confusion

  • Tests skip .env.local. With NODE_ENV=test, Next.js loads .env.test.local, .env.test and .env, and ignores .env.local so tests behave the same on every machine. .env.development and .env.production are skipped too.
  • The src folder doesn't count. .env files belong in the project root next to package.json. Files inside src are ignored.
  • Dollar signs expand. TWITTER_URL=https://x.com/$TWITTER_USER resolves to the value of TWITTER_USER. A literal $ needs a backslash.
  • Multi-line values work inside double quotes, which helps with PEM keys.
  • Outside Next.js, nothing loads automatically. An ORM config file or a test runner started on its own doesn't read .env.local. The @next/env package exposes loadEnvConfig(), the same loader Next.js uses, for those cases.

Run Next.js with values from Crunu

A Crunu environment holds the same variables as your .env.local, encrypted on your devices, with a separate environment and key for each stage. crunu run decrypts the chosen environment in memory and starts your command with those values in its process environment. Because Next.js checks process.env first, the values from Crunu take effect even if an old .env.local is still lying around, and nothing new is written to disk. Tick Load as environment variables (crunu run) on the env file in the environment, as described in files and variables.

  1. On your laptop, sign in once with crunu login --server https://app.crunu.com, then start the dev server with crunu run --env web-app/development -- npm run dev.
  2. In CI, store a service token as CRUNU_TOKEN on the build step only and run exec crunu run --env web-app/production -- npm run build, so the right NEXT_PUBLIC_ values are inlined.
  3. On a Linux server with the agent, start the app with crunu run --env web-app/production --restart on-change -- npm start, so server-side values change without a manual restart.

crunu run hands the program a short allowlist of your shell's variables plus the environment's variables. NODE_ENV isn't on that list, which is fine for next dev and next start because Next.js sets it itself. If a build script depends on another shell variable, add it to the environment or pass --inherit-env. See crunu run for the full list and for signal and exit code handling.

What Crunu changes and what it doesn't

Crunu changes where values live and who can read them. It doesn't change how Next.js treats them.

  • NEXT_PUBLIC_ values are still public after the build, wherever they came from. Keep secrets without the prefix.
  • --restart on-change restarts next start when a value changes, so server code picks up the new value. Browser bundles keep the old public values until you build again.
  • On Vercel, the platform keeps its own environment variables, and Crunu has no built-in sync into them. A pipeline can push values with vercel env add under crunu run, as shown on the Vercel integration page.
  • The agent service runs on Linux with systemd. On macOS, crunu works as a command-line tool for development. There is no Windows build.

Frequently asked questions

Something else? Write to support@crunu.com

Why is my NEXT_PUBLIC_ variable undefined in the browser?

Either it wasn't set when next build ran, or the code reads it with a dynamic lookup such as process.env[name], which Next.js doesn't inline. In development, restart next dev after adding the variable. In production, rebuild with the value present.

Can one Docker image serve staging and production with different values?

For server-side values, yes: read them during dynamic rendering and they come from the environment at request time. NEXT_PUBLIC_ values can't change after the build. Build one image per environment, or serve public settings from your own API endpoint.

If a variable is both in Crunu and in .env.local, which one wins?

The Crunu value. crunu run puts it into process.env, and Next.js checks process.env before any .env file.

Do I still need @next/env when I use crunu run?

Only for tools that run outside Next.js and want to read your .env files themselves, such as an ORM config or a test runner. Started under crunu run, those tools already find the values in process.env.

Sources

Every source this page relies on, with the date we last checked it.

  1. Next.js: How to use environment variables · checked October 30, 2026

Keep your .env values in one place

Free during early access. Values are encrypted on your devices, each environment has its own key, and crunu run hands them to your app without writing a file.