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.
| Mode | Description | Best For |
|---|---|---|
| SSR (Server-Side Rendering) | HTML generated on the server per request | Real-time data, personalized content |
| SSG (Static Site Generation) | All HTML pre-generated at build time | Blogs, documentation, marketing pages |
| SWA (Static Web App / SPA) | All rendering done on the client | Admin dashboards, auth-gated apps |
| Hybrid | Different strategy per route | Most 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:
| Preset | Environment |
|---|---|
node-server | Node.js server (default) |
vercel | Vercel Serverless Functions |
vercel-edge | Vercel Edge Functions |
cloudflare-pages | Cloudflare Pages |
cloudflare-module | Cloudflare Workers |
netlify | Netlify Functions |
netlify-edge | Netlify Edge Functions |
aws-lambda | AWS Lambda |
static | Fully 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.
})
})