Deployment guide

Deploy a Nuxt Application on a VPS

Deploy on a VPS Cloud →

Tutorial

Development

Deploy a Nuxt Application on a VPS

Nuxt and its Nitro engine produce a standalone Node.js artifact, perfectly suited for self-hosting on a VPS. This approach frees you from cloud function quotas and gives you full control over caching, HTTP headers, and per-page rendering strategies. This guide covers the complete setup: choosing a rendering mode, project configuration, build, environment variables, process supervision with PM2, Nginx reverse proxy and HTTPS, logs, and finally troubleshooting the errors that block production.

12 min read7 steps
Contents· Why Self-Host Nuxt on a VPS?1/13
  1. 01Why Self-Host Nuxt on a VPS?
  2. 02Concrete Benefits of a Self-Hosted Nuxt
  3. 03SSR, SSG, SPA or Hybrid: Which Rendering Mode Should You Choose?
  4. 04Hardware and Software Prerequisites
  5. 05Starting from the Nuxt Stack Template
  6. 06Deploy Nuxt Step by Step
  7. 07Environment Variables: Public, Private and Reloading
  8. 08Optimize Rendering with routeRules
  9. 09Nitro server vs Docker vs Vercel/Netlify for hosting Nuxt
  10. 10Logs: Reading, Rotating, Cross-Checking with Nginx
  11. 11Troubleshooting: Common Production Errors
  12. 12Monitoring and Metrics with PM2
  13. 13The Official Documentation

Why Self-Host Nuxt on a VPS?

With Nitro, Nuxt 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.

You still need a server that accepts a permanent Node.js process: most shared hosting does not allow it, which rules out SSR and server routes from the start. On a VPS you also choose the Node.js version, the reverse proxy (Nginx or Caddy) and how TLS certificates are managed, with no artificial cap on CPU or memory.

Concrete Benefits of a Self-Hosted Nuxt

  • server/api server 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 routeRules rules (SSR, SSG, ISR, SWR) controlled on the server side.
  • Several Nuxt apps on a single VPS behind Nginx, each on its own internal port (3000, 3001…).
  • Runtime environment variables (runtimeConfig) managed directly on your own server.
  • Full control over HTTP headers (CSP, HSTS, cache-control) via Nitro middleware.
  • A site prerendered with nuxt generate, served by Nginx alone, with no permanent Node.js process.

SSR, SSG, SPA or Hybrid: Which Rendering Mode Should You Choose?

Scroll the table

ModeRenderingNode.js processBest for
SSR (default)Per request, on the serverYes, supervised by PM2Dynamic app, SEO, personalized content
SSG (`nuxt generate`)At build time, as static HTMLNo, Nginx serves `.output/public`Blog, showcase site, rarely changing content
SPA (`ssr: false` + `nuxt generate`)In the browserNo, Nginx is enoughInternal dashboard, app behind authentication
Hybrid (`routeRules`)Per route: SSR, prerender, SWR, ISRYes, Nitro serverApp mixing static pages and API routes

Hardware and Software Prerequisites

As with any modern framework, the Nuxt build consumes memory: aim for at least 2 GB of RAM (the build can be done in CI if your VPS is more modest). At runtime, 1 vCPU and 1 GB are enough for the Nitro server under light load; move up to 2 vCPU / 2 GB for applications with many simultaneous server/api routes, and to 2 vCPU / 4 GB if several Nuxt applications share the VPS.

On the version side, start from Nuxt 4: Nuxt 3 reached end of life on 31 July 2026 and no longer receives bug fixes or security patches. The deployment described here is the same for both versions, since Nitro is what produces the server. Install a Node.js LTS line that is still maintained, Node.js 24 or 26 (Node.js 20 has been end-of-life since 30 April 2026), via nvm or a node:24-alpine image, along with PM2 for process supervision. node-server is the preset Nitro applies by default to a production build: declaring it in nuxt.config.ts simply makes it explicit. A domain pointing to your VPS is required for the TLS certificate. Ubuntu 22.04 or 24.04 LTS remains the recommended base.

Starting from the Nuxt Stack Template

The Nuxt Stack template in the ServOrbit Marketplace prepares an Ubuntu 24.04 VPS with Node.js on the current LTS line of the NodeSource repository, PM2 installed globally, Nginx and PostgreSQL. It saves you installing the runtime: the steps below then start at the project configuration.

It is a runtime environment, not an application with a login screen: you get in over SSH (ssh root@<vps-ip>) to upload your code. On first access, Nginx serves its default page on port 80; your application's virtual host, the Let's Encrypt certificate (Certbot is not preinstalled) and your code are still to be set up. PostgreSQL listens only locally, with peer authentication for the postgres system user (sudo -u postgres psql): enough to plug a DATABASE_URL into 127.0.0.1 without exposing the database.

Deploy Nuxt Step by Step

  1. Configure the Project for Node

    Set the Nitro preset in nuxt.config.ts:

    export default defineNuxtConfig({
      nitro: { preset: 'node-server' }
    })

    Declare your secrets in runtimeConfig on the server side only — never in runtimeConfig.public. On the VPS, install Node.js 24 LTS via nvm install 24 && nvm use 24 && nvm alias default 24, then PM2 globally: npm install -g pm2. On the Nuxt Stack template, Node.js and PM2 are already in place.

  2. 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 build

    Nitro produces a self-contained .output folder containing server/index.mjs and 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.

  3. Create the Environment File

    Create /srv/nuxt-app/.env with your runtime variables. All variables prefixed NUXT_ override the corresponding keys in runtimeConfig at startup:

    NUXT_MY_SECRET_KEY=production-value
    NITRO_HOST=127.0.0.1
    NITRO_PORT=3000

    Never commit this file. Make sure it belongs to the user running PM2 and is readable only by that user (chmod 600).

    With NITRO_HOST=127.0.0.1, Nitro listens only on the loopback interface: only Nginx can reach it, and port 3000 is not exposed to the Internet.

  4. Launch the Nitro Server with PM2

    Start the application in cluster mode, asking Node to load the environment file: Nuxt does not read .env in production, and the PM2 command line has no dedicated option for it.

    pm2 start /srv/nuxt-app/.output/server/index.mjs \
      --name nuxt-app -i max \
      --node-args="--env-file=/srv/nuxt-app/.env"
    pm2 save
    pm2 startup

    -i max launches one instance per vCPU; this cluster mode is what makes the zero-downtime reloads of the last step possible. The pm2 startup command displays a line to copy-paste to create the systemd service. Run it with administrator rights. Verify the application is running: pm2 status should show the online state.

  5. 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.

  6. 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.com

    Certbot automatically modifies the Nginx block to listen on port 443 and force HTTP → HTTPS redirect. Verify that X-Forwarded-Proto is 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 with systemctl status certbot.timer.

  7. Update Without Service Interruption

    For an update, PM2 reload (not restart) guarantees continuity. Group the commands in a /srv/nuxt-app/deploy.sh script, which set -e stops at the first error, before any reload:

    #!/bin/bash
    set -e
    cd /srv/nuxt-app
    git pull origin main
    npm ci
    npm run build
    pm2 reload nuxt-app

    Make it executable (chmod +x deploy.sh) and call it from your CI/CD pipeline over SSH. In cluster mode, PM2 replaces the instances one by one: the new version takes over before the old one stops. For a rollback, revert to the previous commit, rebuild and reload. If the build happens in CI, transfer only the .output folder via rsync and reload without rebuilding on the server.

Environment Variables: Public, Private and Reloading

Nuxt organizes its runtime configuration on two levels. Keys placed under runtimeConfig.public are sent to the browser and remain readable on the server: public API URL, analytics identifier. The other runtimeConfig keys never leave the server: secret keys, database access.

At startup, an environment variable overrides the matching key if it follows the NUXT_<NAME> convention, or NUXT_PUBLIC_<NAME> for a public key: NUXT_PUBLIC_API_BASE replaces runtimeConfig.public.apiBase. A value read elsewhere, for example process.env.OTHER_VARIABLE written in nuxt.config.ts, is only picked up at build time and stays frozen in the artifact.

In practice, changing an API URL or a key therefore does not require a rebuild: you edit the .env on the server, then reload PM2 (pm2 reload nuxt-app). The same .output folder can thus serve staging and production with two different environment files. The only condition is to have the process load that file, since Nuxt no longer reads .env once the application is built — hence the --env-file option passed to Node when starting with PM2.

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

ApproachAdvantagesConstraints
Nitro `node-server` + PM2Simple, lightweight, zero-downtime reload, fixed VPS costSupervision to manage, single server without native high availability
Docker on VPSIsolation, reproducibility, rollback by image, multi-app composeImage to build and store, additional memory per container
Vercel / NetlifyZero configuration, global edge CDN, PR previewsFunction quotas, variable cost, no infrastructure control
Kamal (zero-downtime deployment)Orchestrates Docker and kamal-proxy, one-command deployment (`kamal deploy`)Ruby tool (gem or Docker image), longer initial config, suited for teams

Logs: Reading, Rotating, Cross-Checking with Nginx

PM2 centralizes the application's output: pm2 logs nuxt-app follows the live stream (standard output and errors), and pm2 logs nuxt-app --lines 100 shows the latest lines. The files are written to ~/.pm2/logs/, one -out.log and one -error.log per application, suffixed with the instance number in cluster mode.

These files do not rotate on their own. The pm2-logrotate module rotates them past 10 MB and keeps 30 by default; adjust these thresholds to the size of your disk:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7

When the browser gets a 502 or a 504, look at Nginx first: /var/log/nginx/error.log tells you whether it could not reach 127.0.0.1:3000 (process stopped, wrong port) or whether Nitro took too long to respond.

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
NUXT_* variables are only read when the Nitro process starts: after changing .env, reload with pm2 reload nuxt-app. Check that the file is actually loaded through --node-args="--env-file=…" rather than exported in the current shell, which disappears with the SSH session. If the value is missing in the browser only, the key must be declared under runtimeConfig.public and the variable prefixed NUXT_PUBLIC_.

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 max

PM2 automatically distributes requests across workers. Export these metrics to Prometheus and Grafana to get alerted on unexpected restarts — a sign of a bug or memory leak to fix before it affects users.

The Official Documentation

For advanced configuration and version changes, refer to the official Nuxt documentation (routeRules, runtimeConfig, modules), the Nitro Node.js runtime page for the NITRO_PORT and NITRO_HOST variables, and the PM2 cluster mode documentation. This guide covers going live on a VPS; the vendor documentation remains the reference for fine-tuning.

Deploy Your Nuxt with Full Autonomy

The ServOrbit Cloud VPS provides a template with Node.js, PM2, Nginx, and automatic SSL, ideal for hosting your Nuxt 3 applications with SSR and server routes.