Why Do We Need This?
Most SPA (Single Page Application) frameworks like React, Vite, Vue, or Angular read environment variables during build time from .env .env.production .env.development .env.staging .env.local etc files.
When the frontend is built, these values become part of the generated static assets.
Because of this, many teams rebuild the frontend separately for each environment:
- Development
- UAT
- Staging
- Production
This introduces unnecessary operational overhead:
- multiple Docker images
- repeated frontend rebuilds
- harder version management
In many cases, the only thing changing between environments is the configuration itself.
Another Common Approach
Some teams build the frontend inside the Docker container and serve it using Nginx or another web server. This solves the hardcoded build-time environment issue, but introduces new operational overhead.
Problems
- slower container startup
- larger Docker images
- longer deployment time
- source code remains inside the image during build
- even small environment changes require rebuilding the frontend
- restarting a pod or Docker container may unnecessarily trigger another build process
In modern deployment systems, rebuilding the same frontend multiple times just to change API URLs or runtime configs is unnecessary overhead.
A better approach is:
- build once
- inject configuration during runtime
This makes deployments faster, cleaner, and easier to maintain.
Why Frontend Env Works Differently Than Backend
Backend applications like Node.js, Golang, or Python run directly on the server, so they can read environment variables dynamically during runtime using:
process.env
But frontend frameworks like React, Vite, Vue, or Angular work differently.
They are built into static files like:
index.html
main.js
style.css
During the build process, environment variables get embedded into these files.
After the build:
-
.envfiles are no longer used -
process.envdoes not exist in the browser - changing API URLs or configs usually requires rebuilding the frontend
That’s why runtime configuration injection is very useful for modern frontend deployments.
The Solution: Runtime Configuration Injection
Instead of injecting environment variables during build time, inject them during runtime.
This allows the frontend build to remain completely environment-independent while configuration is loaded dynamically when the container starts.
This approach gives us:
- a single reusable frontend build
- faster deployments
- cleaner CI/CD pipelines
- easier environment management
- flexible infrastructure configuration
It also reduces accidental exposure because configuration values are no longer permanently embedded inside compiled assets.
How It Works
Load a runtime configuration file before the frontend application starts.
Example:
<script src="/client.secret.js"></script>
SPA entry HTML:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- Runtime configuration -->
<script src="/client.secret.js"></script>
</head>
<body>
<div id="root"></div>
<!-- Frontend application -->
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
The frontend application can then access runtime values from:
window.__ENV__
Runtime Config Example
Example: client.secret.js
window.__ENV__ = {
API_BASE_URL: "https://api.example.com",
APP_ENV: "production",
FEATURE_FLAG: true
}
Usage:
window.__ENV__
Some teams store config in different window locations for light obfuscation:
window.__proto__.config = {
API_BASE_URL: "https://api.example.com"
}
Others use encrypted runtime strings:
window.__ENV__ = "encrypted-json-string"
and decrypt them in the frontend.
However, this is not real security because the decryption logic and keys still exist in the browser.
These techniques only help with:
- obfuscation
- casual protection
- reducing accidental exposure
Runtime config is best suited for:
- API URLs
- feature flags
- environment names
- public runtime configuration
Docker Runtime Injection
The runtime configuration file can be mounted dynamically during deployment.
Example:
services:
frontend:
image: registry.github.com/my-org/some-super-frontend:1.0.0
volumes:
- ./client.secret.js:/app/dist/client.secret.js
ports:
- "5173:5173"
Benefits:
- the same Docker image works across all environments
- only runtime configuration changes
- rebuilding becomes unnecessary
This works especially well with:
- Docker
- Kubernetes
- Nginx
- cloud-native infrastructure
Best Use Cases
This architecture works particularly well with:
- React
- Vite
- Vue
- Angular
- static frontend deployments
and platforms such as:
- Docker
- Kubernetes
- Nginx
- CDN-based hosting
Final Thoughts
Modern frontend infrastructure is gradually shifting toward runtime-configurable frontend applications instead of environment-specific frontend builds.
Rather than rebuilding the same frontend multiple times for Development, UAT, Staging, and Production, teams can build once and inject configuration dynamically during deployment.
This approach provides several operational advantages:
- faster deployments
- smaller and cleaner CI/CD pipelines
- reduced rebuild overhead
- simpler version management
- improved scalability
- more flexible infrastructure configuration
- cleaner Docker and Kubernetes workflows
while keeping deployment pipelines flexible and production friendly.
Top comments (0)