Build-time environment variables considered harmful
Build-time environment variables seem convenient, but they come with hidden costs that can undermine your deployment pipeline’s reliability and performance.
How deployment pipelines actually work
When you push code to Upsun, the build process compiles your binaries, runs npm install
if necessary, and generates a folder of built artifacts. We make a squashfs archive of that folder. Then that archive gets deployed in a fully read-only manner to a runtime environment behind a live URL.
The build container is isolated from the environments resources. No database access, no external APIs, no live traffic. This isolation prevents you from accidentally messing with your production or preview environments during the build phase.
The well-intentioned mistake
We implemented build-time environment variables to address a common need: frontend applications required different settings between staging and production. Different API URLs, different feature flags, that sort of thing.
We used to recommend runtime configuration fetching (generate a JSON file in a mount during the deploy hook, expose it at /config/conf.json
, let your frontend grab it during bootstrap). But JavaScript frameworks didn’t support this pattern well at the time. Build-time environment variables filled that gap.
Why this is a problem
Build-time environment variables create differences between environments, which undermines reliable deployments.
At Upsun, we reuse build outputs whenever possible. When you merge staging to production, you get the exact same code. Not “equivalent” code. The same bytes. This approach gives you three things:
- Confidence: What worked in staging will work in production
- Speed: No waiting for redundant rebuilds
- Security: You’re not re-downloading packages that might have changed (and the npm ecosystem has shown us repeatedly why this matters)
Build-time environment variables break this model. We use environment variables as part of the cache key that determines whether a rebuild is necessary. Push the same commit to two environments with different build-time variables, and you’ll trigger a rebuild every time.
Modern frameworks caught up
JavaScript frameworks now support runtime configuration properly. Nuxt supports server routes for runtime config. Next.js has similar patterns. Most modern frameworks do.
You can now configure your applications at runtime without baking values into your build artifacts. Your staging and production builds can be identical, with only the runtime configuration differing.
What to do instead
Use runtime configuration. Generate your config files during deployment, not during build. Fetch configuration from your server, don’t bake it in during the build process.
Your deployment pipeline will thank you. Your staging-to-production promotions will be faster and safer. And you won’t force rebuilds every time someone changes an environment variable.
Build-time environment variables made sense when frameworks didn’t support better alternatives. They don’t anymore; time to move on.