skip to content

In Nuxt 4, how does `NUXT_PUBLIC_SITE_URL` override `runtimeConfig.public.siteUrl` at runtime, and why does `siteUrl: process.env.SITE_URL` stop working after deployment?

level: middleimportance: should knowfreq 36%

answer

  1. prefix, then the key path
  2. only keys that already exist
  3. values are type-cast
  4. the built server ignores .env
  5. a build-time default is frozen

basics

~10 s

At runtime Nuxt maps each existing runtimeConfig key to an upper-snake-case NUXT_ variable, so public.siteUrl reads NUXT_PUBLIC_SITE_URL. process.env.SITE_URL in nuxt.config is read once at build time and frozen into the output.

solid answer

~50 s

When the built Nuxt server starts, and again per request for `useRuntimeConfig(event)`, Nitro walks the `runtimeConfig` keys that exist in the build and looks for a matching environment variable: the key path in upper snake case with a `NUXT_` prefix (Nitro's own `NITRO_` prefix also works). So `public.siteUrl` reads `NUXT_PUBLIC_SITE_URL` and `analyticsApiKey` reads `NUXT_ANALYTICS_API_KEY`. The key must be declared in `nuxt.config.ts`, even as an empty string; unknown variables are ignored. Values are cast by type, so `4848e0` becomes the number 4848 unless quoted. By contrast `siteUrl: process.env.SITE_URL` is evaluated when `nuxt build` runs; the build machine's value is written into the output, or the key is dropped if it was unset, and setting `SITE_URL` on the server later changes nothing. The running server does not read `.env` either; that file is only loaded by the CLI.

code

bash · 4 lines
bash
nuxt build
NUXT_ANALYTICS_API_KEY=ak_prod_123 \
NUXT_PUBLIC_SITE_URL=https://shop.example \
node .output/server/index.mjs

go deeper

for a junior

Recall that NUXT_ variables override runtimeConfig keys by name, with NUXT_PUBLIC_ for the public section, and that the key must exist in nuxt.config.

for a middle

Explain the name mapping, the destr type cast, the build versus run split, and why process.env reads in nuxt.config are frozen at build time.

for a senior

Show how you keep secrets out of build artifacts with empty defaults, configure every environment through variables, and account for prerendered pages that ignore them.

for a principal

Decide how configuration flows from your secret store to running Nuxt servers, and make one-build-many-environments the enforced default rather than a convention.

## Two moments that are easy to confuse A Nuxt 4 app has a **build moment** and a **run moment**: - **Build**: `nuxt build` loads `nuxt.config.ts` in Node, reads `.env` if present, resolves every option, and writes `.output/`. The resolved `runtimeConfig` is serialised as JSON into the server bundle. - **Run**: `node .output/server/index.mjs` starts the Nitro server. It never loads `nuxt.config.ts` or `.env`. It starts from the JSON written at build time and applies **environment variables** on top. Everything about overrides follows from that split. ## How the override works At run time Nitro walks the keys of the built `runtimeConfig` and, for each one, checks for an environment variable named after its path: 1. Join the path with `_`: `public` then `siteUrl` gives `public_siteUrl`. 2. Convert to snake case and upper-case it: `PUBLIC_SITE_URL`. 3. Look for it with the `NITRO_` prefix, then with Nuxt's `NUXT_` prefix: `NUXT_PUBLIC_SITE_URL` is the documented name. 4. If found, **cast** the value with `destr`: numbers, booleans and JSON become typed values. | runtimeConfig key | Environment variable | |---|---| | `analyticsApiKey` | `NUXT_ANALYTICS_API_KEY` | | `public.siteUrl` | `NUXT_PUBLIC_SITE_URL` | | `public.analytics.sampleRate` | `NUXT_PUBLIC_ANALYTICS_SAMPLE_RATE` | Three rules come out of the implementation: - **Only declared keys are overridable.** Nitro iterates the keys that exist; a variable for a key you never declared is ignored. Declare secrets with an empty default: `analyticsApiKey: ''`. - **Values are type-cast.** Nuxt's docs give the example `NUXT_MY_VAR=4848e0` becoming the number `4848`. To keep a string, the value itself must contain double quotes, for example `NUXT_PUBLIC_SITE_ID='"4848e0"'` in a shell. - **Server routes should pass the event**: `useRuntimeConfig(event)` builds the config for that request with environment overrides applied. ## Why `process.env.SITE_URL` breaks ```ts // nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { public: { siteUrl: process.env.SITE_URL } }, }) ``` This line runs **at build time**. Two things then go wrong: - Nitro's override logic never looks at `SITE_URL`, which is not a `NUXT_` name, so setting it on the production server does nothing. - On a CI machine without `SITE_URL`, the value is `undefined`, and serialising the config to JSON drops keys whose value is `undefined`. The built config then has no `siteUrl` at all, so even a correctly named `NUXT_PUBLIC_SITE_URL` finds no key to fill. Nuxt's docs warn about exactly this: defaulting a key to a differently named variable only works at build time. The same line has a second cost for private keys. Whatever value the build machine had is written into the server output as JSON, so a secret read this way sits in every copy of the build artifact. The fix is to declare the key with a neutral default and set the matching variable where the server runs: - `siteUrl: ''` (or a local default) in `nuxt.config.ts`; - `NUXT_PUBLIC_SITE_URL=https://shop.example` in the production environment. ## Other surprises - **The running server ignores `.env`.** The CLI loads it for `nuxt dev`, `nuxt build` and `nuxt generate` only. In production, set real environment variables. - **Prerendered pages are frozen.** A page generated at build time contains the public config it was generated with; runtime variables affect only pages the server renders. - **Variables apply when read.** Changing an environment variable of a running process is not possible from outside; a new value means restarting or redeploying, but not rebuilding. - **Objects can be overridden whole.** A variable for an object key whose value is a JSON object is merged into that object, while a plain value replaces it; per-field variables are usually clearer. ## A working setup for the analytics site | Where | What | |---|---| | `nuxt.config.ts` | `analyticsApiKey: ''`, `public: { siteUrl: 'http://localhost:3000' }` | | local `.env` | `NUXT_ANALYTICS_API_KEY=` a development key | | staging server | `NUXT_ANALYTICS_API_KEY`, `NUXT_PUBLIC_SITE_URL=https://staging.shop.example` | | production server | the same two variables with production values | The build contains only safe defaults. Each environment supplies its own values when the server starts, and the local `.env` is used by `nuxt dev` alone. When a value looks wrong in production, check three things in order: that the key is declared in `nuxt.config.ts`, that the variable name matches the key path in upper snake case with the `NUXT_` prefix, and that the page in question is server-rendered rather than prerendered.

  • Why declare analyticsApiKey: '' in nuxt.config if the real value always comes from the environment?
    Nitro only applies environment variables to keys that exist in the built runtime config, so an undeclared key is never populated. Declaring it also puts it in the generated types and marks it private, because it sits outside `public`. The empty default keeps any real secret out of the build output.
  • How do you keep an ID like 4848e0 a string when overriding it through NUXT_PUBLIC_SITE_ID?
    Environment values are cast with `destr`, which reads `4848e0` as the number 4848. Put literal double quotes inside the value, for example `NUXT_PUBLIC_SITE_ID='"4848e0"'` in a shell, so the cast yields the string. Make sure your shell or deployment dashboard does not strip those quotes.
  • Can a public runtime config value differ between two requests to the same server?
    Not through environment variables: a running process has one environment, so every request sees the same overrides. On the client, `useRuntimeConfig()` returns a writable object, so code can change it for that browser session, but that is local state, not configuration.

saying these in an interview costs you the question

  • Any NUXT_ variable is picked up, even for keys not declared in nuxt.config.
  • The production server reads the project's .env file on start-up.
  • runtimeConfig values from process.env in nuxt.config are re-read on every server start.
  • Environment values always arrive as strings.
  • A NUXT_PUBLIC_ variable also updates pages that were prerendered at build time.