---
title: "Environment variables and deployment: dev, preview, production · vuejs.school"
description: "How Vite and Nuxt read environment variables, which ones end up in the browser, and how to ship a static or server build with a build step in CI."
canonical_url: "https://vuejs.school/community/blog/environment-variables-and-deployment"
meta:
  "og:description": "How Vite and Nuxt read environment variables, which ones end up in the browser, and how to ship a static or server build with a build step in CI."
  "og:image": "https://vuejs.school/_og/d/a_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24%2Cdescription_~SG93IFZpdGUgYW5kIE51eHQgcmVhZCBlbnZpcm9ubWVudCB2YXJpYWJsZXMsIHdoaWNoIG9uZXMgZW5kIHVwIGluIHRoZSBicm93c2VyLCBhbmQgaG93IHRvIHNoaXAgYSBzdGF0aWMgb3Igc2VydmVyIGJ1aWxkIHdpdGggYSBidWlsZCBzdGVwIGluIENJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnZpcm9ubWVudC12YXJpYWJsZXMtYW5kLWRlcGxveW1lbnQi%2Cs_gEVPmEja-Gp_Qte3.png"
  "og:image:alt": "Environment variables and deployment: dev, preview, production · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Environment variables and deployment: dev, preview, production · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24%2Cdescription_~SG93IFZpdGUgYW5kIE51eHQgcmVhZCBlbnZpcm9ubWVudCB2YXJpYWJsZXMsIHdoaWNoIG9uZXMgZW5kIHVwIGluIHRoZSBicm93c2VyLCBhbmQgaG93IHRvIHNoaXAgYSBzdGF0aWMgb3Igc2VydmVyIGJ1aWxkIHdpdGggYSBidWlsZCBzdGVwIGluIENJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnZpcm9ubWVudC12YXJpYWJsZXMtYW5kLWRlcGxveW1lbnQi%2Cs_gEVPmEja-Gp_Qte3.png"
  "twitter:image:alt": "Environment variables and deployment: dev, preview, production · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Vite env files and the VITE\_ prefix

A Vue project built with Vite reads environment variables from files in the project root. Which files load depends on the mode, a name for the environment: `development` when you run `vite`, `production` when you run `vite build`.

- `.env` is loaded in every mode.
- `.env.local` is loaded in every mode and is meant to stay out of git.
- `.env.[mode]` and `.env.[mode].local` are loaded only in that mode.

A more specific file wins over a general one, and a variable already set in the shell wins over all of them. Vite reads the files when it starts, and the dev server restarts itself when you save one of them.

Only variables whose name starts with `VITE_` reach your code. Vite replaces `import.meta.env.VITE_API_URL` with the string from the file at build time, and leaves every other variable out of the bundle. That prefix is a declaration: "this value is allowed in the browser".

Here `.env` holds two public values, and `src/api/client.ts` reads the API address:

*.env*

```
VITE_API_URL=http://localhost:8787
VITE_STRIPE_PUBLISHABLE_KEY=pk_test_123
```

*src/api/client.ts*

```
export const apiUrl = import.meta.env.VITE_API_URL

export async function getProducts() {
  const response = await fetch(`${apiUrl}/products`)
  return response.json()
}
```

Vite also provides `import.meta.env.MODE`, `BASE_URL`, `DEV`, `PROD` and `SSR` without any file; the last three are booleans. Values from your env files are always strings, so a flag written as `VITE_FEATURE_X=false` is the truthy string `'false'` until you compare it: `import.meta.env.VITE_FEATURE_X === 'true'`.

## Keep secret keys out of VITE\_ variables

Here is the mistake the prefix exists to prevent:

*.env**Breaks***

```
VITE_STRIPE_SECRET_KEY=sk_live_abc
```

Read it anywhere in your code and it works in development, it builds, and the secret key is now in `dist/assets/index-*.js`, readable by anyone who opens the network tab. Vite's documentation is plain about it: `VITE_*` variables should not hold sensitive data.

The fix is a question, not a rename: who needs the value? The browser needs the publishable key; the secret key belongs to a server. If the project has no server, a serverless function on the host is one.

*.env**Works***

```
VITE_STRIPE_PUBLISHABLE_KEY=pk_live_123
```

Keep `*.local` in `.gitignore` (create-vue adds it), and commit `.env` only when it contains nothing you would mind seeing on a public page.

## Type import.meta.env and catch typos

Without a declaration, every `import.meta.env.VITE_*` property is typed `any`, and a typo in a variable name becomes `undefined` at runtime. Declare the ones you use in the `env.d.ts` file that `create-vue` puts in the project root (or any `.d.ts` file your `tsconfig` includes):

*env.d.ts*

```
/// <reference types="vite/client" />

interface ViteTypeOptions {
  strictImportMetaEnv: unknown
}

interface ImportMetaEnv {
  readonly VITE_API_URL: string
  readonly VITE_STRIPE_PUBLISHABLE_KEY: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}
```

The `ViteTypeOptions` lines are what catch typos. Without them, Vite's own types accept any other name as `any`, so `import.meta.env.VITE_API_URl` still compiles. With them it is a type error, and a create-vue project's `npm run build`, which runs `vue-tsc`, fails on it.

Keep this file free of `import` statements; an import turns it into a module and the augmentation stops applying.

## Modes: development, staging, production

A mode can have any name, so a third environment is one flag away: `vite build --mode staging` reads `.env.staging` and still produces an optimised build.

That works because the mode and `NODE_ENV` are separate settings. The mode picks the env files; `NODE_ENV` picks a development or a production bundle. `vite build` makes a production bundle in every mode, `--mode development` included, unless you set `NODE_ENV=development` yourself, in the command or in an env file.

After a build, `vite preview` serves `dist` on port 4173 so you can click through the real bundle. It is a local check, not a server to deploy (the documentation says so), and not the preview deployments of the last section.

## Nuxt: runtimeConfig instead of VITE\_

Nuxt has a server, which changes two things: a value can stay on the server, and configuration is read when the server runs instead of being baked into the bundle. Both work through `runtimeConfig` in `nuxt.config.ts`:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  runtimeConfig: {
    stripeSecretKey: '',
    public: {
      apiBase: '/api',
    },
  },
})
```

Keys outside `public` exist only on the server; keys inside `public` are sent to the browser with the page. The values here are defaults, replaced by environment variables named after the keys:

- `NUXT_STRIPE_SECRET_KEY` overrides `stripeSecretKey`.
- `NUXT_PUBLIC_API_BASE` overrides `public.apiBase`.

Only keys declared in the config can be overridden, and an override is read when the server starts, so one build serves every environment. Values that look like numbers or booleans are parsed: `123456` arrives as a number unless the quotes are part of the value (`NUXT_PIN='"123456"'` in `.env`).

Read the config with `useRuntimeConfig()`. In a server route, pass the event. This route creates a Stripe Checkout session for a price the browser sends:

*server/api/checkout.post.ts*

```
export default defineEventHandler(async (event) => {
  const { stripeSecretKey } = useRuntimeConfig(event)
  const { priceId } = await readBody<{ priceId: string }>(event)

  return $fetch('https://api.stripe.com/v1/checkout/sessions', {
    method: 'POST',
    headers: { Authorization: `Bearer ${stripeSecretKey}` },
    body: new URLSearchParams({
      mode: 'payment',
      'line_items[0][price]': priceId,
      'line_items[0][quantity]': '1',
      success_url: 'https://example.com/thanks',
    }),
  })
})
```

The body is a `URLSearchParams` because Stripe's API takes form-encoded bodies; `$fetch` sends a plain object as JSON.

In a component, `useRuntimeConfig().public.apiBase` works on the server and in the browser. The private keys are there only during server rendering. Never render one into the template or put it in `useState`: both end up in the HTML.

The Nuxt CLI reads your `.env` file in development and for `nuxt build` and `nuxt generate`, so `NUXT_STRIPE_SECRET_KEY=sk_test_123` in `.env` works on your machine. The built server does not read `.env`: on a host, set the variables in its environment settings.

## Deploying a static build to any host

A Vite app is static files: `vite build` writes them to `dist/`, which any host that serves files can take. One thing must be configured on that host: with Vue Router's HTML5 history mode, a direct visit to `/products/42` asks the server for a file that does not exist. The server must answer every unknown path with `index.html` so the router can take over. On nginx that is one line, and most static hosts have an equivalent rewrite rule:

```
location / {
  try_files $uri $uri/ /index.html;
}
```

Once the server never returns a 404, your app has to: add a catch-all route, `{ path: '/:pathMatch(.*)', component: NotFound }`, so a wrong URL shows a not-found page instead of an empty one.

A Nuxt site can be static too. `nuxt generate` starts at `/`, follows the links on each page and renders every page it reaches to HTML at build time. It writes the result to `.output/public` together with `200.html` and `404.html` fallback pages for routes it did not render. Pages that need server code at request time cannot be generated. Everything else (a blog, a docs site, a marketing site) works this way and needs only file hosting.

A static Nuxt site has no server to read runtime config later: its `NUXT_PUBLIC_*` values are fixed when `nuxt generate` runs, so set them in the build environment and build again after changing one.

## Nuxt on a Node or serverless host

When pages need a server, `nuxt build` produces `.output/`, a self-contained server plus the client assets. Run it with Node. Set `NODE_ENV=production` so Vue Router stops warning about unmatched routes, and `PORT` or `HOST` to change the defaults of 3000 and `0.0.0.0`:

```
npx nuxt build
NODE_ENV=production node .output/server/index.mjs
```

The server inside `.output` is built by Nitro, and Nitro has a preset for each kind of host: a plain Node server, a cluster of Node processes, and the serverless and edge platforms. Several hosts, the ones Nitro lists as zero-config providers, are detected from their environment variables at build time, so a build there already targets the platform. Anywhere else the default is the plain Node server.

To choose explicitly, set `nitro.preset` in `nuxt.config.ts` or `NITRO_PRESET=node-server` in the build environment. The `NUXT_` overrides from the runtimeConfig section go in the host's environment settings, one set per environment.

## Previews per branch and a build step in CI

A preview deployment is a build of a branch at its own URL, so a reviewer can click through a pull request before it merges. Many hosts offer it through their git integration, and the part you own is the environment: give previews their own `NUXT_PUBLIC_API_BASE` or `VITE_API_URL` pointing at a staging API, and never the production secret key.

Whatever the host, run your tests and the build in CI on every push and pull request. A minimal workflow for GitHub Actions, which any CI system can mirror:

*.github/workflows/ci.yml*

```
name: CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test
      - run: npm run build
        env:
          VITE_API_URL: ${{ vars.VITE_API_URL }}
```

- `node-version: 24`: Node 24 is the current long-term support release.
- `npm ci`: installs exactly what the lockfile says.
- `npm test`: runs the `test` script in `package.json`; in a create-vue project with Vitest, write `npm run test:unit`.

Public values come from the CI's variables (`vars`), secrets from its secrets store. A `VITE_` value may be a variable because it is public anyway. A `NUXT_` server key belongs in the secrets store, and for a Nuxt server build it is only needed where the server runs, not where it is built.

**Check it stuck**3 questions from the Environment variables, builds and deployment quiz.

[**Practice now**](https://vuejs.school/learn/tooling/environment-variables-and-deployment?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 8 min read**Editor and Vue DevTools setup in 2026**](https://vuejs.school/community/blog/editor-and-vue-devtools-setup-in-2026) [Release notes · 6 min read**Rolldown 1.0: the bundler under Vite 8 is stable**](https://vuejs.school/community/blog/rolldown-1-0-the-bundler-under-vite-8-is-stable)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/environment-variables-and-deployment#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-11-11","description":"How Vite and Nuxt read environment variables, which ones end up in the browser, and how to ship a static or server build with a build step in CI.","name":"Environment variables and deployment: dev, preview, production","url":"https://vuejs.school/community/blog/environment-variables-and-deployment","breadcrumb":{"@id":"https://vuejs.school/community/blog/environment-variables-and-deployment#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/environment-variables-and-deployment"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/environment-variables-and-deployment#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2025-11-11","description":"How Vite and Nuxt read environment variables, which ones end up in the browser, and how to ship a static or server build with a build step in CI.","headline":"Environment variables and deployment: dev, preview, production","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24%2Cdescription_~SG93IFZpdGUgYW5kIE51eHQgcmVhZCBlbnZpcm9ubWVudCB2YXJpYWJsZXMsIHdoaWNoIG9uZXMgZW5kIHVwIGluIHRoZSBicm93c2VyLCBhbmQgaG93IHRvIHNoaXAgYSBzdGF0aWMgb3Igc2VydmVyIGJ1aWxkIHdpdGggYSBidWlsZCBzdGVwIGluIENJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnZpcm9ubWVudC12YXJpYWJsZXMtYW5kLWRlcGxveW1lbnQi%2Cs_gEVPmEja-Gp_Qte3.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/environment-variables-and-deployment#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/environment-variables-and-deployment#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/environment-variables-and-deployment#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Environment variables and deployment: dev, preview, production","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24%2Cdescription_~SG93IFZpdGUgYW5kIE51eHQgcmVhZCBlbnZpcm9ubWVudCB2YXJpYWJsZXMsIHdoaWNoIG9uZXMgZW5kIHVwIGluIHRoZSBicm93c2VyLCBhbmQgaG93IHRvIHNoaXAgYSBzdGF0aWMgb3Igc2VydmVyIGJ1aWxkIHdpdGggYSBidWlsZCBzdGVwIGluIENJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnZpcm9ubWVudC12YXJpYWJsZXMtYW5kLWRlcGxveW1lbnQi%2Cs_gEVPmEja-Gp_Qte3.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24gwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~RW52aXJvbm1lbnQgdmFyaWFibGVzIGFuZCBkZXBsb3ltZW50OiBkZXYsIHByZXZpZXcsIHByb2R1Y3Rpb24%2Cdescription_~SG93IFZpdGUgYW5kIE51eHQgcmVhZCBlbnZpcm9ubWVudCB2YXJpYWJsZXMsIHdoaWNoIG9uZXMgZW5kIHVwIGluIHRoZSBicm93c2VyLCBhbmQgaG93IHRvIHNoaXAgYSBzdGF0aWMgb3Igc2VydmVyIGJ1aWxkIHdpdGggYSBidWlsZCBzdGVwIGluIENJLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNyBtaW4gcmVhZA%2Ctint_sand%2Cp_Ii9jb21tdW5pdHkvYmxvZy9lbnZpcm9ubWVudC12YXJpYWJsZXMtYW5kLWRlcGxveW1lbnQi%2Cs_gEVPmEja-Gp_Qte3.png"}]}
```