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

URL: https://crunu.com/environment-variables/nextjs
Language: en
Updated: 2026-10-30

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](https://nextjs.org/docs/app/guides/environment-variables) 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:

| Order | Source                   | Loaded when                   |
| ----- | ------------------------ | ----------------------------- |
| 1     | `process.env`            | always                        |
| 2     | `.env.$(NODE_ENV).local` | always                        |
| 3     | `.env.local`             | not when `NODE_ENV` is `test` |
| 4     | `.env.$(NODE_ENV)`       | always                        |
| 5     | `.env`                   | always                        |

*Figure: Diagram of how Next.js loads environment variables. Env files are read in this order of priority, and the first one wins: .env.$(NODE_ENV).local, .env.local, .env.$(NODE_ENV), and .env. Some values are inlined at build time and the rest are read when the app starts. Variables that start with NEXT_PUBLIC\_ go into the browser bundle and are visible to every visitor; server and build code can read every variable.*

`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](https://crunu.com/docs/concepts/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](https://crunu.com/docs/cli/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](https://crunu.com/integrations/vercel).
- The agent service runs on Linux with systemd. On macOS, `crunu` works as a command-line tool for development. There is no Windows build.

## Related reading

- [How to share .env files with your team](https://crunu.com/guides/share-env-files-with-a-team) covers where the values should live in the first place.
- [Why NEXT_PUBLIC\_ values leak](https://crunu.com/blog/next-public-env-leaks) looks at real ways public variables end up holding secrets.
- [What a public env variable is](https://crunu.com/learn/public-env-variable), and the same rules in [Vite](https://crunu.com/environment-variables/vite), [SvelteKit](https://crunu.com/environment-variables/sveltekit) and [Nuxt](https://crunu.com/environment-variables/nuxt).
- Generate an `AUTH_SECRET` with the [NextAuth secret generator](https://crunu.com/tools/secret-generator/nextauth-secret).

## FAQ

### 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

- [Next.js: How to use environment variables](https://nextjs.org/docs/app/guides/environment-variables) (accessed 2026-10-30)
