How to Deploy a Node.js App on a VPS with PM2 and Nginx Reverse Proxy
Node.js powers some of the world’s most popular web applications — from real-time chat systems and REST APIs to Next.js frontends and Express backends. Deploying a Node.js app on a VPS gives you full control over the runtime environment, zero platform lock-in, and the flexibility to scale exactly as your application demands.
In this guide, you’ll deploy a production-ready Node.js application on an Ubuntu VPS using PM2 as the process manager and Nginx as a reverse proxy. You’ll also configure HTTPS with Let’s Encrypt and set up zero-downtime reloads — the same architecture used by professional engineering teams.
The Production Stack Explained
| Component | Role | Why it’s needed |
|---|---|---|
| Node.js | JavaScript runtime | Executes your application code |
| PM2 | Process manager | Keeps your app alive, auto-restarts on crash, manages logs |
| Nginx | Reverse proxy + web server | Handles HTTPS, routes traffic to Node, serves static files |
| Let’s Encrypt | Free SSL certificates | Enables HTTPS with auto-renewal |
Node.js listens on an internal port (e.g., 3000). Nginx sits in front, accepting public traffic on ports 80 and 443, and forwards it to Node via a reverse proxy. PM2 ensures Node stays running 24/7, even after crashes or server reboots.
Requirements
- Ubuntu VPS 22.04 or 24.04 LTS with root/sudo access
- A domain name pointed to your VPS IP (for SSL)
- A Node.js application ready to deploy (or use the sample app in this guide)
- Basic familiarity with the Linux terminal and npm
💡 VPS.DO Tip: Node.js apps run comfortably on VPS.DO’s entry-level KVM VPS plans. 2 vCPU and 4 GB RAM handles most APIs and web apps serving hundreds of concurrent users. View USA VPS Plans →
Step 1: Update Your VPS
sudo apt update && sudo apt upgrade -y
Step 2: Install Node.js via NodeSource
Install from NodeSource instead of Ubuntu’s default repos — Ubuntu’s version is often several major versions behind:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
Verify:
node --version
npm --version
Step 3: Install PM2 Globally
sudo npm install -g pm2
pm2 --version
Step 4: Install Nginx
sudo apt install nginx -y
sudo systemctl start nginx
sudo systemctl enable nginx
Step 5: Configure the Firewall
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Step 6: Deploy Your Node.js Application
Option A: Clone from GitHub
sudo apt install git -y
cd /var/www
sudo git clone https://github.com/yourusername/your-app.git
sudo chown -R $USER:$USER /var/www/your-app
cd your-app
npm install --production
Option B: Create a Sample Express App (for testing)
mkdir -p /var/www/myapp && cd /var/www/myapp
npm init -y
npm install express
nano index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
message: 'Node.js app running on VPS.DO!',
timestamp: new Date().toISOString()
});
});
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Test it locally:
node index.js
# In another terminal:
curl http://localhost:3000
Press Ctrl+C to stop before the next step.
Step 7: Start Your App with PM2
cd /var/www/myapp
pm2 start index.js --name "myapp"
For Next.js or apps with a custom npm start script:
pm2 start npm --name "myapp" -- start
Check status:
pm2 status
You should see your app listed as online. ✅
Essential PM2 Commands
| Command | Action |
|---|---|
pm2 status |
Show all running processes |
pm2 logs myapp |
Stream real-time logs |
pm2 restart myapp |
Restart the app |
pm2 reload myapp |
Zero-downtime reload |
pm2 stop myapp |
Stop the app |
pm2 monit |
Live CPU/RAM dashboard |
Step 8: Enable PM2 Auto-Start on Reboot
pm2 startup
PM2 outputs a command — copy and run it exactly as shown. Then save the process list:
pm2 save
Your app now survives server reboots automatically. ✅
Step 9: Configure Nginx as a Reverse Proxy
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
listen [::]:80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
# WebSocket support
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
# Pass real client info to Node.js
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;
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# Serve static files directly via Nginx (faster than proxying)
location /static/ {
alias /var/www/myapp/public/;
expires 30d;
add_header Cache-Control "public, no-transform";
}
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log;
}
Enable and reload:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Visit http://yourdomain.com — your Node.js app should respond through Nginx. ✅
Step 10: Add Free SSL with Let’s Encrypt
sudo apt install certbot python3-certbot-nginx -y
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbot automatically configures HTTPS and sets up HTTP-to-HTTPS redirects. Test auto-renewal:
sudo certbot renew --dry-run
Your Node.js app is now live at https://yourdomain.com. ✅
Step 11: Environment Variables with PM2 Ecosystem File
Never hardcode secrets in source code. Use a PM2 ecosystem config file:
nano /var/www/myapp/ecosystem.config.js
module.exports = {
apps: [{
name: 'myapp',
script: 'index.js',
instances: 'max', // Cluster mode — use all CPU cores
exec_mode: 'cluster',
watch: false,
max_memory_restart: '500M',
env_production: {
NODE_ENV: 'production',
PORT: 3000,
DB_HOST: 'localhost',
DB_PASS: 'yoursecretpassword',
API_KEY: 'yourapikey'
}
}]
};
pm2 delete myapp
pm2 start ecosystem.config.js --env production
pm2 save
Cluster mode automatically spawns one worker per CPU core, maximizing performance without any code changes.
Step 12: Zero-Downtime Deployments
When pushing code updates, use pm2 reload instead of restart — it replaces workers one by one so the app stays online:
cd /var/www/myapp
git pull origin main
npm install --production
pm2 reload myapp
Wrap it in a deploy script for convenience:
nano ~/deploy.sh
#!/bin/bash
set -e
echo "Pulling latest code..."
cd /var/www/myapp && git pull origin main
echo "Installing dependencies..."
npm install --production
echo "Reloading app (zero downtime)..."
pm2 reload myapp
echo "Deploy complete ✅"
pm2 status
chmod +x ~/deploy.sh
Run ~/deploy.sh for every future deployment.
Step 13: PM2 Log Rotation
Prevent logs from filling your disk with the PM2 log rotate module:
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
This keeps 7 days of compressed logs and rotates when a log file hits 50 MB.
Running Multiple Node.js Apps on One VPS
Each app runs as a separate PM2 process on a different port. Nginx routes domains to the right app:
# App 1: yourdomain.com → port 3000 (already configured)
# App 2: api.yourdomain.com → port 3001
server {
listen 443 ssl;
server_name api.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3001;
# ... proxy headers ...
}
}
# App 3: admin.yourdomain.com → port 3002
server {
listen 443 ssl;
server_name admin.yourdomain.com;
location / {
proxy_pass http://127.0.0.1:3002;
# ... proxy headers ...
}
}
No additional VPS required — a single VPS.DO plan comfortably runs multiple Node.js apps simultaneously.
Troubleshooting Common Issues
502 Bad Gateway
Your app isn’t running or is crashing at startup. Check PM2:
pm2 status
pm2 logs myapp --lines 50
App crashes immediately
Run it directly to see the error message:
cd /var/www/myapp && node index.js
Port already in use
sudo lsof -i :3000
sudo kill -9 PID_FROM_ABOVE
PM2 process list empty after reboot
pm2 startup
pm2 save
WebSocket connections dropping
Confirm these two headers are present in your Nginx location block:
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
Final Thoughts
The PM2 + Nginx combination is the industry standard for deploying Node.js in production — and with good reason. PM2 handles process management, clustering, log rotation, and auto-restart. Nginx provides HTTPS termination, static file serving, and multi-app routing behind a single IP address.
With this stack running on a VPS.DO KVM VPS, you have a production-grade Node.js deployment that handles thousands of concurrent connections, survives reboots automatically, and deploys updates with zero downtime — all on an affordable $20/month plan.
Need help with your Node.js deployment? VPS.DO’s technical support team is online 24/7. Open a support ticket →
Related articles you might find useful: