LUCKVM / CLOUD INFRASTRUCTURE

Explore LuckVM

Home Global Acceleration Domains Support News Company

How to Deploy a Node.js App on a VPS in 2026 - Complete PM2, Nginx & SSL Guide

Published: October 2026 · Reading time: 14 minutes · For: Node.js / Express / Fastify / NestJS developers

TL;DR

Quick path: Rent a VPS ($8.80/month gets you a 1 vCPU / 1GB RAM starter at LuckVM), install Node.js 22 LTS via NodeSource, clone your repo, run npm ci --production, install PM2 for process management, place your app behind Nginx with a Let's Encrypt SSL certificate, and you're live in ~30 minutes. This guide gives you copy-paste commands for every step.

 

1. Why Deploy Node.js on a VPS?

Services like Heroku, Render, Railway, and Vercel are fantastic for prototyping, but they grow expensive and restrictive the moment you need a persistent disk, long-running WebSocket connections, custom binaries, or a background worker. A $26/month VPS at LuckVM outperforms a $50/month Heroku dyno on raw CPU and gives you full root control.

You also avoid vendor lock-in: PM2 + Nginx runs on any Linux machine, and the configs in this guide work on Ubuntu 22.04, Debian 12, and any modern systemd distro.

Node.js request flow - Cloudflare to Nginx to PM2 to Node workers
Figure 1 — A production Node.js request path: traffic goes through Cloudflare, terminates TLS at Nginx, and fans out across PM2-managed Node worker processes via a Unix socket.

2. The Production Stack

Before writing any commands, understand what each piece does. You only need three things:

  • PM2 — a process manager that starts your app on boot, restarts it on crash, spreads load across multiple CPU cores, and captures logs.
  • Nginx — a reverse proxy that terminates TLS, serves static files directly (much faster than Express), and routes requests to your Node app via a Unix socket.
  • Let's Encrypt / Certbot — free, auto-renewing HTTPS certificates. Skip this and every browser will flag your site as "Not secure".
PM2 vs systemd vs forever vs nohup comparison
Figure 2 — PM2 is purpose-built for Node.js applications. systemd is robust but requires manual config for clustering. forever/nohup are for quick testing only, not production.

3. Choosing the Right VPS Plan

Node.js is single-threaded by default, but PM2's cluster mode will happily use every CPU core you give it. RAM is the tighter constraint — each Express process uses ~80-150MB idle.

VPS plan recommendations for Node.js by RPS
Figure 3 — LuckVM plan recommendations by expected requests-per-second. Prices as of October 2026.
Quick math: 1 vCPU with PM2 instances: 2 (over-subscription works well for I/O-bound Node apps) handles ~100 RPS comfortably. A 2 vCPU / 2GB RAM VPS at $26.40/month is the sweet spot for most production Node.js APIs.

4. Step-by-Step Deployment

9-step Node.js VPS deployment timeline
Figure 4 — The 9-step deployment process takes 30-60 minutes for the first app.

Step 1 — Provision and prepare the VPS

SSH into the server, update packages, create a non-root user, and lock down the firewall:

ssh root@your-server-ip
apt update && apt upgrade -y
adduser nodejs
usermod -aG sudo nodejs
ufw allow OpenSSH
ufw allow 80/tcp
ufw allow 443/tcp
ufw enable
CRITICAL: From this point on, never run Node.js as root. All npm, git, pm2, and app commands should run as the nodejs user. A compromised app running as root gives an attacker your entire server.

Step 2 — Install Node.js 22 LTS

Use the official NodeSource repository (the default apt package is usually years out of date):

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
node -v   # v22.x
npm -v    # 10.x

Step 3 — Clone your app and install dependencies

sudo su - nodejs
git clone https://github.com/you/myapp.git
cd myapp
npm ci --production
npm run build     # if needed
cp .env.example .env
nano .env         # set NODE_ENV=production, DATABASE_URL, etc.
Always use npm ci (not npm install) in production. It installs exactly the locked versions from package-lock.json, guaranteeing reproducible builds.

5. PM2 Setup with Clustering

Install PM2 globally and create ecosystem.config.js at your project root:

sudo npm install -g pm2
module.exports = {
  apps: [{
    name: 'myapp',
    script: './server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    max_memory_restart: '500M',
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    merge_logs: true,
    time: true
  }]
};

Start the app and configure auto-start on boot:

mkdir -p logs
pm2 start ecosystem.config.js --env production
pm2 save
sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u nodejs --hp /home/nodejs

Useful commands: pm2 logs (tail logs), pm2 monit (live dashboard), pm2 reload myapp (zero-downtime restart), pm2 restart myapp.

6. Nginx Reverse Proxy

Install Nginx and create a site config:

sudo apt install -y nginx
sudo nano /etc/nginx/sites-available/myapp
Nginx reverse proxy config and PM2 ecosystem.config.js
Figure 5 — Full Nginx server block (left) and PM2 ecosystem.config.js (right). Replace example.com and paths to match your app.

Minimal working config:

server {
  listen 80;
  server_name example.com www.example.com;

  location / {
    proxy_pass http://unix:/home/nodejs/myapp/run/pm2.sock;
    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_read_timeout 86400;
  }

  location /static/ {
    alias /home/nodejs/myapp/public/;
    expires 30d;
    add_header Cache-Control "public, immutable";
  }
}

Configure PM2 to listen on the Unix socket (set the socket path in your app via env var), then enable the site:

sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
WebSocket note: The Upgrade / Connection lines are mandatory for Socket.io, ws, and any realtime app. Without them, connections drop after 60 seconds.

7. Free SSL with Let's Encrypt

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

Choose "redirect" when Certbot asks. It edits your Nginx config automatically and installs a systemd timer for auto-renewal. Verify with sudo certbot renew --dry-run.

8. GitHub Actions CI/CD (Optional)

Auto-deploy on every push to main. Create .github/workflows/deploy.yml:

name: Deploy
on:
  push:
    branches: [main]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to VPS
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: nodejs
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ~/myapp
            git pull origin main
            npm ci --production
            npm run build
            pm2 reload myapp --env production

Add SSH_HOST and SSH_PRIVATE_KEY as repository Secrets on GitHub. Every push now deploys with zero downtime via PM2 rolling reload.

9. Production Security Checklist

  1. Never run as root — use the nodejs user; file permissions 644.
  2. Set NODE_ENV=production — enables caching, disables verbose errors, makes Express 2-3x faster.
  3. Secure .env — chmod 600 .env; add to .gitignore.
  4. Install Helmet — npm install helmet sets HSTS, X-Frame-Options, CSP headers.
  5. Add rate limiting — express-rate-limit on auth endpoints.
  6. Configure CORS properly — don't use origin: "*" in production.
  7. UFW only opens 22/80/443 — port 3000 must NOT be open; Node listens on 127.0.0.1 or Unix socket only.
  8. Health endpoint — GET /health for Uptime Kuma / monitoring.
  9. Cloudflare in front — free DDoS protection, CDN, WAF. See our Cloudflare guide.

10. FAQ

Why does my app die when I close SSH?

Because you ran it with npm start or node server.js directly, which are attached to your terminal session. PM2 daemonizes the process so it survives logout and reboots.

PM2 says "errored" — how do I debug?

Run pm2 logs myapp --lines 100. Common causes: missing .env, port already in use, or a missing build step.

Should I use Docker instead of PM2?

Both work. Docker Compose + restart-policy is great if you want reproducible environments; PM2 is the simplest path with one less abstraction layer. See our Docker beginner guide.

How many PM2 workers should I set?

For CPU-bound apps: one per vCPU. For I/O-bound apps (most APIs): instances: "max" works well; PM2 balances connections automatically.

Can I host multiple Node apps on one VPS?

Yes — give each a separate Unix socket (app1.sock, app2.sock), separate Nginx server blocks, and separate PM2 configs. See our multi-site VPS guide.

Do I need Redis / PostgreSQL on the same VPS?

For small apps, yes. Once you exceed ~2GB RAM or need high availability, offload databases to a managed service or separate VPS.

How do I update Node.js later?

Re-run the NodeSource setup script, then sudo apt install -y nodejs && pm2 rebuild && pm2 reload all.

Do I need Cloudflare?

Not mandatory, but strongly recommended for free DDoS mitigation, CDN caching, and IP hiding. See our Cloudflare guide.

Launch Your Node App Today

Deploying Node.js on a VPS is straightforward once you've done it once. LuckVM offers 7 global regions with NVMe SSD, 1 Gbps network, and VNC console access starting at just $8.80/month.

Browse Plans →

Related services

Compare the related LuckVM product plans, network options and resources. Final availability and pricing are subject to the order page. Buy NVMe Cloud VPS | Scalable Cloud Hosting & Servers