- Introduction
- 1. The Problem with Vite and Environment Variables
- 2. The Concept: Build Once, Inject Later
- š How It Works (Visual Recap)
- 3. Step-by-Step Setup
- 4. Advantages and Gotchas
- š Resources
- ā Final Thoughts
Introduction
Stop me if this sounds familiarā¦
Youāve built a slick Vite frontend, bundled it into a Docker container, and youāre ready to deploy it across environments. Staging. QA. Production. You spin up a few containers with Docker Compose, pass in some environment variables, and⦠nothing. Your frontend still shows the old values. Or worse, hardcoded ones from build time.
Why? Because Vite bakes your environment variables into the JavaScript at build time. That means by the time your app hits the container, those variables are already locked in. If you want to change them, youāve got to rebuild your app from scratchāfor each environment. Which defeats the entire point of using Docker in the first place.
Here's the fix:
In this guide, youāll learn how to build your Vite app once with environment placeholders, and inject the actual values at runtime using a simple script inside Docker. No more rebuilding for every deploy. Just plug in your config at startup, and let your container do the rest.
1. The Problem with Vite and Environment Variables
Vite makes development fastābut once you hit deployment, it locks you into a limitation that trips up a lot of devs:
Environment variables are statically replaced at build time, not dynamically read at runtime.
That means when you run npm run build, Vite crawls through your code and literally replaces any reference like import.meta.env.VITE_API_URL with the value in .env.production at that moment. It becomes a hardcoded string in your final JS bundle.
If you want different values for staging or production? Youāre stuck rebuilding your app for each environment. Thatās time-consuming, error-prone, and kills the point of Dockerās "build once, run anywhere" philosophy.
2. The Concept: Build Once, Inject Later
Hereās the idea: instead of hardcoding real values into your build, you bake in placeholdersāclearly identifiable markers like PREFIX_API_URL.
Then, when the container starts up, a small shell script scans the final JavaScript files and swaps those placeholders for actual values from your environmentāusing plain old sed. Just before Nginx serves the files.
Why this works:
- Vite doesnāt know your values at build timeāit just drops in what you give it.
- At runtime, Docker does know your values, and you can use them to patch the final files.
- You still get a fully static, cacheable frontend bundleābut with runtime flexibility.
And no, itās not a hackāitās a pragmatic workaround thatās safe, production-ready, and used in many real-world pipelines.
š How It Works (Visual Recap)
Hereās whatās happening behind the scenes:
- Build phase
- Vite replaces env references with placeholder strings like
"PREFIX_API_URL"in your compiled JS.
- Runtime phase
- Your Docker container starts with the real environment variables (e.g.
PREFIX_API_URL=https://api.myapp.com). - A shell script replaces every placeholder in the final JS/HTML files.
- Nginx serves the updated files.
3. Step-by-Step Setup
Letās walk through how to set this up from scratch.
Step 1: Define Placeholders in .env.production
Create a .env.production file in the root of your project. The .env.production file will be used by vite build for its environment variables. Inside your project, add this line to .env.production:
VITE_API_URL=PREFIX_API_URL
The value PREFIX_API_URL is a placeholder that Vite will statically bake into your code. It doesnāt need to be realājust something unique and easy to find later.
Step 2: Add the Runtime Script (env.sh)
This is the important part
Drop this env.sh script into your project root. Make sure itās executable (chmod +x env.sh) and converted to Unix line endings (dos2unix env.sh if needed). If you don't want to do this every time you can also add those commands to your Dockerfile.
Step 3: Set Up Docker
š³ Dockerfile ā Explained
Hereās a Dockerfile that builds your Vite app and prepares it for runtime replacement:
# Step 1: Build the app using Node
FROM node:23-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Step 2: Use a lightweight Nginx container to serve the files
FROM nginx:alpine
# Copy Nginx config if you have custom routing
COPY ./nginx.conf /etc/nginx/nginx.conf
# Copy the built app from the builder stage
COPY --from=builder /app/dist /var/www/html/
# Copy the runtime injection script into the container
COPY env.sh /docker-entrypoint.d/env.sh
RUN dos2unix /docker-entrypoint.d/env.sh
RUN chmod +x /docker-entrypoint.d/env.sh
# Let Docker run your script before starting Nginx
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
š§© docker-compose.yml ā Explained
This Compose config shows how to inject the real values during container startup:
services:
app:
image: app:latest
build:
context: app
ports:
- "8080:80"
environment:
# Where your built files live in the container
ASSET_DIR: /var/www/html
# Prefix used for placeholder detection
APP_PREFIX: PREFIX_
# Real values for your frontend to consume
PREFIX_API_URL: "https://api.myapp.com"
Key things happening here:
-
ASSET_DIRtellsenv.shwhere to search for files. -
APP_PREFIXensures we only replace the values we want. -
PREFIX_API_URLis the real value your app needsāmatched to the placeholder in.env.production.
4. Advantages and Gotchas
ā Pros
Single build for all environments
Build once. Deploy many times.Static file friendly
Keeps your assets cacheable and CDN-ready.Framework agnostic
Works with any static frontendānot just Vite.
ā ļø Gotchas
Pick a unique prefix
Avoid accidental replacements.PREFIX_is a good default.Beware of quotes and special characters
Escape them properly if needed in shell scripts.
š Resources
- š GitHub Repo: Dutchskull/Vite-Dynamic-Environment-Variables
- š§° Shell script:
env.sh
ā Final Thoughts
This setup gives you flexibility without sacrificing speed or simplicity. You keep your frontend static, lightweight, and easy to cacheāwhile still injecting environment-specific config at runtime.
Itās a clean bridge between how Vite works and how Docker deployments need to work.
You no longer have to rebuild for every little config change. Just build once, inject on start, and move on.
Top comments (2)
Thank you! This was very useful. I implemented your solution in several projects.
Is this not a security breach or data breach and top of that we are Prefixing the configuration values. We are modifying the built artifacts also.