Running a Node.js app in production means more than node app.js in a screen session. PM2 is the process manager that turns Node into a real production runtime — it restarts crashed processes, runs them at boot, manages logs, balances multiple workers across cores, and gives you zero-downtime deploys. Pair it with Nginx as a reverse proxy and you have a stack that handles thousands of requests per second on a small VPS. This guide walks through the whole thing.

📋

Prerequisites: Ubuntu 22.04 or 24.04 VPS, sudo user, an Nginx setup if you want a reverse proxy in front (covered in our Nginx + Let's Encrypt guide). 1GB RAM works for small apps; 2GB+ recommended for anything serious.

Steps in this guide

  1. Install Node.js via NodeSource
  2. Install PM2 globally
  3. Run your first app under PM2
  4. Make PM2 start on boot
  5. Cluster mode for multi-core CPUs
  6. Log rotation
  7. Nginx as reverse proxy
  8. Zero-downtime deploys
  9. FAQ

Step 1: Install Node.js via NodeSource

Don't use Ubuntu's default Node packages — they're typically 2-3 major versions behind. Use NodeSource for current LTS:

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

This installs Node 20 LTS (Iron). Replace 20.x with 22.x for the current LTS as of late 2025+. Verify:

node --version
npm --version

If you need multiple Node versions on the same box (rare for production, common for dev), use nvm instead.

Step 2: Install PM2 globally

sudo npm install -g pm2

Verify:

pm2 --version

Step 3: Run your first app under PM2

Assuming you have a Node app at /home/yourname/myapp/server.js:

cd /home/yourname/myapp
npm install --production
pm2 start server.js --name myapp

The --name flag gives the process a friendly name. Check status:

pm2 list

You'll see your app with status online, memory and CPU usage, and uptime. Useful PM2 commands:

pm2 logs myapp           # tail logs
pm2 logs myapp --lines 100  # last 100 log lines
pm2 restart myapp        # restart
pm2 stop myapp           # stop (but keep config)
pm2 delete myapp         # remove
pm2 monit                # live dashboard
pm2 info myapp           # detailed info

Step 4: Make PM2 start on boot

If your VPS reboots, you want PM2 (and your apps) to start back up automatically.

pm2 startup

This prints a command — copy and run it (it'll be something like sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u yourname --hp /home/yourname). It installs a systemd service that brings PM2 up on boot.

Save your current process list as the one to restore on boot:

pm2 save

Test it: sudo reboot, wait, SSH back in, run pm2 list. Your apps should be running.

Step 5: Cluster mode for multi-core CPUs

Node.js is single-threaded. On a multi-core VPS, a single Node process uses one core. PM2's cluster mode runs multiple instances behind a load balancer, using all your cores.

pm2 delete myapp
pm2 start server.js --name myapp -i max

The -i max flag spawns one instance per CPU core. PM2 distributes incoming connections across them automatically. Use a number instead of max if you want a specific count.

For this to work, your app needs to be stateless (or use a shared store like Redis for session state). Two cluster instances can't share in-process memory.

Step 6: Log rotation

PM2 logs every stdout/stderr line your app produces. On a busy app, log files balloon fast. Install the log rotation module:

pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true

That keeps 7 compressed daily logs of up to 10MB each — plenty for debugging without filling your disk.

Step 7: Nginx as reverse proxy

Don't expose Node directly to the internet. Put Nginx in front for SSL termination, gzip, static-file serving, and a layer of protection. Assuming you've already installed Nginx with Let's Encrypt, the relevant server block:

server {
    listen 443 ssl http2;
    server_name app.yourdomain.com;

    ssl_certificate /etc/letsencrypt/live/app.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/app.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_http_version 1.1;
        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_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 60s;
    }
}

server {
    listen 80;
    server_name app.yourdomain.com;
    return 301 https://$host$request_uri;
}

Your Node app listens on 127.0.0.1:3000 (or whatever port). Nginx terminates SSL and forwards traffic. The X-Forwarded-* headers tell your app the real client info. Your Node app needs to app.set('trust proxy', 1) in Express (or equivalent) to honor those headers.

Production Node.js needs real hardware

Dedicated CPU cores so cluster mode actually scales, NVMe storage so DB queries don't drag, KVM virtualization that doesn't break npm. Starting at $3.99/mo.

See VPS Plans →

Zero-downtime deploys

The naive deploy: push new code, pm2 restart myapp, brief downtime while the process restarts. PM2's reload command is smarter — it brings up new instances before killing old ones, so requests in flight don't drop.

pm2 reload myapp

This works perfectly in cluster mode. PM2 starts a new instance, waits for it to be ready, terminates one old instance, starts the next new one, and so on. Production-grade rolling restart.

Ecosystem files for cleaner config

For real apps, use a PM2 ecosystem file instead of CLI flags:

pm2 ecosystem

Edit the generated ecosystem.config.js:

module.exports = {
  apps: [{
    name: 'myapp',
    script: 'server.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
    },
    max_memory_restart: '500M',
    error_file: './logs/error.log',
    out_file: './logs/out.log',
    time: true,
  }],
};

Then deploy:

pm2 reload ecosystem.config.js --env production
pm2 save

Note max_memory_restart: '500M' — PM2 restarts the process if it exceeds 500MB of RAM. Catches memory leaks before they crash the server.

FAQ

Should I use PM2 or Docker for Node.js apps?

Both work. PM2 is simpler for single-server setups — install Node, install PM2, run your app. Docker is better when you want immutable deploys, multi-service stacks, or eventual orchestration (Kubernetes, Docker Swarm). For a small startup or solo project on a single VPS, PM2 is less overhead. For anything multi-service or scaling out, Docker pays off.

What about systemd instead of PM2?

systemd works fine for single-process Node apps. It's simpler than PM2 and uses fewer resources. PM2 wins when you need cluster mode, log management, ecosystem files, and zero-downtime reload. For "just keep this thing running and restart on boot," systemd is enough.

How much RAM does PM2 itself use?

PM2's daemon (the "God" process) uses about 30-60MB. Each app instance uses whatever Node + your app uses (typically 50-200MB depending on the app). On a 1GB VPS you can run 3-5 small Node apps comfortably with PM2.

Do I need PM2 Plus / PM2 Enterprise?

Almost certainly not. The free PM2 covers everything most production apps need. PM2 Plus adds remote monitoring dashboards and metrics aggregation across multiple servers — useful for fleets of 5+ servers, overkill for a single VPS.

Why is my Node app crashing under load?

Most common: file descriptor limits. Node defaults to 1024 open files; busy apps blow through that. See our Linux performance tuning guide for raising limits. Second most common: synchronous code blocking the event loop. Profile with --inspect and Chrome DevTools.

🐱
The OliveVPS Team

We've debugged enough Node memory leaks at 3am to recognize them on sight.