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
runtimeConfigfor environment variables, withNUXT_prefix mapping. - Cloudflare Workers are fast and cheap but require V8-compatible code — no Node.js built-in modules.
- Docker deployments only need the
.outputdirectory, notnode_modules. - Always test with
nuxi build && nuxi previewbefore deploying — dev mode hides configuration issues.