Skip to main content

15.5 Deployment Strategies — SSR/SSG/SWA Modes, Vercel/Cloudflare Deployment

Nuxt 3 Rendering Modes​

Nuxt 3 supports multiple rendering strategies from a single codebase. You can choose the best strategy for your project's needs, or mix and match them.

ModeDescriptionBest For
SSR(Server-Side Rendering)HTML generated on the server per requestReal-time data, personalized content
SSG(Static Site Generation)All HTML pre-generated at build timeBlogs, documentation, marketing pages
SWA(Static Web App / SPA)All rendering done on the clientAdmin dashboards, auth-gated apps
HybridDifferent strategy per routeMost production applications

SSR Mode (Default)​

SSR is the default rendering mode in Nuxt 3. HTML is generated on the server for every incoming request.

Characteristics​

  • Excellent SEO: Fully-formed HTML is delivered to search engines
  • Fast initial page load: The browser renders complete HTML immediately
  • Always fresh data: Data reflects the state at request time
  • Server required: Needs a Node.js server or serverless environment

Configuration​

// nuxt.config.ts — SSR is the default, so explicit config is optional
export default defineNuxtConfig({
ssr: true, // default
})

Verifying SSR Behavior​

<!-- pages/ssr-demo.vue -->
<template>
<div>
<h1>SSR Demo</h1>
<p>Rendered at: {{ renderTime }}</p>
<p>User Agent: {{ userAgent }}</p>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>

<script setup lang="ts">
// Runs on the server — fetches the latest data on every request
const { data: posts } = await useFetch('/api/posts')

// Access the server request context
const event = useRequestEvent()
const userAgent = event?.node.req.headers['user-agent'] ?? 'client'
const renderTime = new Date().toISOString()
</script>

SSG Mode (Static Site Generation)​

SSG pre-generates all pages as HTML files at build time. Serving them via a CDN delivers extremely fast response times.

Configuration​

// nuxt.config.ts
export default defineNuxtConfig({
// Generate the entire app as static files
nitro: {
prerender: {
crawlLinks: true, // Automatically discover all pages by following links
routes: ['/', '/about', '/contact'], // Explicitly add routes
},
},
})

Pre-rendering Dynamic Routes​

// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true,
// Fetch data to generate dynamic routes
routes: async () => {
// Call an API at build time
const posts = await $fetch('https://api.example.com/posts')
return (posts as any[]).map((post) => `/blog/${post.slug}`)
},
},
},
})

The nuxi generate Command​

# Generate a static site
npx nuxi generate

# Generated files are saved in .output/public/
# Deploy to any static host (Netlify, GitHub Pages, Cloudflare Pages, etc.)

Hybrid Rendering (routeRules)​

One of Nuxt 3's most powerful features is applying different rendering strategies per route through hybrid mode.

// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Home page — regenerate every 5 minutes (ISR: Incremental Static Regeneration)
'/': { isr: 300 },

// Blog list — pre-render at build time + CDN cache for 1 hour
'/blog': { prerender: true, headers: { 'cache-control': 's-maxage=3600' } },

// Individual blog posts — pre-rendered
'/blog/**': { prerender: true },

// Dashboard — SPA mode (requires authentication)
'/dashboard/**': { ssr: false },

// API — no cache, always fresh
'/api/user/**': { headers: { 'cache-control': 'no-store' } },

// Public API — CDN cache for 1 minute
'/api/posts': { cache: { maxAge: 60 } },
},
})

ISR (Incremental Static Regeneration)​

ISR combines the speed of static generation with the freshness of SSR:

// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Cache for 60 seconds, then regenerate in the background
'/products/**': { isr: 60 },
// Cache forever (until manually invalidated)
'/about': { isr: true },
},
})

Deployment Presets​

Nitro includes built-in presets for many deployment targets. Configure them with the NITRO_PRESET environment variable or in nuxt.config.ts.

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

Common presets:

PresetEnvironment
node-serverNode.js server (default)
vercelVercel Serverless Functions
vercel-edgeVercel Edge Functions
cloudflare-pagesCloudflare Pages
cloudflare-moduleCloudflare Workers
netlifyNetlify Functions
netlify-edgeNetlify Edge Functions
aws-lambdaAWS Lambda
staticFully static site

Deploying to Vercel​

Vercel is one of the most tightly integrated platforms for Nuxt 3.

Automatic Deployment​

# Install the Vercel CLI
npm install -g vercel

# Run from your project root
vercel

# Deploy to production
vercel --prod

Vercel automatically detects Nuxt 3 projects and applies the optimal configuration.

Custom vercel.json​

{
"buildCommand": "npm run build",
"outputDirectory": ".output",
"framework": "nuxtjs",
"regions": ["icn1"],
"env": {
"DATABASE_URL": "@database-url",
"JWT_SECRET": "@jwt-secret"
},
"headers": [
{
"source": "/api/(.*)",
"headers": [
{ "key": "X-Content-Type-Options", "value": "nosniff" },
{ "key": "X-Frame-Options", "value": "DENY" }
]
}
]
}

Setting Environment Variables​

# Add environment variables via the Vercel CLI
vercel env add DATABASE_URL production
vercel env add JWT_SECRET production

# Or configure them in the Vercel dashboard

nuxt.config.ts — Vercel Optimizations​

// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel',
// For Vercel Edge Functions:
// preset: 'vercel-edge',
},

routeRules: {
// Leverage Vercel's ISR
'/blog/**': { isr: 3600 },
'/products/**': { isr: 300 },
},
})

Deploying to Cloudflare Pages​

Cloudflare Pages deploys to a global edge network for ultra-low latency.

nuxt.config.ts Configuration​

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

Build and Deploy​

# Production build
npm run build

# Deploy with the Wrangler CLI
npm install -g wrangler
wrangler pages deploy .output/public

wrangler.toml Configuration​

name = "my-nuxt-app"
compatibility_date = "2024-01-01"
pages_build_output_dir = ".output/public"

[vars]
NUXT_PUBLIC_APP_NAME = "My Nuxt App"

# Secrets are set with: wrangler secret put DATABASE_URL

Cloudflare KV Storage Integration​

// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'cloudflare-pages',
cloudflare: {
pages: {
routes: {
include: ['/*'],
exclude: ['/assets/*'],
},
},
},
storage: {
// Use Cloudflare KV
cache: {
driver: 'cloudflare-kv-binding',
binding: 'CACHE', // KV namespace binding name defined in wrangler.toml
},
},
},
})

GitHub Actions CI/CD (Cloudflare Pages)​

# .github/workflows/deploy.yml
name: Deploy to Cloudflare Pages

on:
push:
branches: [main]
pull_request:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
permissions:
contents: read
deployments: write

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build
env:
NUXT_PUBLIC_API_BASE: ${{ secrets.API_BASE_URL }}

- name: Deploy to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: my-nuxt-app
directory: .output/public
gitHubToken: ${{ secrets.GITHUB_TOKEN }}

Node.js Server Deployment​

For self-managed servers.

Build and Run​

# Production build
npm run build

# Inspect the build output
ls .output/
# server/ — server code
# public/ — static assets

# Start the server
node .output/server/index.mjs

Process Management with PM2​

npm install -g pm2

# Start the server
pm2 start .output/server/index.mjs --name "nuxt-app"

# Restart
pm2 restart nuxt-app

# View logs
pm2 logs nuxt-app

# Start automatically on system boot
pm2 startup
pm2 save

ecosystem.config.js (PM2)​

// ecosystem.config.js
module.exports = {
apps: [
{
name: 'nuxt-app',
script: '.output/server/index.mjs',
instances: 'max', // One process per CPU core (cluster mode)
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000,
NITRO_HOST: '0.0.0.0',
},
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
},
],
}

Nginx Reverse Proxy​

# /etc/nginx/sites-available/nuxt-app
server {
listen 80;
server_name example.com www.example.com;

# Redirect HTTP → HTTPS
return 301 https://$host$request_uri;
}

server {
listen 443 ssl http2;
server_name example.com www.example.com;

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

# Serve static assets directly (bypass Node.js)
location /_nuxt/ {
alias /var/www/nuxt-app/.output/public/_nuxt/;
expires 1y;
add_header Cache-Control "public, immutable";
}

# Proxy to the Nuxt app
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
}
}

Docker Container Deployment​

# Dockerfile
FROM node:20-alpine AS base

# Dependencies layer
FROM base AS dependencies
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

# Build layer
FROM base AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production layer (minimal image)
FROM base AS runner
WORKDIR /app

# Run as a non-root user (security best practice)
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 nuxt

COPY --from=builder --chown=nuxt:nodejs /app/.output ./.output

USER nuxt
EXPOSE 3000

ENV PORT=3000
ENV HOST=0.0.0.0
ENV NODE_ENV=production

CMD ["node", ".output/server/index.mjs"]
# docker-compose.yml
version: '3.8'

services:
nuxt-app:
build: .
ports:
- '3000:3000'
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- JWT_SECRET=${JWT_SECRET}
restart: unless-stopped
healthcheck:
test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']
interval: 30s
timeout: 10s
retries: 3

nginx:
image: nginx:alpine
ports:
- '80:80'
- '443:443'
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- nuxt-app
restart: unless-stopped

Environment-Specific Configuration​

// nuxt.config.ts
const isDev = process.env.NODE_ENV === 'development'
const isProd = process.env.NODE_ENV === 'production'

export default defineNuxtConfig({
runtimeConfig: {
// Server-only secrets
databaseUrl: process.env.DATABASE_URL ?? '',
jwtSecret: process.env.JWT_SECRET ?? 'dev-secret',
// Config exposed to the client
public: {
apiBase: process.env.NUXT_PUBLIC_API_BASE ?? '/api',
appEnv: process.env.NODE_ENV ?? 'development',
},
},

nitro: {
// Apply preset only in production
preset: isProd ? (process.env.NITRO_PRESET ?? 'node-server') : undefined,
// Development storage
storage: isDev
? {
cache: { driver: 'fs', base: './.nitro/cache' },
}
: undefined,
},

// Production build optimizations
...(isProd && {
experimental: {
payloadExtraction: true,
renderJsonPayloads: true,
},
}),
})

Pro Tips​

1. Analyze Bundles to Optimize Build Size​

# Run the bundle analyzer
npx nuxi analyze

# Or trigger it automatically during build
NUXT_ANALYZE=true npm run build
// nuxt.config.ts
export default defineNuxtConfig({
build: {
analyze: {
// rollup-plugin-visualizer options
filename: '.nuxt/analyze/bundle.html',
open: true, // Automatically open in browser after analysis
},
},
})

2. Multi-Zone Architecture (Micro-Frontend)​

Run multiple Nuxt apps under a single domain:

// nuxt.config.ts (main app)
export default defineNuxtConfig({
routeRules: {
// Proxy /docs/**to a separate Nuxt app
'/docs/**': {
proxy: {
to: 'https://docs-app.example.com/**',
},
},
// Proxy /shop/**to another app
'/shop/**': {
proxy: {
to: 'https://shop-app.example.com/**',
},
},
},
})

3. Health Check Endpoint​

// server/api/health.ts
export default defineEventHandler(() => {
return {
status: 'ok',
timestamp: new Date().toISOString(),
version: process.env.npm_package_version ?? '0.0.0',
uptime: process.uptime(),
}
})

4. Security Headers​

// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
'/**': {
headers: {
'X-Frame-Options': 'SAMEORIGIN',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'strict-origin-when-cross-origin',
'Permissions-Policy': 'camera=(), microphone=(), geolocation=()',
'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
},
},
},
// Consider using the nuxt-security module
// modules: ['nuxt-security'],
})

5. Zero-Downtime Deployment Strategy​

# Blue-Green deployment script example
#!/bin/bash

# Build the new version
npm run build

# Start the new container (port 3001)
docker run -d --name nuxt-green -p 3001:3000 my-nuxt-app:latest

# Wait for health check to pass
until curl -sf http://localhost:3001/api/health; do
echo "Waiting for health check..."
sleep 2
done

# Switch Nginx traffic to the new version
sed -i 's/proxy_pass http:\/\/localhost:3000/proxy_pass http:\/\/localhost:3001/' /etc/nginx/sites-available/nuxt-app
nginx -s reload

# Stop and remove the old container
docker stop nuxt-blue
docker rm nuxt-blue
docker rename nuxt-green nuxt-blue

echo "Deployment complete!"

6. Performance Monitoring​

// server/plugins/monitoring.ts
export default defineNitroPlugin((nitroApp) => {
// Measure response time
nitroApp.hooks.hook('request', (event) => {
event.context._startTime = Date.now()
})

nitroApp.hooks.hook('afterResponse', (event) => {
const duration = Date.now() - (event.context._startTime ?? Date.now())
const url = getRequestURL(event)

// Warn on slow requests (over 500ms)
if (duration > 500) {
console.warn(`[SLOW] ${getMethod(event)} ${url.pathname} — ${duration}ms`)
}

// In a real app, send metrics to Datadog, New Relic, etc.
})
})