Deploying a Rendered App

A Node server, a prerendered site and an edge deployment need different things from a host; here is what each one needs.

SyntaxNODE_ENV=production node .output/server/index.mjs

A server-rendered build produces an artifact that has to run somewhere. Nuxt builds that artifact with Nitro, and which shape it takes is a build-time choice called the preset: nitro.preset in nuxt.config.ts, or the NITRO_PRESET environment variable on the build command.

A Node server

nuxt build with the node-server preset writes .output/, started with NODE_ENV=production node .output/server/index.mjs. It listens on NITRO_PORT or PORT, defaulting to 3000, and binds NITRO_HOST or HOST, defaulting to 0.0.0.0. Set NODE_ENV=production: some dependencies, Vue Router among them, only drop development warnings when it is set. The host must keep the process alive, restart it, and give it enough memory for peak concurrent renders. .output/ is self-contained, so node_modules does not ship with it.

A prerendered site

nuxt generate crawls your routes and writes .output/public/, including 200.html and 404.html fallbacks and a _payload.json per route holding the data captured at build time. Any static host or CDN serves it. The only configuration it needs is the SPA fallback, so unknown paths return 200.html rather than the host's own 404.

An edge deployment

Presets for Cloudflare, Vercel and Netlify build for a per-request worker instead of a long-lived process. Latency drops because code runs near the user, but there is no filesystem, no long-running memory and a short CPU budget per request.

Example

Example · vue
<!-- app/pages/status.vue -->
<script setup lang="ts">
const config = useRuntimeConfig()

const apiBase: string = config.public.apiBase
const release: string = config.public.release
</script>

<template>
  <dl>
    <dt>API base</dt>
    <dd>{{ apiBase }}</dd>
    <dt>Release</dt>
    <dd>{{ release }}</dd>
  </dl>
</template>

<style scoped>
dl {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 0.25rem 1rem;
}
</style>

When to use it

  • Containerising a server-rendered app without shipping node_modules into the image.
  • Moving a site from a Node host to a CDN once its pages stopped needing per-request data.
  • Pointing one built image at staging or production by setting NUXT_PUBLIC_* variables.

More examples

Preset, runtime config and route rules

Keys under `runtimeConfig.public` reach the browser and are read with `useRuntimeConfig()`; keys outside `public` stay on the server. Each is overridden per environment by an env var, so `apiBase` becomes `NUXT_PUBLIC_API_BASE` and `apiSecret` becomes `NUXT_API_SECRET`, which is how one image serves staging and production. `failOnError: true` is worth setting so a broken prerendered route fails the build rather than shipping.

Example · ts
// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'node-server',
    prerender: {
      routes: ['/sitemap.xml'],
      crawlLinks: true,
      failOnError: true,
    },
  },
  runtimeConfig: {
    apiSecret: '',
    public: {
      apiBase: '/api',
      release: 'dev',
    },
  },
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { isr: 600 },
  },
})

The Node output in a container

The second stage copies `.output` and nothing else, because the build already bundled the server's dependencies. That keeps the image small and means no `npm install` at deploy time. `NITRO_PORT` is set here for clarity; most platforms inject `PORT` themselves and Nitro reads either.

Example · dockerfile
# Dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
ENV NODE_ENV=production
ENV NITRO_PORT=3000
COPY --from=build /app/.output ./.output
EXPOSE 3000
CMD ["node", ".output/server/index.mjs"]

Two builds, two kinds of host

The same source produces both. Switching is a build flag, not a rewrite, which is why it is worth knowing what each host requires before choosing. The prerendered build is the cheaper and more reliable option whenever content changes only at deploy time; the moment a page must reflect data newer than the last build, you need the server output or a cached route rule.

Example · bash
# A long-running server: needs a process manager and a health check
NITRO_PRESET=node-server npm run build
ls .output/server/index.mjs

# A prerendered site: needs a static host with a SPA fallback to 200.html
npx nuxt generate
ls .output/public

Discussion

  • Be the first to comment on this lesson.