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
PM2Node.jsLinuxSystemdProcess ManagementDevOpsProductionDeployment

TL;DR

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

Written by

Tushar Agrawal

Full-Stack Engineer in New Delhi building healthcare SaaS at Dr. Dangs Lab. 3+ years shipping Python/Go microservices, event-driven systems, and HIPAA-compliant platforms at 99.9% uptime. Creator of QAuth and QuantumShield.

Related Articles