4 min read
On this page

Deployment

Nuxt applications deploy differently depending on your rendering mode. A static site goes to any CDN. An SSR app needs a server runtime. Nuxt's build system, powered by Nitro, abstracts this through presets — you pick a target, and Nitro produces the right output format. The same codebase can deploy to Vercel, Cloudflare Workers, a Docker container, or a bare Node.js server with a config change.

Nitro Presets

Nitro presets control how your Nuxt app is bundled for production. Set the preset in nuxt.config.ts or via the NITRO_PRESET environment variable:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'cloudflare-pages', // or 'vercel', 'netlify', 'node-server', etc.
  },
})

Most hosting platforms are auto-detected. If you deploy to Vercel, Nitro picks the Vercel preset automatically. You only need to set it explicitly when deploying to less common targets or when you want to override auto-detection.

Deploying to Vercel

Vercel detects Nuxt automatically. Push to GitHub, connect the repo in Vercel's dashboard, and it works. No config needed for basic deployments.

For more control, add a vercel.json:

{
  "buildCommand": "npx nuxi build",
  "outputDirectory": ".output",
  "framework": "nuxt"
}

SSR pages run as Vercel Serverless Functions. Static pages are served from the CDN. routeRules with prerender: true generate static files at build time.

// nuxt.config.ts
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { isr: 3600 }, // ISR with 1-hour revalidation
    '/app/**': { ssr: false }, // SPA for the app section
  },
})

Environment Variables

Vercel injects environment variables at build and runtime. Access them through runtimeConfig:

// nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    // Server-only — never exposed to the client
    databaseUrl: '',
    stripeSecretKey: '',

    // Public — available on both server and client
    public: {
      apiBase: '',
      posthogKey: '',
    },
  },
})

Nuxt maps environment variables automatically. NUXT_DATABASE_URL maps to runtimeConfig.databaseUrl. NUXT_PUBLIC_API_BASE maps to runtimeConfig.public.apiBase. Set them in Vercel's dashboard under Settings > Environment Variables.

<script setup lang="ts">
const config = useRuntimeConfig()
// config.public.apiBase is available here
// config.databaseUrl is NOT available here (server-only)
</script>
// server/api/checkout.post.ts
export default defineEventHandler((event) => {
  const config = useRuntimeConfig()
  // config.stripeSecretKey is available in server routes
})

Deploying to Netlify

Similar to Vercel — Netlify detects Nuxt and configures the build automatically. SSR runs via Netlify Functions.

# netlify.toml (optional, for customization)
[build]
  command = "npx nuxi build"
  publish = "dist"

[build.environment]
  NODE_VERSION = "20"

For static sites using nuxi generate:

[build]
  command = "npx nuxi generate"
  publish = ".output/public"

Netlify edge functions can run Nuxt closer to users:

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'netlify-edge',
  },
})

Deploying to Cloudflare Workers

Cloudflare Workers run at the edge — your app executes in the data center closest to the user. Cold starts are near zero, and pricing is based on requests rather than compute time. This is an excellent choice for SSR apps with global traffic.

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'cloudflare-pages',
  },
})

Deploy with Wrangler:

npx nuxi build
npx wrangler pages deploy .output/public

Or connect your GitHub repo in the Cloudflare dashboard for automatic deployments.

Limitations

Cloudflare Workers use the V8 runtime, not Node.js. This means no access to fs, path, child_process, or other Node.js built-in modules. If your server routes or API handlers use Node-specific APIs, they will not work on Cloudflare without adaptation.

// This works on Cloudflare
export default defineEventHandler(async (event) => {
  const body = await readBody(event)
  const result = await fetch('https://api.stripe.com/v1/charges', {
    method: 'POST',
    headers: { Authorization: `Bearer ${useRuntimeConfig().stripeKey}` },
    body: new URLSearchParams(body),
  })
  return result.json()
})

// This does NOT work on Cloudflare — uses Node.js fs
import { readFileSync } from 'node:fs'
export default defineEventHandler(() => {
  const data = readFileSync('./data/products.json', 'utf-8')
  return JSON.parse(data)
})

Use Cloudflare KV or D1 for data storage instead of the filesystem.

Deploying to a Node.js Server

For self-hosted deployments, use the node-server preset. This produces a standalone Node.js server.

// nuxt.config.ts
export default defineNuxtConfig({
  nitro: {
    preset: 'node-server',
  },
})

Build and run:

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

The server starts on port 3000 by default. Override with the PORT or NITRO_PORT environment variable.

Docker

# Dockerfile
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx nuxi build

FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/.output .output

ENV HOST=0.0.0.0
ENV PORT=3000
EXPOSE 3000

CMD ["node", ".output/server/index.mjs"]
docker build -t my-nuxt-app .
docker run -p 3000:3000 --env-file .env my-nuxt-app

The .output directory is self-contained. You do not need node_modules in the runtime image — Nitro bundles everything the server needs. This keeps Docker images small (usually under 100MB).

Process Managers

In production, run the Node.js server behind a process manager:

# PM2
npm install -g pm2
pm2 start .output/server/index.mjs --name my-nuxt-app
pm2 save
pm2 startup

Put Nginx or Caddy in front for TLS termination, gzip compression, and static asset caching:

server {
    listen 443 ssl;
    server_name myapp.com;

    ssl_certificate /etc/letsencrypt/live/myapp.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/myapp.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Build Configuration

Analyzing the Build

# Generate a build report
npx nuxi build --analyze

This opens a visual report showing module sizes. Use it to find unexpectedly large dependencies that should be lazy-loaded or replaced.

Source Maps

Enable source maps in production for error tracking with Sentry, Datadog, or similar tools:

// nuxt.config.ts
export default defineNuxtConfig({
  sourcemap: {
    server: true,
    client: true,
  },
})

Upload source maps to your error tracking service during the build, then disable serving them to users — you do not want to expose your source code.

Common Pitfalls

Not setting environment variables correctly. runtimeConfig values are empty strings by default. If you forget to set NUXT_DATABASE_URL in your hosting platform, the app starts with an empty database URL and fails silently.

Using nuxi generate when you need SSR. Static generation is a build-time process. If your pages depend on user-specific data or real-time content, nuxi generate produces stale pages. Use nuxi build with SSR instead.

Including node_modules in Docker images. The .output directory is self-contained. Copying node_modules into your runtime image wastes space and slows down deployments.

Ignoring cold starts on serverless. Serverless functions (Vercel, Netlify) have cold starts. The first request after a period of inactivity takes longer. Use routeRules with prerender or swr for pages that need fast initial response times.

Hardcoding URLs. Do not hardcode https://myapp.com in your code. Use runtimeConfig.public.siteUrl or useRequestURL() so the same build works across staging and production.

Key Takeaways

  • Nitro presets control the build output format. Most platforms auto-detect the right preset.
  • Use runtimeConfig for environment variables, with NUXT_ prefix mapping.
  • Cloudflare Workers are fast and cheap but require V8-compatible code — no Node.js built-in modules.
  • Docker deployments only need the .output directory, not node_modules.
  • Always test with nuxi build && nuxi preview before deploying — dev mode hides configuration issues.