Back to Blog
Engineering2026-06-026 min

Why your NEXT_PUBLIC_ env var is undefined in production — the Docker build-time trap

J

John C. Thomas

Founder, BlueWave Projects

You added an analytics ID to your production environment, rebuilt the container, deployed, and opened the site. The analytics script is not loading. You check the env file on the server — the value is right there. You check the browser — the variable is undefined. You rebuild again. Still undefined.

This one cost us an afternoon, so here is the explanation and the fix.

The two kinds of environment variable

Next.js has two completely different lifecycles for environment variables, and the trap is not knowing which one you are dealing with.

Server-only variables — database URLs, API secrets, anything without the NEXT_PUBLIC_ prefix — are read at runtime. The running server process looks them up when a request comes in. Change the value, restart the process, done.

Public variables — anything prefixed NEXT_PUBLIC_ — are different. Next.js inlines them into the JavaScript bundle at build time. During the build, the compiler finds every reference to a NEXT_PUBLIC_ variable and replaces it with the string value as it existed at build time. By the time the code reaches the browser there is no variable lookup left — just a baked-in literal. If the value was not present when the build ran, the literal that got baked in is undefined, and it stays undefined until the next build.

Why Docker makes this bite

Here is the exact sequence that burned us. Our production app runs in Docker, and docker-compose injects environment through env_file. That works perfectly for server-only variables, because env_file is a runtime mechanism — the values are present in the container's environment when the server process starts.

But env_file does nothing at build time. When the image was built, the Next.js build ran inside the Docker builder stage, where the env_file values were not present. So every NEXT_PUBLIC_ reference got inlined as undefined. Adding the value to the env file and restarting the container could never fix it, because the variable had already been frozen into the bundle during the build — and the build had not seen it.

We added the ID, rebuilt, and it baked in undefined again. The rebuild reran the build inside the same builder stage, which still could not see a runtime-only env_file. The fix had to reach the build stage, not the run stage.

The fix: pass public vars as build args

A public variable has to be present during the build. In Docker that means a build argument, not a runtime env file. Three small changes:

  • In the Dockerfile builder stage, before the build step, declare a build ARG for the variable and promote it to an ENV so the build can read it.
  • In docker-compose, under the service build section, pass the value through as a build arg, substituting it from the host environment.
  • Provide that value to compose's substitution. Compose reads substitution values from a file named .env in the same directory — note this is compose's own .env, not your app's .env.production. Put the variable there. If your deploy excludes .env from its file sync, the value survives across deploys, which is what you want.
  • Now the build runs with the variable present, inlines the real value, and the browser gets the correct string.

    How to confirm it actually took

    Because the value is baked into the bundle, you can verify the fix without opening browser devtools: fetch the deployed page and search the served HTML or the referenced script chunk for the value. If your ID appears in the bundle, the build saw it. If you find undefined where the ID should be, the build still did not get it. This is the same "grep the bundle, not the health check" habit that catches a lot of deploy lies.

    The one-sentence version

    Server variables are read at runtime; NEXT_PUBLIC_ variables are frozen into the bundle at build time — so in Docker they must arrive as build args, and adding them to a runtime env file and rebuilding will silently bake in undefined.

    We hit this wiring an analytics property into a production marketing build. It is a five-minute fix once you understand the lifecycle, and an afternoon of rebuilding in circles if you do not.

    If your team is shipping Next.js on Docker and these are the edges you would rather someone had already hit for you, [we are around](https://bluewaveprojects.com/booking).

    More from BlueWave