Deploying Nuxt Apps

Static generation vs server deployment vs SPA mode, and environment config with runtimeConfig.

Nuxt's build output depends on the target

Because Nuxt supports several rendering modes (covered on the data-fetching page), it also supports several distinct deployment shapes — the same source code can be built into a fully static site, a server that renders on every request, or a single-page app with no server rendering at all. Choosing the right one for a given project matters more for Nuxt than for a plain client-only app, since picking the wrong target either strips out features the app actually relies on or pays for infrastructure it didn't need.

Static generation: nuxt generate

Bash
npx nuxi generate

This pre-renders every route to a plain static HTML file at build time, producing a .output/public directory deployable to any static host (a CDN, GitHub Pages, S3, Netlify's static hosting) with no Node.js server required at all:

Plaintext
.output/public/
├── index.html
├── about/
│   └── index.html
└── blog/
    └── hello-world/
        └── index.html

This is the right target for content that's identical for every visitor and doesn't depend on per-request data — documentation, a marketing site, a blog. It's the wrong target the moment a page needs something genuinely per-request: reading a cookie, showing per-user data, or calling server/api/ routes that need to run live rather than being baked in at build time — static generation has no server left at request time to run any of that.

Server deployment: nuxt build + nuxt start

Bash
npx nuxi build
node .output/server/index.mjs

nuxt build produces a .output directory containing a self-contained Node.js server (via Nitro, Nuxt's server engine) that handles universal rendering, server/api/ routes, and on-demand SSR per request — this is the target for anything that needs real server-side logic: authentication, live data that can't be baked in at build time, or server API routes consumed by the app itself.

Like a self-hosted Next.js app, this needs a long-running process kept alive by a process manager (pm2, systemd, or a platform's own equivalent) — unlike the static output above, there's an actual server here that can crash, needs restarting, and needs to keep running continuously to serve requests.

Nitro's real advantage for self-hosting is that it isn't tied to one specific runtime — the same Nuxt app can target Node.js, but also Deno, Cloudflare Workers, Vercel's or Netlify's serverless functions, and several other platforms, by changing a single build preset:

Bash
NITRO_PRESET=cloudflare-pages npx nuxi build

SPA mode: ssr: false

Typescript
// nuxt.config.ts
export default defineNuxtConfig({
  ssr: false,
});

With ssr: false, Nuxt ships an (almost) empty HTML shell and renders everything client-side, like a plain Vite + Vue SPA — no server rendering happens at all, for any route. This trades away SEO and fast first-paint (there's no meaningful content in the initial HTML for a crawler or a slow connection to see) for the simplest possible deployment: the build output is just static files, deployable anywhere, with no Node server needed, similar in spirit to nuxt generate but rendered per-visit in the browser instead of pre-rendered per-route at build time. This is a reasonable choice specifically for an internal tool behind a login, where SEO is irrelevant and every visitor already has a capable browser and a fast connection to the app.

Target Command Needs a server at runtime Best for
Static generation nuxi generate No Docs, marketing sites, blogs — content identical for every visitor
Server (universal/SSR) nuxi build + nuxi start Yes Apps needing auth, live per-request data, or server API routes
SPA ssr: false + a static build No Internal tools/dashboards where SEO and first paint don't matter

Environment variables per environment

Nuxt's runtimeConfig in nuxt.config.ts is the single place environment-dependent values are declared, split explicitly into a server-only section and a public section exposed to the browser:

Typescript
// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    apiSecret: '',            // server-only by default — never sent to the client
    public: {
      apiBase: '/api',        // exposed to both server and client code
    },
  },
});

Each value is overridable per environment via a matching environment variable, using a NUXT_ prefix and underscores for nesting — NUXT_API_SECRET overrides apiSecret, and NUXT_PUBLIC_API_BASE overrides public.apiBase:

Bash
# .env (development)
NUXT_API_SECRET=dev-secret-key
NUXT_PUBLIC_API_BASE=http://localhost:3000/api
Bash
# production environment variables (set via the host's dashboard/secrets, not committed)
NUXT_API_SECRET=prod-secret-key
NUXT_PUBLIC_API_BASE=https://api.example.com

The public/non-public split is a hard security boundary, not just an organizational convenience — anything under runtimeConfig.public is embedded into the client-side JavaScript bundle and readable by anyone who opens the browser's dev tools, while everything else stays server-only and is only ever readable from server/api/ routes and other server-side code. A real secret (an API key, a signing secret) placed under public by mistake ships directly to every visitor's browser.

Common mistakes

  • Choosing nuxt generate for an app that actually needs authentication, per-request data, or live server/api/ routes — static generation has no server at request time, so any of that silently stops working (or was never possible) in the static build.
  • Putting a genuine secret under runtimeConfig.public instead of the server-only top-level section — it gets bundled directly into client-side JavaScript, visible to any visitor.
  • Running the built server (node .output/server/index.mjs) without a process manager in production — a single unhandled crash takes the app down until someone notices and restarts it by hand.
  • Assuming ssr: false is a strictly "simpler" default for every project — it's the right trade-off for an internal tool, but a poor one for anything public-facing that needs to be indexed well by search engines or load fast on a slow connection.