Deploying Nuxt apps: Vue & Nuxt best practices explained. Nitro deploys almost anywhere.
By day 49 of your Vue.js and Nuxt journey, you've built components, structured your app, and handled state. Now comes the moment that separates hobby projects from production systems: deployment. Deploying Nuxt apps requires more than just pushing code to a server—it demands understanding where and how your app runs, how to optimize for different platforms, and how to cache intelligently across regions.
Nuxt's Nitro engine transforms your app into outputs tailored to almost any hosting environment. Whether you're deploying to a Node.js server, Vercel's edge network, Cloudflare Workers, or a static CDN, the same Nuxt codebase adapts through configuration. This article walks through deployment targets, presets, containerization, and caching strategies that keep your app fast and reliable in production.
Understanding Deployment Targets and Where They Fit
The first decision in deploying Nuxt apps is choosing your target. Nitro supports several deployment targets, each suited to different hosting needs and team capabilities. Your choice determines where your app renders, how it scales, and what infrastructure you manage.
Node.js servers (the default target) work on any VPS, container platform, or managed Node service like Render or Railway. You get full control over the runtime environment and can run both server-side rendering and API routes. This target suits teams comfortable managing servers and needing predictable costs.
Vercel and Netlify targets are zero-config options for teams wanting platform-specific optimizations. Vercel's target deploys your app to their serverless infrastructure, automatically handling edge caching and splitting code by route. Netlify's target behaves similarly but integrates with Netlify's build and deployment pipeline. Both require minimal configuration—add the target to nuxt.config.ts and deploy.
Cloudflare runs your Nuxt app on Cloudflare Workers, rendering HTML at edge locations globally. This delivers content from servers near your users, reducing latency dramatically. Cloudflare is ideal for content-heavy sites needing regional performance without managing multiple server instances.
Static generation prebuilds your entire site to HTML files, deployable to any CDN (Netlify, Vercel, AWS S3 + CloudFront). Use this for blogs, documentation, or marketing sites where content doesn't change per request. Static deployments are the cheapest and fastest—no server overhead, pure CDN distribution.
Here's how to configure different targets:
// nuxt.config.ts - Node.js (default)
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: false,
routes: ['/sitemap.xml', '/rss.xml']
}
}
})
// For Vercel
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: true
}
}
})
// For Cloudflare
export default defineNuxtConfig({
nitro: {
prerender: {
crawlLinks: false
}
}
})
Switch targets by setting the NITRO_PRESET environment variable during build or by adding preset to nuxt.config.ts. Teams using monorepos or CI/CD often inject the preset via environment variables to avoid code changes between deployments.
Nitro Presets: Configuring Output for Your Platform
A Nitro preset is a configuration layer that shapes how your built app executes on its target platform. Presets handle runtime details—environment variables, storage paths, cold start optimization—so you don't hardcode platform-specific logic into your app.
Common presets include node-server, vercel, netlify, cloudflare-workers, and static. Each preset generates different output in the .output directory after npm run build.
The node-server preset creates a production-ready Node.js app that listens on a port (default 3000). It includes server middleware, API routes, and SSR rendering. Use this when deploying to services that support Node.js directly.
The vercel preset generates serverless functions for each route, splitting your code optimally for Vercel's pricing model. Function size stays small, cold starts decrease, and you pay only for execution. Vercel's preset is "zero-config"—connect your GitHub repo, and Vercel detects it automatically.
The cloudflare-workers preset outputs code compatible with Cloudflare's Worker runtime. This isn't Node.js—it's a V8 JavaScript context with different APIs. The preset handles this translation, allowing you to write standard Nuxt code that runs on Cloudflare's edge.
The static preset prebuilds all routes and generates static HTML, CSS, and JS. No server is needed. Use this for sites where routes don't change between deploys—blogs, docs, marketing sites.
Configure presets in nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
presets: ['node-server'], // or 'vercel', 'netlify', 'cloudflare-workers', 'static'
storage: {
redis: {
driver: 'redis',
url: process.env.REDIS_URL
}
},
// Environment-specific settings
minify: true,
sourceMap: false
}
})
Presets also expose hooks for customizing build output. Want to inject environment variables into your bundle? Use Nitro's prerender hooks. Need custom middleware for your edge deployment? Presets let you layer it in.
Docker: Containerizing Your Nuxt Application
Many teams deploy Nuxt apps in Docker containers running on Kubernetes, ECS, or Docker Compose. Containerization ensures consistency between dev and production while simplifying scaling and updates.
After running npm run build, the .output/server/index.mjs file is your entry point. It's a standalone Node.js app requiring only the node_modules for dependencies. Build a Docker image that bundles this output with Node.js and runs it:
FROM node:18-alpine
WORKDIR /app
# Copy built output and package files
COPY .output .output
COPY node_modules node_modules
COPY package.json .
# Expose port and run the app
EXPOSE 3000
ENV NITRO_HOST=0.0.0.0 NITRO_PORT=3000
CMD ["node", ".output/server/index.mjs"]
A multi-stage Dockerfile reduces image size by building dependencies separately:
# Build stage
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --frozen-lockfile
COPY . .
RUN npm run build
# Runtime stage
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/.output .output
COPY --from=builder /app/node_modules node_modules
COPY package.json .
EXPOSE 3000
ENV NITRO_HOST=0.0.0.0 NITRO_PORT=3000
CMD ["node", ".output/server/index.mjs"]
Teams using ECS or Kubernetes typically extend this Dockerfile with health checks and graceful shutdown handlers. Add a health check endpoint in your Nuxt server middleware:
// server/middleware/health.ts
export default defineEventHandler((event) => {
if (event.node.req.url === '/health') {
return { status: 'ok', timestamp: new Date().toISOString() }
}
})
Then reference it in your Dockerfile:
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => r.statusCode === 200 ? process.exit(0) : process.exit(1))"
This ensures orchestrators can detect if your container is alive and restart it if needed. Deploy this image to your container registry (ECR, Docker Hub, etc.), then scale it with your orchestrator's load balancer in front.
Caching Strategy: Route Rules and CDN Integration
Deploying Nuxt apps fast requires intelligent caching. Combine Nitro's route rules with your CDN's cache headers to serve content from edge locations without hitting your server repeatedly.
Route rules in Nuxt let you control cache behavior per endpoint. Set cache duration, mark routes as prerenderable, or add custom headers. Here's a production example:
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
routeRules: {
// Cache static pages for 1 hour, revalidate in background
'/': { cache: { maxAge: 60 * 60 } },
'/about': { cache: { maxAge: 60 * 60 } },
// Cache blog posts for 24 hours
'/blog/**': { cache: { maxAge: 24 * 60 * 60 } },
// Cache API responses for 5 minutes
'/api/products/**': { cache: { maxAge: 5 * 60 } },
// Never cache dynamic pages or forms
'/cart': { cache: false },
'/checkout': { cache: false },
// Prerender these routes at build time
'/products/**': { prerender: true },
// Add custom headers
'/api/**': {
headers: { 'X-Custom-Header': 'api-v1' },
cache: { maxAge: 60 }
}
}
}
})
These cache headers propagate to your CDN. Vercel and Netlify automatically respect them. For self-hosted setups, add a reverse proxy like Nginx or deploy behind Cloudflare to enforce caching.
Pair route rules with a CDN for maximum performance. When a user requests a cached route, the CDN returns it from its edge server. After the cache expires, the CDN refetches from your origin server, keeping content fresh without blocking users.
Example Nginx reverse proxy config for a self-hosted Nuxt app:
upstream nuxt_app {
server localhost:3000;
}
server {
listen 80;
server_name myapp.com;
location / {
proxy_pass http://nuxt_app;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# Cache for 1 hour
proxy_cache_valid 200 1h;
proxy_cache_key "$scheme$request_method$host$request_uri";
}
location /api/ {
proxy_pass http://nuxt_app;
# Don't cache API responses (or cache briefly)
proxy_cache_valid 200 1m;
}
location /static/ {
proxy_pass http://nuxt_app;
# Cache static assets for 30 days
proxy_cache_valid 200 30d;
}
}
This setup caches HTML for 1 hour, static assets for a month, and API responses briefly. Users get instant page loads while your server handles requests efficiently.
Common Mistakes
Forgetting environment variables in Docker. Your built Nuxt app is bundled and doesn't re-read environment variables from the host. If your app needs NITRO_POSTGRES_URL or PUBLIC_API_BASE, pass them when running the container: docker run -e NITRO_POSTGRES_URL=... myapp:latest. For secrets, use your container orchestrator's secret management (Kubernetes secrets, ECS task role, etc.).
Assuming static generation works for all routes. Static prerendering crawls your site and prebuilds HTML. If you have protected routes, infinite pagination, or user-specific content, static generation fails silently—routes become 404s. Reserve static generation for public, enumerable content.
Setting cache duration too aggressively. A long cache on frequently updated content frustrates users. A blog post cached for 24 hours means edits take a day to appear. Use route rules to cache by content type: static pages get long TTLs, user content gets short ones or no cache.
Not monitoring cold starts on serverless. Vercel and Netlify cold starts (first invocation after idle) add 0.5–2 seconds to response time. If your app has large dependencies or slow server logic, measure cold start duration and optimize—code splitting, lazy loading, and keeping dependencies lean help.
Key Takeaways
- Choose your target based on your needs: Node servers for control, Vercel/Netlify for zero-config, Cloudflare for edge performance, static for content-only sites.
- Nitro presets configure how your app runs on each platform—they handle environment details so your code stays platform-agnostic.
- Docker containerization ensures consistency across dev and production while enabling scaling on Kubernetes or ECS. Use multi-stage builds to keep images lean.
- Route rules + CDN caching dramatically improve performance: cache static pages for hours, API responses for minutes, and disable caching for dynamic content.
- Test your deployment locally before going live. Run
npm run build && npm run previewto simulate production, then verify caching headers and performance with real tools like Lighthouse or WebPageTest. - Monitor your deployment in production: track error rates, response times, and cold start latency so you catch regressions early.






Comments
Loading comments…