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.
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".
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.
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
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
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.
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
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
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
- Never run as root — use the
nodejsuser; file permissions644. - Set
NODE_ENV=production— enables caching, disables verbose errors, makes Express 2-3x faster. - Secure
.env—chmod 600 .env; add to.gitignore. - Install Helmet —
npm install helmetsets HSTS, X-Frame-Options, CSP headers. - Add rate limiting —
express-rate-limiton auth endpoints. - Configure CORS properly — don't use
origin: "*"in production. - UFW only opens 22/80/443 — port 3000 must NOT be open; Node listens on 127.0.0.1 or Unix socket only.
- Health endpoint —
GET /healthfor Uptime Kuma / monitoring. - 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 →



