How to Deploy a Node.js App on a VPS with PM2 and Nginx Reverse Proxy

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:

Fast • Reliable • Affordable VPS - DO It Now!

Get top VPS hosting with VPS.DO’s fast, low-cost plans. Try risk-free with our 7-day no-questions-asked refund and start today!