Back to Blog
PM2 & Linux Services: Production Node.js Deployment Guide

PM2 & Linux Services: Production Node.js Deployment Guide

December 18, 2024
15 min read
Tushar Agrawal

Complete guide to deploying Node.js applications in production using PM2 process manager and Linux systemd services. Learn cluster mode, zero-downtime deployments, and monitoring.

Introduction

Running Node.js applications in production requires robust process management to handle crashes, enable zero-downtime deployments, and utilize multiple CPU cores effectively. Two primary approaches dominate the Linux ecosystem: PM2 (a Node.js-specific process manager) and systemd (the Linux system and service manager).

In this guide, we'll explore:

  • PM2 fundamentals and advanced features
  • Linux systemd service creation and management
  • When to use each approach
  • Production-ready configurations
  • Monitoring and logging strategies

PM2 Fundamentals

Installation and Basic Usage

# Install PM2 globally
npm install -g pm2

# Or with yarn
yarn global add pm2

# Verify installation
pm2 --version

Starting Applications

# Basic start
pm2 start app.js

# With a custom name
pm2 start app.js --name "my-api"

# Start with arguments
pm2 start app.js -- --port 3000

# Start a script from package.json
pm2 start npm --name "my-app" -- start

# Start with environment variables
pm2 start app.js --env production

Process Management Commands

# List all processes
pm2 list
pm2 ls
pm2 status

# Detailed process info
pm2 show <app-name>
pm2 describe <app-name>

# Restart processes
pm2 restart <app-name>
pm2 restart all

# Stop processes
pm2 stop <app-name>
pm2 stop all

# Delete processes from PM2
pm2 delete <app-name>
pm2 delete all

# Reload with zero downtime (graceful reload)
pm2 reload <app-name>
pm2 reload all

PM2 Cluster Mode

PM2's cluster mode allows you to run multiple instances of your Node.js application, utilizing all available CPU cores:

PM2 Cluster Mode Architecture
=============================

         ┌─────────────────────────────────────┐
         │            PM2 Master               │
         │     (Load Balancer + Monitor)       │
         └──────────────┬──────────────────────┘
                        │
        ┌───────────────┼───────────────┐
        │               │               │
        ▼               ▼               ▼
   ┌─────────┐    ┌─────────┐    ┌─────────┐
   │ Worker  │    │ Worker  │    │ Worker  │
   │   0     │    │   1     │    │   2     │
   │ (CPU 0) │    │ (CPU 1) │    │ (CPU 2) │
   └─────────┘    └─────────┘    └─────────┘
        │               │               │
        └───────────────┼───────────────┘
                        │
                        ▼
              Incoming Requests
              (Round-robin LB)

Starting in Cluster Mode

# Start with all available CPUs
pm2 start app.js -i max

# Start with specific number of instances
pm2 start app.js -i 4

# Start with CPUs minus 1 (leave one for OS)
pm2 start app.js -i -1

Cluster Mode Application Example

// app.js - Express application optimized for cluster mode
const express = require('express');
const process = require('process');

const app = express();
const PORT = process.env.PORT || 3000;

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({
    status: 'healthy',
    pid: process.pid,
    uptime: process.uptime(),
    memory: process.memoryUsage(),
  });
});

// Your application routes
app.get('/api/data', (req, res) => {
  res.json({
    message: 'Hello from worker ' + process.pid,
    timestamp: new Date().toISOString(),
  });
});

// Graceful shutdown handling
process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown);

function gracefulShutdown() {
  console.log(`Worker ${process.pid} shutting down gracefully...`);

  // Stop accepting new connections
  server.close(() => {
    console.log(`Worker ${process.pid} closed all connections`);
    process.exit(0);
  });

  // Force exit after 10 seconds
  setTimeout(() => {
    console.error(`Worker ${process.pid} forcing shutdown`);
    process.exit(1);
  }, 10000);
}

const server = app.listen(PORT, () => {
  console.log(`Worker ${process.pid} listening on port ${PORT}`);
});

PM2 Ecosystem File

The ecosystem file (ecosystem.config.js) is the recommended way to configure PM2 applications:

// ecosystem.config.js
module.exports = {
  apps: [
    {
      name: 'api-server',
      script: './src/server.js',
      instances: 'max',  // Cluster mode with all CPUs
      exec_mode: 'cluster',

      // Environment variables
      env: {
        NODE_ENV: 'development',
        PORT: 3000,
      },
      env_production: {
        NODE_ENV: 'production',
        PORT: 8080,
      },
      env_staging: {
        NODE_ENV: 'staging',
        PORT: 8081,
      },

      // Logging
      log_file: '/var/log/pm2/api-combined.log',
      out_file: '/var/log/pm2/api-out.log',
      error_file: '/var/log/pm2/api-error.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      merge_logs: true,

      // Auto-restart settings
      watch: false,  // Don't watch in production
      max_memory_restart: '1G',  // Restart if memory exceeds 1GB
      restart_delay: 4000,  // Wait 4s between restarts

      // Crash recovery
      autorestart: true,
      max_restarts: 10,
      min_uptime: '10s',

      // Graceful shutdown
      kill_timeout: 5000,  // 5s to gracefully shutdown
      listen_timeout: 3000,  // 3s to start listening

      // Instance variables
      instance_var: 'INSTANCE_ID',
    },
    {
      name: 'worker',
      script: './src/worker.js',
      instances: 2,
      exec_mode: 'cluster',

      env_production: {
        NODE_ENV: 'production',
        QUEUE_URL: 'redis://localhost:6379',
      },

      cron_restart: '0 0 * * *',  // Restart daily at midnight
    },
    {
      name: 'scheduler',
      script: './src/scheduler.js',
      instances: 1,  // Single instance for cron jobs
      exec_mode: 'fork',

      env_production: {
        NODE_ENV: 'production',
      },
    },
  ],

  // Deployment configuration
  deploy: {
    production: {
      user: 'deploy',
      host: ['server1.example.com', 'server2.example.com'],
      ref: 'origin/main',
      repo: 'git@github.com:username/repo.git',
      path: '/var/www/production',
      'pre-deploy-local': '',
      'post-deploy': 'npm ci && pm2 reload ecosystem.config.js --env production',
      'pre-setup': '',
    },
    staging: {
      user: 'deploy',
      host: 'staging.example.com',
      ref: 'origin/develop',
      repo: 'git@github.com:username/repo.git',
      path: '/var/www/staging',
      'post-deploy': 'npm ci && pm2 reload ecosystem.config.js --env staging',
    },
  },
};

Using the Ecosystem File

# Start all apps
pm2 start ecosystem.config.js

# Start specific app
pm2 start ecosystem.config.js --only api-server

# Start with specific environment
pm2 start ecosystem.config.js --env production

# Reload all apps (zero downtime)
pm2 reload ecosystem.config.js

# Deploy to production
pm2 deploy production setup    # First time setup
pm2 deploy production          # Deploy
pm2 deploy production revert 1 # Rollback 1 commit

Linux systemd Services

systemd is the default init system on most modern Linux distributions and provides a powerful way to manage services.

systemd Service File Structure

# /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js Application
Documentation=https://example.com/docs
After=network.target
Wants=network-online.target

[Service]
Type=simple
User=nodeapp
Group=nodeapp
WorkingDirectory=/var/www/myapp

# Environment
Environment=NODE_ENV=production
Environment=PORT=3000
EnvironmentFile=/var/www/myapp/.env

# Process management
ExecStart=/usr/bin/node /var/www/myapp/dist/server.js
ExecReload=/bin/kill -HUP $MAINPID
ExecStop=/bin/kill -TERM $MAINPID

# Restart policy
Restart=always
RestartSec=10
StartLimitInterval=60
StartLimitBurst=3

# Security hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/www/myapp/logs /var/www/myapp/uploads

# Resource limits
MemoryMax=1G
CPUQuota=80%

# Logging
StandardOutput=append:/var/log/myapp/stdout.log
StandardError=append:/var/log/myapp/stderr.log
SyslogIdentifier=myapp

[Install]
WantedBy=multi-user.target

Managing systemd Services

# Reload systemd after changing service files
sudo systemctl daemon-reload

# Enable service to start on boot
sudo systemctl enable myapp.service

# Start service
sudo systemctl start myapp.service

# Stop service
sudo systemctl stop myapp.service

# Restart service
sudo systemctl restart myapp.service

# Check status
sudo systemctl status myapp.service

# View logs
sudo journalctl -u myapp.service
sudo journalctl -u myapp.service -f  # Follow logs
sudo journalctl -u myapp.service --since "1 hour ago"

Running PM2 Under systemd

The best practice is to run PM2 itself under systemd:

# Generate systemd startup script
pm2 startup systemd

# This outputs a command like:
# sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u deploy --hp /home/deploy

# Save current PM2 process list
pm2 save

# The saved list will auto-restore on system boot

Generated systemd file for PM2:

# /etc/systemd/system/pm2-deploy.service
[Unit]
Description=PM2 process manager
Documentation=https://pm2.keymetrics.io/
After=network.target

[Service]
Type=forking
User=deploy
LimitNOFILE=infinity
LimitNPROC=infinity
LimitCORE=infinity
Environment=PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/home/deploy/.nvm/versions/node/v20.10.0/bin
Environment=PM2_HOME=/home/deploy/.pm2
PIDFile=/home/deploy/.pm2/pm2.pid
Restart=on-failure

ExecStart=/home/deploy/.nvm/versions/node/v20.10.0/lib/node_modules/pm2/bin/pm2 resurrect
ExecReload=/home/deploy/.nvm/versions/node/v20.10.0/lib/node_modules/pm2/bin/pm2 reload all
ExecStop=/home/deploy/.nvm/versions/node/v20.10.0/lib/node_modules/pm2/bin/pm2 kill

[Install]
WantedBy=multi-user.target

PM2 vs systemd: When to Use Which

Feature Comparison
==================

Feature                    PM2              systemd
──────────────────────────────────────────────────────────────
Cluster mode               Built-in         Manual (multiple units)
Zero-downtime reload       Built-in         Rolling restart
Log management             Built-in         journald
Process monitoring         Built-in + UI    journalctl + custom
Watch & auto-reload        Yes              inotify + script
Memory threshold restart   Yes              MemoryMax (OOM only)
Cron restarts              Yes              systemd timers
Deployment integration     Yes              No
Boot persistence           Needs setup      Native
Non-Node.js apps           Limited          Full support
Security hardening         Basic            Comprehensive
Resource limits            Basic            Comprehensive (cgroups)
Dependencies               Node.js          None
Learning curve             Easy             Moderate

Use PM2 When:

1. Running Node.js applications exclusively 2. Need cluster mode for multi-core utilization 3. Want built-in zero-downtime reloads 4. Prefer simple deployment workflows 5. Need log rotation and management 6. Want a web dashboard (PM2 Plus)

Use systemd When:

1. Running multiple types of services (not just Node.js) 2. Need comprehensive security hardening 3. Want tight resource controls (cgroups, memory limits) 4. Managing system-level services 5. Need complex service dependencies 6. Prefer native Linux tooling

Use Both (PM2 under systemd):

1. Best of both worlds 2. PM2 handles Node.js specifics (clustering, reloads) 3. systemd handles boot persistence and supervision 4. Most production-ready approach

Zero-Downtime Deployments

PM2 Zero-Downtime Reload

# Graceful reload - waits for new workers before killing old ones
pm2 reload ecosystem.config.js

# Reload specific app
pm2 reload api-server

How it works:

Zero-Downtime Reload Process
============================

Time  │  Old Workers    │  New Workers    │  Traffic
──────┼─────────────────┼─────────────────┼──────────────
T0    │  [1] [2] [3]    │                 │  → [1,2,3]
T1    │  [1] [2] [3]    │  [4] starting   │  → [1,2,3]
T2    │  [1] [2] [3]    │  [4] ready      │  → [1,2,3,4]
T3    │  [1] stopping   │  [4]            │  → [2,3,4]
T4    │  [2] [3]        │  [4] [5] start  │  → [2,3,4]
T5    │  [2] stopping   │  [4] [5]        │  → [3,4,5]
...   │                 │                 │
Tn    │                 │  [4] [5] [6]    │  → [4,5,6]

Graceful Shutdown Implementation

// server.js with graceful shutdown
const express = require('express');
const app = express();

// Track active connections
let connections = new Set();
let isShuttingDown = false;

const server = app.listen(3000, () => {
  console.log('Server started on port 3000');

  // Notify PM2 that we're ready
  if (process.send) {
    process.send('ready');
  }
});

// Track connections
server.on('connection', (conn) => {
  connections.add(conn);
  conn.on('close', () => connections.delete(conn));
});

// Graceful shutdown handler
async function gracefulShutdown(signal) {
  console.log(`Received ${signal}, starting graceful shutdown...`);
  isShuttingDown = true;

  // Stop accepting new connections
  server.close(async () => {
    console.log('HTTP server closed');

    // Close database connections
    await closeDatabase();

    // Close message queue connections
    await closeMessageQueue();

    console.log('All connections closed, exiting');
    process.exit(0);
  });

  // Close existing connections gracefully
  for (const conn of connections) {
    conn.end();
  }

  // Force close after timeout
  setTimeout(() => {
    console.error('Forcing shutdown after timeout');
    for (const conn of connections) {
      conn.destroy();
    }
    process.exit(1);
  }, 10000);
}

// Listen for shutdown signals
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));

// Health check that respects shutdown state
app.get('/health', (req, res) => {
  if (isShuttingDown) {
    res.status(503).json({ status: 'shutting_down' });
  } else {
    res.json({ status: 'healthy' });
  }
});

PM2 Configuration for Graceful Shutdown

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'api',
    script: './server.js',
    instances: 'max',
    exec_mode: 'cluster',

    // Wait for 'ready' signal before considering app online
    wait_ready: true,
    listen_timeout: 10000,

    // Give app time to close connections
    kill_timeout: 10000,

    // Shutdown signal
    shutdown_with_message: true,
  }],
};

Log Management

PM2 Log Commands

# View logs
pm2 logs
pm2 logs api-server
pm2 logs api-server --lines 100
pm2 logs --json

# Flush logs
pm2 flush
pm2 flush api-server

# Rotate logs
pm2 install pm2-logrotate

# Configure logrotate
pm2 set pm2-logrotate:max_size 10M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD_HH-mm-ss
pm2 set pm2-logrotate:rotateModule true
pm2 set pm2-logrotate:workerInterval 30
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'

systemd Journal Logging

# View all logs for a service
journalctl -u myapp.service

# Follow logs in real-time
journalctl -u myapp.service -f

# Logs since last boot
journalctl -u myapp.service -b

# Logs from specific time
journalctl -u myapp.service --since "2024-01-01 00:00:00"
journalctl -u myapp.service --since "1 hour ago"

# JSON output for parsing
journalctl -u myapp.service -o json

# Limit output
journalctl -u myapp.service -n 100

# Filter by priority
journalctl -u myapp.service -p err  # Only errors

Structured Logging for Production

// logger.js - Winston with PM2-friendly configuration
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: {
    service: process.env.SERVICE_NAME || 'api',
    pid: process.pid,
    instanceId: process.env.INSTANCE_ID || 0,
  },
  transports: [
    // Console transport (PM2 captures this)
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      ),
    }),
    // File transport for persistent logs
    new winston.transports.File({
      filename: '/var/log/app/error.log',
      level: 'error',
      maxsize: 10485760, // 10MB
      maxFiles: 5,
    }),
    new winston.transports.File({
      filename: '/var/log/app/combined.log',
      maxsize: 10485760,
      maxFiles: 10,
    }),
  ],
});

module.exports = logger;

Monitoring and Metrics

PM2 Built-in Monitoring

# Real-time monitoring dashboard
pm2 monit

# Process information
pm2 show api-server

# Metrics
pm2 env api-server

# Memory/CPU snapshot
pm2 prettylist

PM2 Plus (Paid Service)

# Link to PM2 Plus for advanced monitoring
pm2 plus

# Or link with specific credentials
pm2 link <secret_key> <public_key>

Custom Metrics with PM2

// Add custom metrics
const io = require('@pm2/io');

// Counter metric
const requestCounter = io.counter({
  name: 'Requests',
  id: 'app/requests',
});

// Meter metric (requests per second)
const requestMeter = io.meter({
  name: 'req/sec',
  id: 'app/requests/sec',
});

// Histogram metric
const responseTime = io.histogram({
  name: 'Response Time',
  id: 'app/response-time',
  measurement: 'mean',
});

// Gauge metric
const activeConnections = io.metric({
  name: 'Active Connections',
  id: 'app/connections',
});

// Express middleware to track metrics
app.use((req, res, next) => {
  const start = Date.now();
  requestCounter.inc();
  requestMeter.mark();

  res.on('finish', () => {
    const duration = Date.now() - start;
    responseTime.update(duration);
  });

  next();
});

// Update active connections gauge
server.on('connection', () => {
  activeConnections.set(server.connections);
});

Prometheus Metrics Export

// metrics.js - Prometheus metrics for Node.js
const client = require('prom-client');
const express = require('express');

// Create a Registry
const register = new client.Registry();

// Add default metrics (CPU, memory, etc.)
client.collectDefaultMetrics({ register });

// Custom metrics
const httpRequestDuration = new client.Histogram({
  name: 'http_request_duration_seconds',
  help: 'Duration of HTTP requests in seconds',
  labelNames: ['method', 'route', 'status_code'],
  buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
});
register.registerMetric(httpRequestDuration);

const httpRequestTotal = new client.Counter({
  name: 'http_requests_total',
  help: 'Total number of HTTP requests',
  labelNames: ['method', 'route', 'status_code'],
});
register.registerMetric(httpRequestTotal);

// Middleware
function metricsMiddleware(req, res, next) {
  const end = httpRequestDuration.startTimer();

  res.on('finish', () => {
    const route = req.route?.path || req.path;
    const labels = {
      method: req.method,
      route: route,
      status_code: res.statusCode,
    };
    end(labels);
    httpRequestTotal.inc(labels);
  });

  next();
}

// Metrics endpoint
function metricsHandler(req, res) {
  res.set('Content-Type', register.contentType);
  register.metrics().then(data => res.send(data));
}

module.exports = { metricsMiddleware, metricsHandler, register };

Running Multiple Applications

PM2 Multiple Apps

// ecosystem.config.js with multiple apps
module.exports = {
  apps: [
    {
      name: 'api',
      script: './services/api/server.js',
      instances: 4,
      exec_mode: 'cluster',
      env_production: {
        PORT: 3000,
        NODE_ENV: 'production',
      },
    },
    {
      name: 'admin',
      script: './services/admin/server.js',
      instances: 2,
      exec_mode: 'cluster',
      env_production: {
        PORT: 3001,
        NODE_ENV: 'production',
      },
    },
    {
      name: 'websocket',
      script: './services/websocket/server.js',
      instances: 2,
      exec_mode: 'cluster',
      env_production: {
        PORT: 3002,
        NODE_ENV: 'production',
      },
    },
    {
      name: 'worker-email',
      script: './services/workers/email.js',
      instances: 1,
      exec_mode: 'fork',
      cron_restart: '0 */6 * * *',
    },
    {
      name: 'worker-reports',
      script: './services/workers/reports.js',
      instances: 1,
      exec_mode: 'fork',
    },
  ],
};

systemd Multiple Services

# /etc/systemd/system/myapp-api.service
[Unit]
Description=MyApp API Server
After=network.target postgresql.service redis.service
Requires=postgresql.service

[Service]
Type=simple
User=nodeapp
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node services/api/server.js
Restart=always

[Install]
WantedBy=multi-user.target

# /etc/systemd/system/myapp-worker.service
[Unit]
Description=MyApp Background Worker
After=network.target myapp-api.service redis.service
Requires=redis.service

[Service]
Type=simple
User=nodeapp
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/node services/workers/main.js
Restart=always

[Install]
WantedBy=multi-user.target

Creating a target for all services:

# /etc/systemd/system/myapp.target
[Unit]
Description=MyApp All Services
Requires=myapp-api.service myapp-worker.service
After=myapp-api.service myapp-worker.service

[Install]
WantedBy=multi-user.target

# Start all myapp services
sudo systemctl start myapp.target

# Enable all to start on boot
sudo systemctl enable myapp.target

Security Best Practices

PM2 Security

// ecosystem.config.js with security settings
module.exports = {
  apps: [{
    name: 'api',
    script: './server.js',

    // Run as non-root user
    uid: 'nodeapp',
    gid: 'nodeapp',

    // Limit resources
    max_memory_restart: '500M',

    // Don't expose PM2 info
    pmx: false,
  }],
};

systemd Security Hardening

[Service]
# Run as unprivileged user
User=nodeapp
Group=nodeapp

# Filesystem restrictions
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectSystem=strict
ProtectHome=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ReadWritePaths=/var/www/myapp/logs /var/www/myapp/uploads

# Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
PrivateNetwork=false

# System call filtering
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources

# Capabilities
CapabilityBoundingSet=
AmbientCapabilities=

# Memory restrictions
MemoryDenyWriteExecute=true

# Misc security
LockPersonality=true
RestrictRealtime=true
RestrictSUIDSGID=true

Complete Production Setup

Here's a complete setup combining PM2 with systemd:

#!/bin/bash
# setup-production.sh

set -e

APP_NAME="myapp"
APP_USER="nodeapp"
APP_DIR="/var/www/$APP_NAME"
NODE_VERSION="20"

echo "=== Setting up $APP_NAME production environment ==="

# 1. Create application user
sudo useradd -r -s /bin/false $APP_USER || true

# 2. Create directories
sudo mkdir -p $APP_DIR
sudo mkdir -p /var/log/$APP_NAME
sudo chown -R $APP_USER:$APP_USER $APP_DIR
sudo chown -R $APP_USER:$APP_USER /var/log/$APP_NAME

# 3. Install Node.js via nvm (as app user)
sudo -u $APP_USER bash << 'EOF'
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash
source ~/.nvm/nvm.sh
nvm install 20
nvm use 20
npm install -g pm2
EOF

# 4. Setup PM2 systemd service
sudo -u $APP_USER bash -c 'source ~/.nvm/nvm.sh && pm2 startup systemd -u nodeapp --hp /home/nodeapp'

# 5. Enable and start PM2 service
sudo systemctl enable pm2-$APP_USER
sudo systemctl start pm2-$APP_USER

echo "=== Production setup complete ==="
echo "Deploy your app to $APP_DIR and run:"
echo "sudo -u $APP_USER bash -c 'cd $APP_DIR && pm2 start ecosystem.config.js --env production'"
echo "sudo -u $APP_USER bash -c 'pm2 save'"

Conclusion

Both PM2 and systemd are excellent tools for running Node.js applications in production. The key takeaways:

1. PM2 excels at Node.js-specific features: clustering, zero-downtime reloads, and built-in monitoring 2. systemd provides robust system-level service management with comprehensive security hardening 3. The combination of both (PM2 under systemd) is the most production-ready approach 4. Always implement graceful shutdown for zero-downtime deployments 5. Use structured logging and metrics for observability

At Dr Dangs Lab, we run our Node.js microservices using PM2 in cluster mode, supervised by systemd, with Nginx as a reverse proxy. This stack has provided us with 99.9% uptime and seamless deployments.

Related Articles

Share this article

Related Articles