Deploy Next.js App on VPS with PM2 and Nginx

Next.js is one of the most popular React frameworks for building modern web applications. While Vercel offers a convenient free tier, many developers and teams choose to deploy on their own VPS for complete control, cost savings, and flexibility. This guide walks you through every step from a fresh Ubuntu server to a production Next.js app running over HTTPS.

Why Deploy Next.js on VPS Instead of Vercel

Vercel is an excellent platform to get started, but as your project grows, several factors push teams toward self-hosted VPS deployments:

Prerequisites

Before starting, ensure you have the following ready:

Install Node.js 20 LTS via NodeSource

The recommended approach is using the NodeSource repository, which provides the latest Node.js versions and stays up to date:

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs node --version # Should output v20.x.x npm --version

Alternatively, use NVM (Node Version Manager) if you need to manage multiple Node.js versions:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash source ~/.bashrc nvm install 20 nvm use 20 nvm alias default 20

Clone Your Project and Build

SSH into your VPS and pull your code from the Git repository:

cd /var/www git clone https://github.com/youruser/your-nextjs-app.git cd your-nextjs-app # Install dependencies npm install --production=false # Create production environment file cp .env.example .env.local nano .env.local # Update with real values # Build for production npm run build

After a successful build, the .next/ directory will contain static assets and server bundles ready to serve.

Install and Configure PM2

PM2 is a process manager that keeps your Next.js app running continuously, even after a reboot or crash:

npm install -g pm2 # Start Next.js with PM2 cd /var/www/your-nextjs-app pm2 start npm --name "nextjs-app" -- start # Check status pm2 status pm2 logs nextjs-app # Configure auto-start on VPS reboot pm2 startup systemd # Run the command PM2 outputs (sudo env PATH=...) pm2 save

Next.js defaults to port 3000. You can specify a different port using the PORT=8080 environment variable.

Configure Nginx as a Reverse Proxy

Install Nginx and create a virtual host that forwards traffic from ports 80/443 to the Next.js server on port 3000:

sudo apt install -y nginx # Create a new virtual host config sudo nano /etc/nginx/sites-available/nextjs-app

Paste the following configuration:

server { listen 80; server_name yourdomain.com www.yourdomain.com; location / { proxy_pass http://localhost: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; } }
sudo ln -s /etc/nginx/sites-available/nextjs-app /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx

Add Free SSL with Certbot and Let's Encrypt

Install Certbot and obtain a free SSL certificate in one command:

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

Certbot automatically updates the Nginx config to add an HTTPS listener, redirect HTTP to HTTPS, and set up auto-renewal via systemd timer — everything configured in a single command.

Auto-Deploy with Git Pull and PM2 Reload

Create a shell script to redeploy your application in seconds:

#!/bin/bash # /var/www/your-nextjs-app/deploy.sh cd /var/www/your-nextjs-app git pull origin main npm install --production=false npm run build pm2 reload nextjs-app --update-env echo "Deploy complete at $(date)"
chmod +x /var/www/your-nextjs-app/deploy.sh # Run deployment with a single command ./deploy.sh

You can connect this script to GitHub Actions, GitLab CI, or any webhook to build a fully automated CD pipeline.

Manage Environment Variables in PM2

For environment variables that change without requiring a rebuild, use a PM2 ecosystem file:

// ecosystem.config.js module.exports = { apps: [{ name: 'nextjs-app', script: 'node_modules/.bin/next', args: 'start', env_production: { NODE_ENV: 'production', PORT: 3000 } }] }

Pro tip: Run pm2 logs nextjs-app to stream logs in real time, or pm2 monit to open a live dashboard showing CPU usage, memory, and error rates for each process — invaluable for quick debugging.

Troubleshooting Reference

Useful commands when things go wrong:

Boost Performance with Nginx Static Asset Caching

Configuring Nginx to serve Next.js static assets directly reduces load on the Node.js process and speeds up page loads significantly. Next.js places all compiled static files under /_next/static/ — these files are content-hashed and safe to cache for a full year:

server { listen 443 ssl; server_name yourdomain.com; # Serve Next.js static assets directly from disk location /_next/static/ { alias /var/www/your-nextjs-app/.next/static/; expires 1y; add_header Cache-Control "public, immutable"; } # Cache public images and media files location /images/ { root /var/www/your-nextjs-app/public; expires 30d; add_header Cache-Control "public"; } # Proxy all other requests to Next.js location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } }

Enable Gzip compression to reduce response sizes:

# Add inside the http{} block in /etc/nginx/nginx.conf gzip on; gzip_types text/plain text/css application/json application/javascript text/xml; gzip_min_length 1024; gzip_comp_level 5;

Scale with PM2 Cluster Mode on Multi-Core VPS

A VPS with 2 or more CPU cores can run Next.js in cluster mode, where PM2 spawns one worker per core and distributes incoming requests automatically using round-robin load balancing:

// ecosystem.config.js (updated) module.exports = { apps: [{ name: 'nextjs-app', script: 'node_modules/.bin/next', args: 'start', instances: 'max', // use all available CPU cores exec_mode: 'cluster', // enable cluster mode watch: false, max_memory_restart: '512M', env_production: { NODE_ENV: 'production', PORT: 3000 } }] }
# Start with cluster mode pm2 start ecosystem.config.js --env production pm2 save # Monitor individual workers pm2 list

Cluster mode note: PM2 cluster uses the same port (3000) and balances requests across workers. However, Next.js Server Actions and WebSocket connections may need sticky sessions — test thoroughly with your Nginx upstream config before going live.

VPS Spec Guide by Project Size

Choose the right VPS specification based on your Next.js app's traffic and complexity:

Project Size RAM CPU PM2 Mode Notes
Portfolio / Small Blog 1 GB 1 Core fork Sufficient for <1,000 req/day
E-Commerce / Mid SaaS 2–4 GB 2 Cores cluster Handles 10,000–50,000 req/day
Large App / API-heavy 8 GB+ 4+ Cores cluster max Consider adding a load balancer

Monitor Memory Leaks and Configure Restart Policy

Node.js applications can develop memory leaks over time. PM2's max_memory_restart option automatically restarts a process when RAM usage exceeds a set threshold, keeping the app stable without manual intervention:

# View real-time memory and CPU usage pm2 monit # Tail the last 200 log lines pm2 logs nextjs-app --lines 200 # View error log only pm2 logs nextjs-app --err # Clear old logs pm2 flush nextjs-app # Set restart threshold in ecosystem.config.js: # max_memory_restart: '400M'

Check overall VPS health with these commands:

# Available RAM free -h # Disk space in the web root df -h /var/www # CPU load average top -bn1 | head -5

Get a VPS to Run Next.js

AsiaGB VPS comes with full root access, SSD storage, and 99% Uptime guarantee starting at just 500 THB/month — ready for Node.js, PM2, and Nginx out of the box.

View VPS Plans

View all affordable VPS Thailand plans →