December 17, 2025 · 9 min

Nuxt 4 - Deploy with PM2 and Nginx (No Docker)

Production-ready Nuxt 4 deployment on a Linux server using PM2 and Nginx. Zero-downtime reloads, SSL with Certbot, logs, and best practices—no Docker required.

Nuxt 4 - Deploy with PM2 and Nginx (No Docker)

nuxt-pm2-nginx

A step-by-step guide to deploy a Nuxt 4 application on a VPS using PM2 and Nginx—no Docker. Includes HTTPS, logs, and zero-downtime reloads.

Tags: Nuxt 4, PM2, Nginx

Time to read: 9 min

Note on what’s new: Nuxt 4 ships with an improved Nitro node-server output that boots faster and handles graceful shutdown signals more reliably—perfect for PM2’s zero-downtime reloads.

Prerequisites

  • A Linux server (Ubuntu 22.04/24.04 recommended) with sudo access
  • A domain pointing to your server’s IP (A/AAAA record)
  • Node.js 20+ installed on the server
  • A Nuxt 4 app (repo or local project you can push to the server)

Tip: If you need Node.js quickly on Ubuntu:

sudo apt update
sudo apt install -y ca-certificates curl gnupg
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs build-essential git

Install PM2 globally:

sudo npm i -g pm2

1) Prepare your Nuxt 4 app (optional new project)

If you need a fresh app for testing:

mkdir -p ~/apps && cd ~/apps
npx nuxi@latest init nuxt4-app
cd nuxt4-app
npm install
npm run dev

Open http://localhost:3000 to verify in development. Add a tiny health endpoint for uptime checks:

Create server/api/health.get.ts:

export default defineEventHandler(() => ({
  status: 'ok',
  time: new Date().toISOString(),
}))

2) Production build

On your server (or CI), build the app:

# inside your project (e.g., /var/www/nuxt4-app)
npm ci
npm run build

This creates the Nitro output at .output/. The production server entry is .output/server/index.mjs.

Note: With Nuxt 4’s Nitro, you don’t need dev dependencies at runtime. You can build on CI and deploy only the .output folder plus any public assets if you prefer.

3) Run with PM2 (clustered, zero-downtime)

Create a dedicated user and directories (recommended):

sudo useradd -m -s /bin/bash nuxt
sudo mkdir -p /var/www/nuxt4-app /var/log/pm2/nuxt4-app
sudo chown -R nuxt:nuxt /var/www/nuxt4-app /var/log/pm2/nuxt4-app

Place your project at /var/www/nuxt4-app (git clone or copy). Then, as the nuxt user:

sudo -u nuxt -H bash -lc 'cd /var/www/nuxt4-app && npm ci && npm run build'

Create ecosystem.config.cjs at /var/www/nuxt4-app:

module.exports = {
  apps: [
    {
      name: 'nuxt4-app',
      cwd: '/var/www/nuxt4-app',
      script: '.output/server/index.mjs',
      exec_mode: 'cluster',
      instances: 'max', // use all CPU cores
      env: {
        NODE_ENV: 'production',
        PORT: '3000',         // Nitro respects PORT
        NITRO_HOST: '127.0.0.1' // bind internally
      },
      watch: false,
      max_memory_restart: '512M',
      out_file: '/var/log/pm2/nuxt4-app/out.log',
      error_file: '/var/log/pm2/nuxt4-app/error.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z'
    }
  ]
}

Start and persist:

sudo -u nuxt -H bash -lc 'cd /var/www/nuxt4-app && pm2 start ecosystem.config.cjs'
sudo -u nuxt -H bash -lc 'pm2 status'
sudo -u nuxt -H bash -lc 'pm2 logs nuxt4-app --lines 50'

Enable PM2 on boot (systemd):

sudo pm2 startup systemd -u nuxt --hp /home/nuxt
# PM2 will print a command. Copy/paste that command with sudo, then:
sudo -u nuxt -H bash -lc 'pm2 save'

Optional: enable log rotation to keep logs tidy:

sudo -u nuxt -H bash -lc 'pm2 install pm2-logrotate'
sudo -u nuxt -H bash -lc 'pm2 set pm2-logrotate:max_size 10M'
sudo -u nuxt -H bash -lc 'pm2 set pm2-logrotate:retain 10'

4) Configure Nginx as reverse proxy

Install Nginx:

sudo apt update
sudo apt install -y nginx

Create a site config at /etc/nginx/sites-available/nuxt4-app:

# /etc/nginx/sites-available/nuxt4-app

# Optional websocket upgrade mapping for cleanliness
map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

upstream nuxt4_upstream {
  server 127.0.0.1:3000;
  keepalive 64;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com www.example.com;

  # Tune as needed
  client_max_body_size 10m;

  # Gzip for text assets
  gzip on;
  gzip_comp_level 5;
  gzip_min_length 256;
  gzip_types
    text/plain text/css text/javascript application/javascript application/json
    application/xml+rss application/xml image/svg+xml application/vnd.ms-fontobject
    application/x-font-ttf font/opentype;

  # Proxy all traffic to Nuxt
  location / {
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $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_set_header X-Forwarded-Host $host;

    proxy_pass http://nuxt4_upstream;
  }

  # Optional: a lightweight health route that bypasses Nuxt
  location = /nginx-health {
    add_header Content-Type text/plain;
    return 200 'ok';
  }
}

Enable and reload:

sudo ln -s /etc/nginx/sites-available/nuxt4-app /etc/nginx/sites-enabled/nuxt4-app
sudo nginx -t
sudo systemctl reload nginx

You should now be able to open http://example.com and see your app proxied via Nginx to PM2/Nitro.

5) Add HTTPS with Certbot

Get and install certificates:

sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

Test auto-renewal:

sudo certbot renew --dry-run

Certbot will update your Nginx config to listen on 443 with ssl_certificate paths. Nginx reloads automatically.

6) Zero-downtime deployments

PM2 will gracefully restart cluster processes with no downtime:

  • Build-first, then reload:
cd /var/www/nuxt4-app
git pull
npm ci
npm run build
pm2 reload nuxt4-app --update-env
  • If you ship artifacts from CI:
    • Upload .output and public (if needed) to the server
    • Swap directories atomically (symlink strategy), then:
pm2 reload nuxt4-app --update-env

Notes:

  • No sticky sessions are required for typical Nuxt apps because SSR is stateless by default.
  • If you use in-memory sessions or web sockets with session affinity, handle session storage externally (e.g., Redis) to avoid affinity requirements.

7) Quick checks and troubleshooting

  • Process running:
pm2 status
pm2 logs nuxt4-app --lines 100
  • App health through Nginx:
curl -I https://example.com/health
  • Common issues:
    • 502 Bad Gateway: The app is not running or PORT mismatch. Confirm PM2 env PORT=3000 and that Nginx upstream points to the same.
    • Permission denied on port 80/443: Only Nginx should bind these ports; Nitro should bind to 127.0.0.1:3000.
    • Can’t find .output/server/index.mjs: You forgot to build. Run npm run build.
    • Wrong Node used by PM2 after reboot: Ensure you ran pm2 startup and pm2 save as the nuxt user.

8) Optional Nuxt 4 production tweaks

Add a few production-safe defaults in nuxt.config.ts:

export default defineNuxtConfig({
  nitro: {
    preset: 'node-server', // default on Node; explicit for clarity
    compressPublicAssets: true, // enables gzip for public assets
    serveStatic: true
  },
  experimental: {
    viewTransition: true // small UX polish if you use <NuxtLink>
  },
  app: {
    baseURL: '/', // ensure correct base when behind Nginx
  }
})

Note: compressPublicAssets lets Nitro serve pre-compressed assets (gz) when available; Nginx handles gzip on the proxy level as configured above.

Conclusion

You’ve deployed a Nuxt 4 app with:

  • PM2 running in cluster mode with log rotation and startup on boot
  • Nginx reverse proxy with WebSocket upgrades
  • HTTPS via Certbot
  • Zero-downtime reloads on each deploy

This setup is fast, reliable, and simple to operate—no Docker required.