nodejs

Deploying a Next.js app to a VPS with Nginx and PM2

Phuong NguyenPhuong Nguyen5 min read
Cover image for "Deploying a Next.js app to a VPS with Nginx and PM2"

Why self-host instead of using Vercel

Vercel is an excellent choice for most Next.js projects, but there are situations where you need a VPS instead: cost control at scale, data residency requirements, running Next.js alongside other services on the same machine, or simply wanting full control over the server.

This guide walks through deploying a Next.js app on a Ubuntu 22.04 VPS with:

  • PM2 to keep the Node.js process alive and restart it on crashes or reboots
  • Nginx as a reverse proxy to handle incoming HTTP/HTTPS traffic
  • Certbot for a free Let's Encrypt SSL certificate

The same approach works for any Debian-based distro.

Prerequisites

  • A VPS with Ubuntu 22.04 (2 GB RAM minimum — Next.js builds are memory-hungry)
  • A domain name pointed at the VPS IP
  • SSH access as a non-root user with sudo privileges
  • Node.js 18+ and npm installed

To install Node.js on a fresh server:

curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt install -y nodejs

Step 1: Build the app on the server

Clone your repository and install dependencies:

git clone https://github.com/your-user/your-app.git /var/www/your-app
cd /var/www/your-app
npm ci --omit=dev

Copy your production environment variables into a .env.production.local file (or set them as system environment variables — more on that later). Then run the build:

npm run build

The output lives in .next/. A successful build prints the route sizes and the message ✓ Compiled successfully.

Step 2: Install and configure PM2

PM2 is a process manager for Node.js. It restarts the app if it crashes and can start it automatically on server reboot.

sudo npm install -g pm2

Start Next.js through PM2:

pm2 start npm --name "your-app" -- start

This runs npm start (which runs next start) under PM2's supervision. Verify it is running:

pm2 status

You should see your-app with status online. To tail logs:

pm2 logs your-app

Save the PM2 process list so it survives a reboot:

pm2 save
pm2 startup

pm2 startup prints a command you need to run as root — copy and execute it. From this point, PM2 restarts your app automatically after a server restart.

By default next start listens on port 3000. You can change this with the -p flag if needed:

pm2 start npm --name "your-app" -- start -- -p 3001

Step 3: Configure Nginx as a reverse proxy

Install Nginx:

sudo apt install -y nginx

Create a site configuration file:

sudo nano /etc/nginx/sites-available/your-app

Paste this configuration, replacing your-domain.com with your actual domain:

server {
    listen 80;
    server_name your-domain.com www.your-domain.com;

    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;
    }
}

The Upgrade and Connection headers are needed for Next.js HMR websockets in development, but they do not hurt in production and keep the config reusable.

Enable the site and reload Nginx:

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

At this point, visiting http://your-domain.com should return your app.

Step 4: Add SSL with Certbot

Install Certbot and the Nginx plugin:

sudo apt install -y certbot python3-certbot-nginx

Request a certificate:

sudo certbot --nginx -d your-domain.com -d www.your-domain.com

Certbot modifies the Nginx config automatically to redirect HTTP to HTTPS and insert the certificate paths. When prompted, choose the redirect option so all HTTP traffic is sent to HTTPS.

Certificates are valid for 90 days. Certbot installs a systemd timer that auto-renews them before they expire. Check it with:

sudo systemctl status certbot.timer

Step 5: Manage environment variables

Avoid committing secrets to your repository. The cleanest approach for a VPS is a .env.production.local file in the project root that is not tracked by git.

For environment variables that change rarely, this file is fine. For variables managed by a team or rotated often, use a tool like direnv or export them in /etc/environment:

# /etc/environment
MY_SECRET=value

After changing /etc/environment, you need to restart PM2 so the new values are picked up:

pm2 restart your-app --update-env

Deploying updates

A straightforward update workflow:

cd /var/www/your-app
git pull
npm ci --omit=dev
npm run build
pm2 restart your-app

If the build fails, PM2 continues serving the previous .next/ build — nothing breaks until you explicitly restart.

For zero-downtime deploys you can use PM2's cluster mode, but that requires your app to handle multiple workers sharing the same port. The simpler approach above has a restart gap of a few seconds, which is acceptable for most personal or small-team projects.

Common issues

Build runs out of memory. Next.js builds can exceed 1 GB of RAM on large projects. Increase the Node.js heap limit:

NODE_OPTIONS="--max-old-space-size=2048" npm run build

502 Bad Gateway from Nginx. The app is not running on port 3000. Check pm2 status and pm2 logs your-app to see what failed.

Environment variables not available at runtime. Variables prefixed with NEXT_PUBLIC_ are inlined at build time. If you change them after the build, you must rebuild — restarting PM2 alone is not enough.

Nginx config test fails. Run sudo nginx -t and read the error message. A common mistake is a missing semicolon or a typo in the server name.

Summary

  • PM2 manages the Next.js process and restarts it on failure or reboot
  • Nginx terminates SSL and proxies requests to localhost:3000
  • Certbot handles certificate issuance and auto-renewal
  • Updates are a four-step pull → install → build → restart cycle

This setup runs well on a $6/month VPS and gives you full control over your deployment without relying on a managed platform.

Comments