Deploy Next.js on Vultr VPS with PM2 + Nginx (2026 Guide)

Vercel is great until you look at the bill. The Hobby tier is free but crippled for production. Pro is $20/seat/month and includes 1 TB of data transfer a month; traffic past that is billed at on-demand regional rates. For most Next.js apps, a $6 VPS with PM2 and Nginx handles the same load for a fraction of the cost. This guide walks through the exact setup for Next.js 16 on Ubuntu 24.04, including the one step most tutorials skip that causes broken CSS in production.

This post contains affiliate links. If you purchase through our links, we may earn a commission at no extra cost to you.

Deploy Next.js 16 on Vultr VPS with PM2 and Nginx — architecture overview
Deploy Next.js 16 on Vultr VPS with PM2 and Nginx: architecture overview

Vercel vs self-hosting: the cost case

Before touching a terminal, it’s worth being honest about when each option makes sense.

Scenario Vercel Vultr VPS ($6/mo)
Solo dev, low traffic $0 (Hobby) $6/mo
Pro tier (1 seat) $20/mo $6/mo
Team (5 seats) $100/mo $6/mo (no per-seat fee)
1 TB bandwidth/month Included in Pro ($20/mo); regional rates beyond 1 TB Included in $6 plan
Build minutes 6,000 min/mo cap Unlimited (your CPU)

The breakeven is fast. Once you’re past a small side project or need multiple team members without per-seat billing, self-hosting wins on cost. A site at 1 TB/month bandwidth still fits inside Vercel Pro’s included transfer, so it costs $20 on Vercel vs $6 on Vultr, and past 1 TB Vercel adds regional per-GB charges while the $6 Vultr plan includes 2 TB.

Vercel’s advantages are real though: zero DevOps, edge CDN built-in, and first-party Next.js integration by the same team. If you’d rather push code and never think about servers, Vercel at $20/mo is fair value. This guide is for everyone else.

Vultr‘s $6 High Performance plan (1 vCPU AMD EPYC-Genoa, 1 GB RAM, 25 GB NVMe SSD, 2 TB transfer, IPv4 included) is the baseline for this guide. New accounts get $300 in free credit valid for 30 days, which means you can follow this entire guide without spending anything.

🎁 Try Vultr with up to $300 in promo credit (new customers, select products): Claim your credit →

For context on how Vultr compares across its full plan lineup, see our Vultr pricing breakdown.


Stack overview

  • Next.js 16.2.7 (current stable, June 2026): requires Node.js 20.9.0+
  • PM2 7.0.1: process manager with auto-restart, cluster mode, systemd integration
  • Nginx: reverse proxy, SSL termination, static asset caching
  • Certbot: free Let’s Encrypt SSL
  • Ubuntu 24.04 LTS: recommended OS for this setup

Why PM2 instead of Docker? For a single Next.js app on a 1 GB VPS, PM2 uses ~100-150 MB less RAM than Docker’s overhead. Docker shines for multi-service isolation; for just Node.js + Nginx, PM2 is cleaner and simpler.


Step 1: server setup + swap (do this first)

Deploy a Vultr Ubuntu 24.04 instance. SSH in as root, then add swap immediately, because next build will OOM-kill on 1 GB RAM without it:

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

💡 Tip: Unsure how much swap your server needs? Our free Linux Swap Calculator recommends a size based on your RAM.

Then create a non-root user:

adduser yourname
usermod -aG sudo yourname
# Log out and back in as yourname

Update packages:

sudo apt update && sudo apt upgrade -y

Step 2: install Node.js via nvm

Do not use Ubuntu’s default apt install nodejs, because it installs an outdated version. Next.js 16 requires Node.js 20.9.0+. Use nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
source ~/.bashrc
nvm install 22
nvm use 22
node -v  # Must show v22.x.x

Then install PM2:

npm install -g pm2

Step 3: clone, configure, and build

Configure standalone mode (in your repo; commit this before deploying)

Edit next.config.js to enable standalone output and streaming-compatible Nginx headers:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
  async headers() {
    return [
      {
        source: '/:path*{/}?',
        headers: [{ key: 'X-Accel-Buffering', value: 'no' }],
      },
    ]
  },
}
module.exports = nextConfig

The X-Accel-Buffering: no header is required if you use React Suspense streaming or Server Component streaming through Nginx. Without it, responses buffer and streaming breaks.

Clone and build on the server

mkdir ~/apps && cd ~/apps
git clone https://github.com/your-username/your-nextjs-app.git
cd your-nextjs-app
npm ci
npm run build

Copy static assets (critical: most tutorials skip this)

After a standalone build, static files must be manually copied or production CSS/JS returns 404:

cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public

This is the most common cause of “app loads but looks broken” on first deployment.

Next.js standalone build steps — copy static assets after build
Next.js standalone build steps: copy static assets after build

Step 4: PM2 ecosystem config

Create ecosystem.config.js at the project root:

module.exports = {
  apps: [
    {
      name: 'nextjs-app',
      script: '.next/standalone/server.js',
      instances: 'max',
      exec_mode: 'cluster',
      autorestart: true,
      watch: false,
      max_memory_restart: '500M',
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
        HOSTNAME: '127.0.0.1',
      },
    },
  ],
}

instances: 'max' uses cluster mode, one worker per CPU core. On Vultr’s 1 vCPU plan, that’s one worker, but the config scales automatically if you upgrade to a larger plan later. The script points at the standalone server.js from Step 3, and HOSTNAME keeps it listening on localhost only, behind Nginx.

Start the app and persist across reboots:

pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup  # Copy and run the command it outputs

Verify it’s running: pm2 list should show your app with status online.


Step 5: Nginx reverse proxy

sudo apt install nginx -y
sudo nano /etc/nginx/sites-available/yourdomain.com

Paste this config (replace yourdomain.com with your actual domain):

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

server {
  listen 80;
  server_name yourdomain.com www.yourdomain.com;
  return 301 https://$host$request_uri;
}

server {
  listen 443 ssl;
  server_name yourdomain.com www.yourdomain.com;

  ssl_certificate /etc/letsencrypt/live/yourdomain.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/yourdomain.com/privkey.pem;

  gzip on;
  gzip_types text/plain application/json application/javascript text/css;

  location /_next/static {
    proxy_pass http://nextjs_upstream;
    expires max;
    add_header Cache-Control "public, max-age=31536000, immutable";
  }

  location / {
    proxy_pass http://nextjs_upstream;
    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;
  }
}

💡 Tip: Want a head start on the config? Our free Nginx Config Generator builds a working server block you can adapt to your setup.

Enable the config:

sudo ln -s /etc/nginx/sites-available/yourdomain.com /etc/nginx/sites-enabled/
sudo nginx -t  # Must return "test is successful"
sudo systemctl reload nginx

Step 6: SSL with Certbot

Your domain’s DNS A-record must already point to the Vultr server IP before this step, because Certbot validates via HTTP.

sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
sudo certbot renew --dry-run  # Verify auto-renewal works

Certbot patches your Nginx config automatically and installs a systemd timer for renewal. On Ubuntu 24.04 with Nginx, HTTP/2 is enabled by default on SSL listeners, so no extra config is needed.


Updating your deployment

When you push code changes:

cd ~/apps/your-nextjs-app
git pull origin main
npm ci
npm run build
cp -r .next/static .next/standalone/.next/static
cp -r public .next/standalone/public
pm2 reload ecosystem.config.js --env production  # Zero-downtime reload

pm2 reload restarts workers one at a time, so existing requests keep being served while new code rolls in.


Common pitfalls

Problem Cause Fix
OOM during npm run build 1 GB RAM exhausted Add 2 GB swap first; or: NODE_OPTIONS='--max-old-space-size=1024' npm run build
CSS/JS 404 in production Static assets not copied after standalone build cp -r .next/static .next/standalone/.next/static and cp -r public .next/standalone/public
502 Bad Gateway PM2 not running or wrong port pm2 list; curl http://localhost:3000 on server; check Nginx error log
NEXT_PUBLIC_ vars not updating Baked into bundle at build time Must rebuild after changing any NEXT_PUBLIC_ variable
App doesn’t start after reboot PM2 startup not configured pm2 startup → run the output command → pm2 save
Node.js version too old Ubuntu apt nodejs is outdated Use nvm to install Node.js 22 explicitly
Streaming broken through Nginx Nginx buffering enabled Add X-Accel-Buffering: no header in next.config.js

The $6 plan is enough once you outgrow Vercel’s free tier

For a Next.js app that’s outgrown Vercel’s free tier (or where you’d rather own the infrastructure), Vultr’s $6 plan with PM2 + Nginx is a clean, production-grade setup. The critical details that most tutorials miss: add swap before building, copy static assets after a standalone build, and configure PM2 startup so your app survives a reboot.

This stack scales reasonably well on a single VPS. When you outgrow 1 GB RAM (usually around 100+ concurrent users with a database), upgrade to the $12/mo 2 GB plan. For the full picture on Vultr’s plan options, see our Vultr review.

Vultr has a $300 free credit offer for new accounts, valid for 30 days, which is enough time to build, test, and ship without paying anything.


Next.js version, PM2 version, and Vultr pricing verified June 22, 2026.

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top