Skip to main content

Node.js + PM2 on a VPS: Deploying Applications

VDS / VPS · 10.05.2025 · 4 min read
Illustration for “Node.js + PM2 on a VPS: Deploying Applications”

Node.js lets you run JavaScript on the server. PM2 manages processes: it automatically restarts the application on crashes and at system startup.

Installing Node.js via NodeSource

# Install Node.js 20 LTS
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

# Check the versions
node --version   # v20.x.x
npm --version    # 10.x.x

Installing PM2

sudo npm install -g pm2

Starting the Application

# Clone the project
git clone https://github.com/your/app.git /var/www/myapp
cd /var/www/myapp
npm install
npm run build   # for Next.js

# Start with PM2
pm2 start npm --name "myapp" -- start

# For Express
pm2 start server.js --name "myapp"

PM2 Autostart on Reboot

# Save the current processes
pm2 save

# Configure autostart
pm2 startup systemd
# Copy and run the command that PM2 outputs

Useful PM2 Commands

pm2 list              # list of all processes
pm2 logs myapp        # application logs
pm2 restart myapp     # restart
pm2 stop myapp        # stop
pm2 monit             # real-time CPU/RAM monitoring

Nginx as a Reverse Proxy

sudo nano /etc/nginx/sites-available/myapp
server {
    listen 80;
    server_name example.com;

    location / {
        proxy_pass http://localhost:3000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
    }
}
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
💡 Use pm2 monit to track RAM usage in real time. For Node.js applications, we recommend a VPS with 1–2 GB RAM.

Installing Node.js and PM2

# Node.js 20 LTS via NodeSource
curl -fsSL https://deb.nodesource.com/setup_20.x | bash -
apt install nodejs -y

# PM2 globally
npm install -g pm2

# Check
node --version && pm2 --version

ecosystem.config.js

module.exports = {
  apps: [{
    name: 'myapp',
    script: './server.js',
    instances: 'max',
    exec_mode: 'cluster',
    watch: false,
    max_memory_restart: '500M',
    env: {
      NODE_ENV: 'production',
      PORT: 3000
    }
  }]
}

Main PM2 commands

CommandDescription
pm2 listList of processes
pm2 logs myappApplication logs
pm2 monitReal-time monitoring
pm2 reload myappZero-downtime reload
pm2 saveSave the process list
pm2 startupAuto-start on boot

Auto-start on reboot

pm2 save
pm2 startup
# Run the command that PM2 outputs
💡 In cluster mode, PM2 automatically balances the load between workers and restarts crashed instances.

Common Node.js and PM2 Errors

MessageWhat it means
Error: listen EADDRINUSE: address already in use :::3000The port is already taken by a process PM2 isn't tracking — the app was started manually via node instead of pm2 start
PM2 process died with exit code 1 (restart loop)The app crashes right after starting, most often due to missing environment variables or a Node.js version mismatch
npm ERR! code EACCES during a global installnpm install -g was run without permission or without a properly configured prefix for the current user
The app didn't come back after a server rebootpm2 save wasn't run after the last change to the process list — autostart restores the old snapshot

PM2 vs systemd: What to Use

CriterionPM2systemd
Zero-downtime reloadYes, pm2 reloadNo, only restart with dropped connections
Cluster mode for Node.jsBuilt-in exec_mode clusterRequires manually running several unit files
Log rotationVia pm2-logrotateVia journald out of the box
Simplicity for non-Node processesNot designed for thatUniversal for any service

When PM2 Isn't a Fit

  • The project is already managed by Docker and an orchestrator (Kubernetes, Swarm) — restarting and scaling are the orchestrator's job
  • The server runs a systemd-first distro, and the team wants one way to manage every service through systemctl
  • The application isn't Node.js — PM2 can run arbitrary binaries, but that's not its main use case
  • You need integration with cgroups and OS-level resource limits — systemd or containers fit that better

Verifying the Result

# Detailed process status: memory, uptime, restart count
pm2 describe myapp

# The app actually responds over HTTP
curl -sI http://localhost:3000/ | head -1

If pm2 describe shows a high restart count in a short time, that's the restart loop — look for the cause in pm2 logs myapp --lines 50. Related: basic Nginx setup and Docker Compose on a VDS as an alternative to PM2.

Was this article helpful?
← Back to Knowledge Base Ask Support