
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:
- Full control — manage environment variables, server resources, and configuration freely without platform limitations
- Cost efficiency — a single VPS can host multiple Next.js projects simultaneously, compared to Vercel's per-project pricing model
- Custom domains and SSL — configure subdomains, wildcard SSL, and Nginx rules exactly as needed
- No bandwidth limits or serverless timeouts — ideal for apps that require long-running processes or high data transfer
- Co-locate with databases and services — reduce latency by keeping your app and database on the same network
Prerequisites
Before starting, ensure you have the following ready:
- Ubuntu 20.04 LTS or 22.04 LTS VPS with root or sudo access
- Node.js version 18 LTS or later (Node 20 LTS recommended)
- PM2 — process manager for Node.js applications
- Nginx — web server for reverse proxying
- A domain with DNS A record pointing to your VPS IP address
- Your Next.js project hosted in a Git repository
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 --versionAlternatively, 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 20Clone 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 buildAfter 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 saveNext.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-appPaste 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 nginxAdd 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.comCertbot 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.shYou 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:
pm2 status— view the status of all processespm2 restart nextjs-app— restart the applicationsudo nginx -t— validate Nginx configuration syntaxsudo tail -f /var/log/nginx/error.log— tail Nginx error logsudo ufw allow 'Nginx Full'— open firewall for HTTP and HTTPS traffic
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 listCluster 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 -5Get 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