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

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

Vercel is great until you look at the bill. The Hobby tier is free but crippled for production. Pro is $20/seat/month, and once your site hits real bandwidth, the overages pile up fast — $40 per 100 GB. 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.

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 $400 overages 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 costs $400+ on Vercel vs $6 on Vultr.

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.

🎁 Get $300 free credit when you sign up (valid for 30 days, limited-time offer)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: 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 — 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 — 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: 'node_modules/next/dist/bin/next',
      args: 'start',
      instances: 'max',
      exec_mode: 'cluster',
      autorestart: true,
      watch: false,
      max_memory_restart: '500M',
      env_production: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
    },
  ],
}

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.

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 — 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 — no extra config 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 — 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

FAQ

Does this setup work for Next.js App Router?

Yes. The PM2 + Nginx setup works identically for App Router and Pages Router. App Router with React Server Components and streaming requires the X-Accel-Buffering: no header — covered in Step 3.

Can I run a database on the same VPS?

On Vultr’s 1 GB plan, running PostgreSQL or MySQL alongside Next.js is tight — similar to the Ghost CMS RAM situation. It’s doable with tuning, but I’d recommend a separate Vultr instance for the database (starting at $6/mo) or a managed database (Vultr’s managed PostgreSQL starts at $15/mo). If you want a lightweight option, SQLite works fine for low-traffic apps and runs comfortably on 1 GB alongside Next.js.

How do I handle environment variables in production?

Server-side variables (without NEXT_PUBLIC_ prefix) go in the env_production block of ecosystem.config.js or in a .env file in your project root. Never commit .env to git. NEXT_PUBLIC_ variables must be set at build time — they’re inlined into the JavaScript bundle and cannot be changed at runtime.

What if my build keeps running out of memory?

Two options: add more swap (up to 4 GB is fine on NVMe), or temporarily upgrade your Vultr plan for the build, then downgrade. Vultr allows plan changes without data loss. The NODE_OPTIONS='--max-old-space-size=1024' npm run build workaround increases Node’s heap limit and usually gets builds through on 1 GB RAM.

Is there a way to automate deployments (CI/CD)?

Yes — GitHub Actions can SSH into your Vultr server and run the update commands on every push to main. A basic workflow takes about 30 minutes to set up. Alternatively, tools like Kamal (from Basecamp) or Coolify handle the full deploy pipeline and are worth exploring if you’re managing multiple apps. That said, manual git pull + pm2 reload is perfectly fine for solo projects.


Final Verdict

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