Application Development

Vite Env Variables Undefined in Production Build

J
James Eriksson
··12 min read
Vite environment variables undefined in production? They're inlined at build time, not runtime. Learn the five common mistakes, how to fix them, and deploy reliably.
TL;DR
  • Vite inlines environment variables at build time, not runtime: they must be present during vite build or they become undefined in your bundle.
  • Only VITE_ prefixed variables are exposed to client code; never put secrets in VITE_ variables because they become visible in your JavaScript.
  • Environment variables set in your hosting platform's dashboard don't update your app unless you rebuild: most platforms do this automatically on git push.
  • Verify variables early with runtime checks like if (!import.meta.env.VITE_API_URL) throw new Error(...) to catch mistakes before deployment.
  • Ship handles all of this automatically by rebuilding whenever you push code, eliminating the entire class of "I set the var but forgot to rebuild" mistakes.

Vite environment variables become undefined in production because they're inlined at build time, not injected at runtime. If a variable doesn't exist when you run vite build, it won't be available in your bundled code, no matter how many times you set it in your hosting platform's dashboard.

Why Vite Env Variables Become Undefined in Production

Vite takes a different approach to environment variables than traditional Node.js servers. During your build step (vite build), Vite scans your code for references to import.meta.env.VITE_* variables and replaces them with their actual values directly in the generated JavaScript files. This is called static replacement, and it happens at compile time, not runtime.

This design choice exists for good reasons. Browser code cannot access the environment at runtime--there's no process.env in the browser. So Vite bakes the variables into your bundle. If the variable doesn't exist during the build process, it gets bundled as undefined.

Here's the trap: you can set environment variables in your hosting platform's dashboard, but if you don't rebuild your application after setting them, the old bundle stays live. Vercel and Netlify rebuild on every git push, so changes usually propagate correctly. But if you're using a manual build process, a CI/CD step, or a DIY hosting setup like Coolify, you have to explicitly trigger the rebuild. This is why AI tools and tutorials often get this wrong--they assume automatic rebuilds, which most platforms provide, but not all.

The Five Common Mistakes Developers Make

The most frequent errors fall into a clear pattern. Each one prevents Vite from seeing the variable during the build:

Mistake 1: Missing VITE_ prefix. Only variables with the VITE_ prefix are exposed to your client code. If you name a variable API_KEY instead of VITE_API_KEY, Vite won't include it in the bundle. The fix: rename it to VITE_API_KEY and rebuild.

Mistake 2: Variable not set during the build. The.env file must exist on the build machine at the moment you run vite build. If you set the variable in your hosting platform's dashboard after the deploy happens, or if your CI doesn't pass the variable to the build process, Vite never sees it. The fix: set the variable before triggering the build, or ensure your CI exports it as an environment variable before running npm run build.

Mistake 3:.env file in the wrong location or named incorrectly. Vite looks for.env files in your project root, not in src/ or public/. If you place it anywhere else, Vite won't find it. Also, Vite loads.env files in a specific hierarchy:.env (all modes), then.env.production (production mode only), etc. The fix: place all.env files in your project root, and ensure they're in your.gitignore if they contain secrets.

Mistake 4: Built with an old bundle. You set a variable in your hosting platform, but forget to rebuild. The deployed code still has the old value (or undefined). The fix: always rebuild and redeploy after changing environment variables. Most platforms do this automatically on git push; some require a manual trigger.

Mistake 5: Treating it as runtime config. Vite bakes variables into the bundle at build time. You can't inject new values after deployment. If you need truly dynamic configuration at runtime--say, different API URLs depending on the user's region--you can't do it with VITE_ variables. The fix: call an API endpoint instead. Your backend can return dynamic values; the client code fetches them at runtime.

The most common trap by far: AI tools (ChatGPT, GitHub Copilot, Claude) often suggest using process.env.API_KEY in Vite client code, which doesn't work. In browsers, process.env doesn't exist. Vite's static replacement only works with import.meta.env.VITE_*. If you copy-paste an example without the VITE_ prefix, your code will fail silently in production.

Set Up.env Files for Development and Production

Vite loads.env files in a specific order, with more specific files overriding less specific ones:

  1. .env -- Loaded in all modes.
  2. .env.local -- Loaded in all modes, ignored by git (for local secrets).
  3. .env.[mode] -- Loaded only in that specific mode (e.g., .env.production only when running vite build without --mode flag).
  4. .env.[mode].local -- Loaded in the specific mode, ignored by git.

By default, vite build runs in "production" mode. So Vite will load .env and .env.production (and their.local variants if they exist).

Here's a typical setup:

.env
VITE_APP_NAME=MyApp
VITE_API_URL=<your-dev-api-server>

.env.production
VITE_API_URL=<your-production-api-url>
VITE_LOG_LEVEL=error

.env.staging
VITE_API_URL=<your-staging-api-url>
VITE_LOG_LEVEL=debug

In your app code:

const apiUrl = import.meta.env.VITE_API_URL;
const appName = import.meta.env.VITE_APP_NAME;

To build for a custom mode:

vite build --mode staging

This tells Vite to load .env and .env.staging (and their.local variants). Now VITE_LOG_LEVEL will be "debug" in the staging bundle, not "error".

Understanding the VITE_ Prefix and Client Bundling

Only variables with the VITE_ prefix are exposed to your client code. This is a deliberate security boundary. When you run vite build, Vite scans your JavaScript and finds every reference to import.meta.env.VITE_*. It then replaces each reference with the actual value. If the value is a string, it becomes a string literal in your bundle. If it's a number, a number literal. If it's missing, undefined.

Here's an example:

Your source code:

const config = {
  apiUrl: import.meta.env.VITE_API_URL,
  appName: import.meta.env.VITE_APP_NAME,
};

After build (with proper env variables set):

const config = {
  apiUrl: "your-configured-api-url",
  appName: "MyApp",
};

Anyone inspecting your bundle (via DevTools Network tab, or downloading your JavaScript) can see those values. This is why you must never put secrets in VITE_ variables: API keys, database passwords, private tokens--they are not secret once bundled.

For secrets and runtime values that should never be baked into your client, use a backend endpoint. Call your API from client code; the backend handles the secret logic. This is the only safe pattern for sensitive data. A common example: instead of VITE_DATABASE_PASSWORD, call an endpoint /api/query and let your backend authenticate and access the database.

Verify Variables at Build Time, Not Runtime

The worst way to debug this is to deploy to production and check your browser console. By then, the damage is done--either the old code is live (with wrong values) or the new code shipped with undefined variables. Instead, validate early in your build process.

Add runtime checks in your code. If a critical variable is missing, fail fast:

if (!import.meta.env.VITE_API_URL) {
  throw new Error('VITE_API_URL is not defined. Check your.env file and rebuild.');
}
const apiUrl = import.meta.env.VITE_API_URL;

If the variable is missing during the build, you'll see this error immediately in dev mode or when running vite build locally. Fix it before committing.

Print variables during CI builds. In your deploy pipeline (GitHub Actions, GitLab CI, CircleCI, etc.), add a step that prints the variable names (not values) before building:

echo "Build environment:"
echo "VITE_API_URL is set: ${VITE_API_URL:-not set}"
echo "Building..."
npm run build

This log becomes part of your build record. If the deploy succeeds but the code is broken, you can trace back and see whether the variable was actually present during the build.

Inspect the bundle in CI. After building, search the generated JavaScript for your hardcoded value:

grep "YourProductionApiValue" dist/index-*.js || echo "ERROR: API URL not found in bundle"

If the expected value isn't there, the build failed to inject it. The deploy fails before shipping broken code. This single check catches most environment variable mistakes.

Deployment Specifics: When and Where Variables Are Injected

Different hosting platforms handle environment variables differently. Understanding the exact flow for your platform prevents most surprises.

Vercel and Netlify. You set environment variables in the platform's web dashboard. When you push code to git, the platform detects the change, clones your repo, runs npm run build, and deploys the result. The environment variables are passed to the build process, so Vite can inline them. Changes appear live after the redeploy completes (usually within 2-5 minutes).

The catch: if you add an environment variable to the dashboard without changing your code, nothing happens. The platform doesn't rebuild automatically. You must trigger a rebuild by pushing a new commit or clicking "Redeploy latest" in the platform's UI.

Coolify (DIY on Hetzner or any server). Coolify is an open-source deployment platform you run on your own server. You set environment variables in Coolify's UI for your application. When you push code to your git repository (or trigger a manual redeploy), Coolify pulls the code, exports the environment variables to the container, runs npm run build inside the container, and starts your application.

The flow is the same as Vercel, but you own the infrastructure. Coolify handles the rebuild automatically on git push; you control the server where it runs. Coolify and Hetzner together cost roughly 30-40% of what Vercel charges for equivalent compute, but you're trading money for operational overhead.

DIY with Docker and build arguments. If you're running your own container without Coolify, you might pass environment variables to the build step via a Dockerfile:

FROM node:18-alpine
WORKDIR /app
COPY..
ARG VITE_API_URL
ARG VITE_APP_NAME
RUN npm ci && npm run build
EXPOSE 3000
CMD ["npm", "run", "preview"]

Then build:

docker build -t myapp \
  --build-arg VITE_API_URL=ProductionAPIUrl \
  --build-arg VITE_APP_NAME=MyApp \
.

The variables are passed as build arguments; Vite sees them during the RUN npm run build step and inlines them into the bundle.

Northflank (the incumbent). Northflank is similar to Vercel: set env vars in the platform, push code, Northflank rebuilds and deploys. The main differences are Northflank's pricing model and available regions.

process.env vs. import.meta.env Explained

If you've worked in Node.js (Express, Next.js server-side code, or build tools), you're used to process.env.VAR_NAME. In the browser, process does not exist. Vite cannot provide it directly.

Vite's answer is import.meta.env. At build time, Vite replaces import.meta.env.VITE_* with static values. This works in the browser because the values are already hardcoded; the browser never needs to look up a variable at runtime.

ContextUse ThisWhy
Vite client code (React, Vue components)import.meta.env.VITE_API_URLOnly this is available in the browser; Vite inlines it at build time.
Vite server config (vite.config.js)process.env.DATABASE_URLvite.config.js runs in Node.js, which has process.env.
Build step or CI scriptprocess.env.CI_COMMIT_SHACI tools set process.env; your build script reads it.
Vite tests (Vitest)import.meta.env.VITE_*Vitest runs in Node but uses Vite's env replacement.

In plain Vite (SPA), you'll always use import.meta.env.VITE_* in your component code. In a full-stack app (Vite + backend + tests), you'll use both patterns in different files.

Hosting Your Vite App with Predictable Costs

If you're tired of debugging environment variable mysteries and rebuild-after-config-change mistakes, you need a hosting platform that rebuilds automatically, shows clear logs, and charges a flat rate without surprises.

Northflank is the industry incumbent--it works well but pricing scales with dyno hours and can be unpredictable. Vercel and Netlify are popular for frontend-only apps, but both can get expensive if you underestimate traffic or build time.

Ship is Opsily's managed PaaS, built for this exact use case. You push code to git, Ship detects the push, pulls your environment variables from Ship's configuration UI, rebuilds your Vite app, and deploys it. No more "wait, which.env file did I use?" mysteries. Ship's pricing is flat-rate per month: you pay once and deploy unlimited times. No per-minute overages, no surprise bills. You know the cost upfront.

For raw price per month, Hetzner plus Coolify (DIY) is cheaper. But it costs time: you're running the infrastructure, backing up databases, managing SSL certificates, and debugging deployment issues on your own server. Most small teams find that time cost outweighs the ~30/month they'd save.

Check out Ship's managed PaaS hosting for details on what's included in the flat rate, or browse self-hosted PaaS alternatives if you're still evaluating the DIY option.

Frequently Asked Questions

Why does my Vite env variable work in development but not in production?

The development server (vite dev) reads your .env.local or .env file every request. Production is a static bundle created with vite build. If the variable wasn't present during the build, it's baked in as undefined. Rebuild after setting the variable.

Do I need to restart Vite after editing.env?

Yes. The dev server caches environment variables. If you edit.env or.env.local, save the file and the dev server will reload automatically (Vite watches.env files). If it doesn't, restart the server: npm run dev.

Can I put an API secret in a VITE_ variable?

No. VITE_ variables are bundled into your client-side JavaScript, which anyone can download and inspect. Never use VITE_ for secrets. Instead, call an API endpoint from your client code; the backend holds the secret and returns only what the client needs.

What if my variable has the VITE_ prefix but is still undefined?

Check these, in order: (1) Is the.env file in your project root, not in src/ or public/? (2) Did you restart the dev server or rebuild after editing the file? (3) Is the mode correct? If you're running vite build --mode staging, Vite looks for.env.staging, not.env.production. (4) Did you commit the.env file? If it's in.gitignore, your CI build won't see it; set the variable in your hosting platform's UI instead.

How do I use different environment variables for development, staging, and production?

Create separate.env files:.env.development,.env.staging,.env.production. Each contains its own VITE_ variables. Run vite dev (uses development mode by default), vite build --mode staging (uses staging), or vite build (uses production by default). Each build pulls variables from the matching.env file.

Can I access import.meta.env in a Node.js backend?

No, import.meta.env is Vite's client-side construct. In a Node.js backend (Express, Fastify, etc.), use process.env instead. If you're using SSR (server-side rendering) with Vite, the server code uses process.env, and the client code uses import.meta.env.

Should I commit.env files to git?

Commit.env and.env.staging (for non-secrets). Add.env.local,.env.production.local, and any file containing secrets to.gitignore. In your CI/deployment, pass secrets as environment variables (GitHub Actions secrets, Vercel/Netlify env vars, Coolify config, etc.). The build process picks them up.

The Bottom Line

Vite environment variables are undefined in production because they're inlined at build time, not injected at runtime. If the variable isn't present when you run vite build, it doesn't exist in your bundle. The fix is straightforward: use the VITE_ prefix, ensure your.env files are in the project root, rebuild after setting variables, and validate early with runtime checks in your code. Understanding this one fact--that environment variables are static, not dynamic--eliminates 90% of environment variable confusion in Vite apps.

The real payoff is in choosing the right hosting platform. Flat-rate platforms like Ship remove the rebuild-after-env-change mistake entirely because they rebuild automatically whenever you push code. If you're building a Vite app and tired of environment variable surprises, get started with Ship and see how much simpler deployment becomes.

Predictable hosting for Vite apps
Ship rebuilds automatically when you push code and charges a flat rate per month, no per-build overages.
Get Started Free

Ready to self-host your own apps?

One server. Multiple apps. No per-app fees.

Get started →