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
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:
.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
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:
NITRO_PRESET=cloudflare-pages npx nuxi build
SPA mode: ssr: false
// 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:
// 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:
# .env (development)
NUXT_API_SECRET=dev-secret-key
NUXT_PUBLIC_API_BASE=http://localhost:3000/api
# 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 generatefor an app that actually needs authentication, per-request data, or liveserver/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.publicinstead 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: falseis 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.