Vite: Build Once, Deploy Everywhere
The classic Vite problem: environment variables are baked into the bundle at build time. That means a separate build for dev, staging, and prod — or a single build with hardcoded values that you hope are right. Neither is great.
There’s a better pattern: build once, inject config at deploy time via a runtime file. One artifact, promoted straight through your environments.
How It Works
- Build once — no env-specific values baked in.
- A tiny
env.jsis loaded at runtime, before your app bundle. - Each deployment target has its own
env.js, injected by CI/CD or your container entrypoint.
Step 1: Create the Runtime Config File
public/env.js
Vite copies everything in public/ to dist/ unchanged and serves it from root during dev. This file ships with your build — overwrite it per environment at deploy time.
| |
Security note:
window.__ENV__is visible to anyone who opens DevTools. Never put secrets, API keys, or tokens here — only public config like base URLs and feature flags.
index.html — Load It Before Your App
| |
Step 2: Create a Typed Helper
src/env.ts
| |
The optional chaining on window.__ENV__?.[key] guards against env.js failing to load — the app degrades instead of crashing.
The fallback to import.meta.env means .env.local still works during vite dev. Developers can override individual values there without touching public/env.js.
Usage
| |
Step 3: Inject at Deploy Time
Pick whichever matches your setup.
Option A: Docker Entrypoint
| |
env.sh
| |
The 50- prefix controls execution order — nginx:alpine runs scripts in /docker-entrypoint.d/ alphabetically on startup. Pass config as normal container env vars:
| |
Option B: CI/CD Pipeline
After build, before deploy — just overwrite the file. GitHub Actions example:
| |
Use vars.* (repository/environment variables) not secrets.* — these values end up in a public JS file anyway, so they shouldn’t be secrets.
Option C: Kubernetes ConfigMap
| |
subPath mounts just that one key as a file rather than replacing the whole directory.
Local Dev Still Works
During vite dev, Vite serves public/env.js at /env.js and window.__ENV__ gets the dev defaults from the file. The env() helper’s fallback chain covers you either way:
.env.local → vite dev (import.meta.env.VITE_*)
public/env.js → built app at runtime (window.__ENV__)
src/env.ts → unified accessor, checks bothThe Payoff
One build artifact. Promote it through staging → prod by swapping a config file or passing env vars. No rebuilds, no configuration drift, no “works in staging but not prod because someone forgot to set VITE_X.”