Why your NEXT_PUBLIC_ env var is undefined in production — the Docker build-time trap
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:
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
RoomPlan vs Matterport vs Polycam: which one belongs in your contractor's toolkit?
8 min
Hawaii complianceHawaii GET tax for contractors: how the §237-13(3)(B) sub-deduction actually works
6 min
WorkflowHow to scope a renovation in 60 seconds (and why your hand-written estimate keeps losing jobs)
5 min