Why Self-Host Nuxt on a VPS?
With Nitro, Nuxt 3 generates a portable build (preset: node-server) that starts as a simple Node server listening on a port. This portability is ideal for a VPS: you benefit from server-side rendering, server/api routes, and route caching without depending on a proprietary edge runtime.
Self-hosting removes function quotas and lets you tune caching, compression, and the number of Node instances. For a production Vue 3 application with authentication, server API calls, and careful SEO, the VPS offers the stability of a persistent server and a fixed cost, with no per-invocation billing surprises.
Concrete Benefits of a Self-Hosted Nuxt
server/apiserver routes with no per-call billing.- SSR and Vue 3 hydration served from a persistent, stable Node server.
- Portable Nitro build (
node-server) easy to containerize and reproduce. - Route caching and
routeRulesrules (SSR, SSG, ISR, SWR) controlled on the server side. - Pooling of several Nuxt apps on a single VPS behind Nginx.
- Runtime environment variables (
runtimeConfig) managed directly on your own server. - Full control over HTTP headers (CSP, HSTS, cache-control) via Nitro middleware.
Hardware and Software Prerequisites
As with any modern framework, the Nuxt build consumes memory: aim for 2 GB of RAM minimum (the build can be run in CI if your VPS is smaller). At runtime, 1 vCPU and 1 GB are enough for the Nitro server under light load; go up to 2 vCPU / 2 GB for applications with many concurrent server/api routes.
Install Node.js 20 LTS via nvm or a node:20-alpine image, along with PM2 for process supervision. Configure nitro: { preset: 'node-server' } in nuxt.config.ts. A domain pointed to your VPS is required for the TLS certificate. Ubuntu 22.04 or 24.04 LTS remains the recommended base.
Deploy Nuxt Step by Step
Configure the Project for Node
Set the Nitro preset in
nuxt.config.ts:export default defineNuxtConfig({ nitro: { preset: 'node-server' } })Declare your secrets in
runtimeConfigon the server side only — never inruntimeConfig.public. On the VPS, install Node.js 20 LTS vianvm install 20 && nvm use 20 && nvm alias default 20, then PM2 globally:npm install -g pm2.Build the Application
Clone the repository and install dependencies:
git clone https://git.yourdomain.com/your-org/your-app.git /srv/nuxt-app cd /srv/nuxt-app npm ci npm run buildNitro produces a self-contained
.outputfolder containingserver/index.mjsand static assets in.output/public. This folder is all the VPS needs to serve the application in production — you can also assemble it in CI and transfer it by rsync to avoid building on the server.Create the Environment File
Create
/srv/nuxt-app/.envwith your runtime variables. All variables prefixedNUXT_override the corresponding keys inruntimeConfigat startup:NUXT_MY_SECRET_KEY=production-value NITRO_HOST=0.0.0.0 NITRO_PORT=3000Never commit this file. Make sure it belongs to the user running PM2 and is readable only by that user (
chmod 600).Launch the Nitro Server with PM2
Start the application and configure automatic startup:
pm2 start /srv/nuxt-app/.output/server/index.mjs \ --name nuxt-app \ --env-file /srv/nuxt-app/.env pm2 save pm2 startupThe
pm2 startupcommand displays a line to copy-paste to create the systemd service. Run it with administrator rights. Verify the application is running:pm2 statusshould show theonlinestate.Configure Nginx as a Reverse Proxy
Create
/etc/nginx/sites-available/nuxt-app:server { listen 80; server_name yourdomain.com www.yourdomain.com; # Hashed assets: long cache, served directly location /_nuxt/ { root /srv/nuxt-app/.output/public; expires 1y; add_header Cache-Control "public, immutable"; } # Static public files location /favicon.ico { root /srv/nuxt-app/.output/public; } # Everything else → Nitro location / { proxy_pass http://127.0.0.1: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; } }Enable the site and test the configuration:
ln -s /etc/nginx/sites-available/nuxt-app /etc/nginx/sites-enabled/ && nginx -t && systemctl reload nginx.Enable HTTPS with Let's Encrypt
Install Certbot and the Nginx plugin, then generate the certificate:
apt install certbot python3-certbot-nginx certbot --nginx -d yourdomain.com -d www.yourdomain.comCertbot automatically modifies the Nginx block to listen on port 443 and force HTTP → HTTPS redirect. Verify that
X-Forwarded-Protois correctly forwarded to Nitro — without it,useRequestHeaders()on the server side and protocol-based redirects may behave incorrectly. Automatic renewal is managed by Certbot's systemd timer, checkable withsystemctl status certbot.timer.Update Without Service Interruption
For an update, PM2 reload (not restart) guarantees continuity:
cd /srv/nuxt-app git pull npm ci npm run build pm2 reload nuxt-appPM2 starts the new version, waits for it to respond, then cuts the old one: zero downtime. For a rollback, revert to the previous commit, rebuild and reload. If the build happens in CI, transfer only the
.outputfolder via rsync and reload without rebuilding on the server.
Optimize Rendering with routeRules
One of Nuxt's strengths on a VPS is the granularity of per-page rendering strategies, without changing infrastructure. In nuxt.config.ts, routeRules allow you to mix SSR, SSG, ISR, and SWR on a single Nitro deployment.
Static pages (blog, legal notices, landing pages) can be pre-generated at build time with prerender: true: Nitro serves them from disk, without Node. Semi-dynamic pages (product listings, public dashboards) benefit from swr: 60 for a one-minute cache invalidated in the background. Full SSR remains reserved for personalized pages that depend on session or user profile.
This approach reduces the load on Node without requiring a paid CDN or an external cache layer — your Nginx caches what Nitro pre-generates.
Nitro server vs Docker vs Vercel/Netlify for hosting Nuxt
Scroll the table
| Approach | Advantages | Constraints |
|---|---|---|
| Nitro `node-server` + PM2 | Simple, lightweight, zero-downtime reload, fixed VPS cost | Supervision to manage, single server without native high availability |
| Docker on VPS | Isolation, reproducibility, rollback by image, multi-app compose | Image to build and store, additional memory per container |
| Vercel / Netlify | Zero configuration, global edge CDN, PR previews | Function quotas, variable cost, no infrastructure control |
| Kamal (zero-downtime deployment) | Orchestrates Docker + Traefik proxy, git push is enough | Ruby required, longer initial config, suited for teams |
Troubleshooting: Common Production Errors
Error: listen EADDRINUSE: address already in use :::3000
A process is already holding port 3000. Identify it with ss -tlnp | grep 3000 and stop the orphaned PM2 instance: pm2 delete nuxt-app followed by a clean restart. Avoid running multiple instances without a load balancer — configure PM2's cluster_mode instead.
502 Bad Gateway in Nginx
Nitro is not started or is listening on the wrong port. Check pm2 status and review the logs: pm2 logs nuxt-app --lines 50. Also verify that the proxy_pass directive in Nginx points to http://127.0.0.1:3000 (not localhost which may resolve to IPv6 if the process only listens on IPv4).
useRuntimeConfig() returns undefined on the server sideNUXT_* variables are only read when the Nitro process starts. If you modify .env after launch, reload: pm2 reload nuxt-app. Make sure you use --env-file at startup and not export variables in the current shell.
server/api routes return 404 in production
Check that your build includes the server/ folder in .output. A misconfigured nitro.preset (for example static instead of node-server) generates a static site without a server. Rebuild with npm run build after verifying nuxt.config.ts.
X-Forwarded-Proto ignored, redirect loop
If Nuxt redirects HTTP → HTTPS in a loop, it means the redirect middleware sees http despite the client-side HTTPS. Make sure Nginx forwards proxy_set_header X-Forwarded-Proto $scheme and that your nuxt.config.ts does not force HTTPS redirect twice alongside Certbot.
Monitoring and Metrics with PM2
Beyond simple pm2 status, PM2 exposes a real-time dashboard with pm2 monit: CPU, memory, restarts, and process logs. For a Nuxt application under load, enable cluster mode to use all available vCPUs:
pm2 start .output/server/index.mjs --name nuxt-app -i maxPM2 automatically distributes requests across workers. Pair PM2 with a tool like Grafana via pm2-prometheus for alerts on unexpected restarts — a sign of a bug or memory leak to fix before it affects users.