PM2 Process Management for Node.js in Production

PM2 process management for Node.js production: ecosystem file configuration, zero-downtime restarts, log management, monitoring, clustering, and deployment workflows.

E
ECOSIRE Research and Development Team
|2026年3月19日6 分钟阅读1.3k 字数|

生产中 Node.js 的 PM2 流程管理

当您的 Node.js 应用程序在凌晨 2 点崩溃时,PM2 就是它自动重新启动和您的用户在您醒来之前看到空白页面之间的区别。 PM2 是一款经过实战考验的流程管理器,可处理自动重启、多核利用率集群、日志聚合和零停机部署 - 所有这些都通过存储库中的单个配置文件进行。

本指南涵盖了同时管理 5 个 Node.js 进程的生产 PM2 设置:Next.js(前端)、NestJS(API)、Docusaurus(文档)和两个品牌站点。这些模式同样适用于单进程部署。

要点

  • ecosystem.config.cjs 文件(CommonJS,而不是 .js)适用于 ESModule 和 CommonJS 项目
  • 重新启动时需要 --update-env 标志以获取新的环境变量
  • 更新 .env.local 后,切勿在没有 --update-env 的情况下使用 pm2 restart all
  • 生产中的 watch: false — 文件监视导致构建输出无限重启循环
  • max_memory_restart 提供自动内存泄漏保护,无需永久终止进程
  • node_args: '--max-old-space-size=4096' 防止内存密集型操作发生 OOM 崩溃
  • PM2 日志与 pm2-logrotate 模块一起旋转 — 在 PM2 本身之后立即安装它
  • pm2 savepm2 startup 在服务器重新启动后保留您的进程列表

安装

# Install PM2 globally
npm install -g pm2

# Install the log rotation module immediately
pm2 install pm2-logrotate

# Configure log rotation
pm2 set pm2-logrotate:max_size 50M
pm2 set pm2-logrotate:retain 7
pm2 set pm2-logrotate:compress true
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD

生态系统配置文件

ecosystem.config.cjs 文件(CommonJS 格式可用于 ESM 和 CJS 项目)定义您的所有流程:

// ecosystem.config.cjs
module.exports = {
  apps: [
    // ─── Next.js Frontend ────────────────────────────────────────────
    {
      name: 'ecosire-web',
      script: 'node_modules/.bin/next',
      args: 'start',
      cwd: '/opt/ecosire/app/apps/web',
      instances: 1,        // Single instance — Next.js handles its own multi-threading
      exec_mode: 'fork',
      env: {
        NODE_ENV: 'production',
        PORT: 3000,
      },
      // Memory management
      max_memory_restart: '1G',
      node_args: '--max-old-space-size=1024',
      // Logging
      out_file: '/var/log/pm2/ecosire-web.out.log',
      error_file: '/var/log/pm2/ecosire-web.err.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      // Restart behavior
      watch: false,
      restart_delay: 3000,
      max_restarts: 10,
      min_uptime: '30s',   // Must stay up 30s to count as successful start
      autorestart: true,
      // Graceful shutdown
      kill_timeout: 30000, // 30 seconds to shut down gracefully
      wait_ready: true,    // Wait for process.send('ready')
      listen_timeout: 60000,
    },

    // ─── NestJS API ──────────────────────────────────────────────────
    {
      name: 'ecosire-api',
      script: 'dist/main.js',
      cwd: '/opt/ecosire/app/apps/api',
      instances: 2,         // Cluster mode for multi-core utilization
      exec_mode: 'cluster',
      env: {
        NODE_ENV: 'production',
        PORT: 3001,
      },
      max_memory_restart: '512M',
      node_args: '--max-old-space-size=512',
      out_file: '/var/log/pm2/ecosire-api.out.log',
      error_file: '/var/log/pm2/ecosire-api.err.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      watch: false,
      restart_delay: 2000,
      max_restarts: 10,
      min_uptime: '20s',
      autorestart: true,
      kill_timeout: 15000,
      // Graceful cluster reload support
      listen_timeout: 30000,
    },

    // ─── Docusaurus Docs ─────────────────────────────────────────────
    {
      name: 'ecosire-docs',
      script: 'node_modules/.bin/docusaurus',
      args: 'serve',
      cwd: '/opt/ecosire/app/apps/docs',
      instances: 1,
      exec_mode: 'fork',
      env: {
        NODE_ENV: 'production',
        PORT: 3002,
      },
      max_memory_restart: '256M',
      out_file: '/var/log/pm2/ecosire-docs.out.log',
      error_file: '/var/log/pm2/ecosire-docs.err.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      watch: false,
      restart_delay: 3000,
      max_restarts: 5,
      min_uptime: '30s',
      autorestart: true,
      kill_timeout: 10000,
    },

    // ─── Brand Site: Odovation ───────────────────────────────────────
    {
      name: 'odovation-web',
      script: 'node_modules/.bin/next',
      args: 'start',
      cwd: '/opt/ecosire/app/apps/odovation',
      instances: 1,
      exec_mode: 'fork',
      env: {
        NODE_ENV: 'production',
        PORT: 3010,
      },
      max_memory_restart: '512M',
      out_file: '/var/log/pm2/odovation-web.out.log',
      error_file: '/var/log/pm2/odovation-web.err.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      watch: false,
      restart_delay: 3000,
      max_restarts: 10,
      min_uptime: '30s',
      autorestart: true,
    },

    // ─── Brand Site: MuhammadAmir ────────────────────────────────────
    {
      name: 'muhammadamir-web',
      script: 'node_modules/.bin/next',
      args: 'start',
      cwd: '/opt/ecosire/app/apps/muhammadamir',
      instances: 1,
      exec_mode: 'fork',
      env: {
        NODE_ENV: 'production',
        PORT: 3020,
      },
      max_memory_restart: '512M',
      out_file: '/var/log/pm2/muhammadamir-web.out.log',
      error_file: '/var/log/pm2/muhammadamir-web.err.log',
      merge_logs: true,
      log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
      watch: false,
      restart_delay: 3000,
      max_restarts: 10,
      min_uptime: '30s',
      autorestart: true,
    },
  ],
};

核心 PM2 命令

# Start all processes from ecosystem file
pm2 start ecosystem.config.cjs

# Restart all (with updated environment variables)
pm2 restart ecosystem.config.cjs --update-env

# Graceful reload (zero-downtime for cluster mode)
pm2 reload ecosystem.config.cjs

# Stop all processes
pm2 stop all

# Delete all processes from PM2 registry
pm2 delete all

# Individual process management
pm2 restart ecosire-api
pm2 stop ecosire-docs
pm2 logs ecosire-web --lines 100

# Real-time monitoring dashboard
pm2 monit

# Status overview
pm2 status
pm2 list

服务器重启时启动

如果没有启动配置,所有 PM2 进程都会在服务器重新启动时丢失:

# Generate and install the startup script for your init system
pm2 startup
# Copy the output command and run it (it looks like:)
# sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u ubuntu --hp /home/ubuntu

# Save the current process list
pm2 save
# This creates ~/.pm2/dump.pm2 — processes are restored on reboot

# Verify startup works
pm2 resurrect  # Manually restore from dump.pm2

每次添加或删除进程时,请再次运行 pm2 save 以更新转储文件。


零停机部署

对于集群模式下的 NestJS,PM2 支持真正的零停机重新加载:

# Reload restarts workers one at a time (zero-downtime)
# Old workers handle requests while new ones start
pm2 reload ecosire-api

# vs restart — kills all workers simultaneously (brief downtime)
pm2 restart ecosire-api

对于 Next.js(以 fork 模式运行,单实例),零停机需要不同的方法。将 wait_ready + listen_timeout 配置与来自应用程序的启动信号结合使用:

// apps/web — this is handled automatically by Next.js
// But for NestJS, send the ready signal explicitly:

// apps/api/src/main.ts
async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3001);

  // Signal PM2 that the process is ready
  if (process.send) {
    process.send('ready');
  }
}

bootstrap();

日志管理

如果不进行管理,PM2 日志可能会填满您的磁盘。立即配置日志轮转:

# Install log rotation module
pm2 install pm2-logrotate

# Configuration
pm2 set pm2-logrotate:max_size 50M       # Rotate when log reaches 50MB
pm2 set pm2-logrotate:retain 7           # Keep 7 days of logs
pm2 set pm2-logrotate:compress true      # Gzip rotated logs
pm2 set pm2-logrotate:dateFormat YYYY-MM-DD
pm2 set pm2-logrotate:workerInterval 30  # Check rotation interval (seconds)
pm2 set pm2-logrotate:rotateInterval '0 0 * * *'  # Daily at midnight

有用的日志命令:

# View all logs combined
pm2 logs

# View specific process logs
pm2 logs ecosire-api

# View with timestamps
pm2 logs --timestamp

# Flush all log files
pm2 flush

# Tail error logs only
pm2 logs ecosire-api --err --lines 200

监控和指标

PM2 Plus(以前称为 Keymetrics)提供基于云的监控。对于自托管监控:

# Built-in terminal dashboard
pm2 monit

# Get JSON status for scripting/monitoring integration
pm2 jlist    # JSON process list
pm2 prettylist  # Formatted process list

# Integrate with your monitoring stack
pm2 set pm2-server-monit:interval 5  # Metrics collection interval

对于生产监控,将 PM2 指标公开给 Prometheus:

npm install -g pm2-prometheus-exporter
pm2 set pm2-prometheus-exporter:port 9209

# Scrape in Prometheus config:
# - job_name: pm2
#   static_configs:
#     - targets: ['localhost:9209']

部署脚本集成

典型的部署顺序:

#!/bin/bash
# scripts/deploy-production.sh

set -e

echo "=== Starting deployment ==="

# 1. Pull latest code
git pull origin main

# 2. Install dependencies
pnpm install --frozen-lockfile

# 3. Build all apps (with Turbo remote cache)
TURBO_TOKEN="$TURBO_TOKEN" TURBO_TEAM="$TURBO_TEAM" \
  npx turbo run build

# 4. Run database migrations
pnpm --filter @ecosire/db db:migrate

# 5. Restart PM2 processes
# --update-env picks up changes in .env.local
pm2 restart ecosystem.config.cjs --update-env

# 6. Wait for processes to stabilize
sleep 10

# 7. Health checks
curl -f https://ecosire.com/ -o /dev/null -s || {
  echo "Web health check failed — rolling back"
  git revert HEAD --no-edit
  pm2 restart ecosystem.config.cjs --update-env
  exit 1
}

curl -f https://api.ecosire.com/api/health -o /dev/null -s || {
  echo "API health check failed — rolling back"
  git revert HEAD --no-edit
  pm2 restart ecosystem.config.cjs --update-env
  exit 1
}

# 8. Save process state
pm2 save

echo "=== Deployment complete ==="

常见陷阱和解决方案

陷阱 1:忘记 --update-env

更新 .env.local 后,在没有 --update-env 的情况下运行 pm2 restart all 会导致进程使用旧环境变量重新启动。始终使用 pm2 restart ecosystem.config.cjs --update-env

陷阱 2:在生产中使用 watch: true

当任何文件更改时 watch: true 会重新启动该进程。在生产中,构建输出会更改每次部署 - 这会导致无限重启循环。始终设置 watch: false

陷阱 3:不处理 SIGTERM 来正常关闭

PM2 在重新启动/停止时发送 SIGTERM。如果您的应用程序无法处理它,PM2 会等待 kill_timeout 毫秒并发送 SIGKILL — 这可能会导致请求丢失。在 NestJS 中处理 SIGTERM:

// main.ts
const app = await NestFactory.create(AppModule);
await app.listen(3001);

// Graceful shutdown
process.on('SIGTERM', async () => {
  await app.close();
  process.exit(0);
});

陷阱 4:PM2 日志磁盘空间不足

如果没有 pm2-logrotate,PM2 日志会无限增长。流量大的 API 每天可以生成千兆字节的日志。立即安装pm2-logrotate并设置合理的max_size(50MB)和retain(7天)。

陷阱 5:重启后进程丢失

pm2 start 不会在重新启动后保留进程。初始设置后始终运行 pm2 startup + pm2 save。如果进程在重新启动后消失,请运行 pm2 resurrect 从保存的转储中恢复。


常见问题

什么时候应该使用集群模式和 fork 模式?

对 CPU 密集型工作负载使用集群模式(需要大量计算、数据处理的 NestJS API)。集群模式生成 instances 工作进程并在它们之间进行 PM2 负载平衡 - 利用所有 CPU 核心。对于 I/O 密集型工作负载(Next.js、静态文件服务)或进程不支持集群(单线程脚本、Docusaurus 服务)时使用 fork 模式。 Next.js 在内部处理自己的工作线程,因此使用 instances: 1 的 fork 模式是正确的。

如何在 Docker 容器中运行 PM2?

Docker 中的 PM2 使用 pm2-docker (或 pm2-runtime)而不是 pm2 来正确处理信号。运行时版本不会守护进程(这会导致 Docker 退出),正确地将信号转发到子进程,并将日志记录到 stdout/stderr 而不是文件。在 Dockerfile 中使用 CMD ["pm2-runtime", "ecosystem.config.cjs"]

如何从远程计算机监控 PM2 进程?

PM2 Plus(按进程付费云服务)提供了一个 Web 仪表板。对于自托管监控,通过 Prometheus 导出器公开 PM2 的指标并在 Grafana 中可视化。对于简单的状态检查,您可以通过 SSH 并运行 pm2 status,或通过监控系统轮询的 HTTP 端点公开指标。

pm2 重新加载和 pm2 重启有什么区别?

pm2 restart 同时杀死所有工作人员并重新启动它们 - 有一段短暂的时间没有运行工作人员(停机时间)。 pm2 reload 很优雅:它启动新的工作程序,等待它们准备好,然后关闭旧的工作程序 - 零停机时间。使用 pm2 reload 进行生产部署。注意:reload仅在集群模式下才能正常工作; fork mode falls back to restart behavior.

如何为不同的进程设置不同的环境变量?

ecosystem.config.cjs 中的每个进程都有自己的 envenv_production 部分。当您将 --env production 传递给 PM2 命令时,将使用 env_production 部分。对于秘密,永远不要将它们直接放在生态系统文件中 - 将它们设置在系统环境或 .env.local 文件中,并让 PM2 继承它们。 --update-env 标志确保 PM2 在重新启动时重新读取环境变量。


后续步骤

PM2 是任何生产 Node.js 部署的基本组成部分。 ECOSIRE 管理生产中的 5 个 PM2 流程 - Next.js、NestJS、Docusaurus 和两个品牌网站 - 每次推送到主程序时都会自动重启、日志轮换和零停机部署。

无论您需要 DevOps 工程支持、生产部署架构还是需要帮助迁移到容器化设置,探索我们的服务 以了解我们如何提供帮助。

E

作者

ECOSIRE Research and Development Team

在 ECOSIRE 构建企业级数字产品。分享关于 Odoo 集成、电商自动化和 AI 驱动商业解决方案的洞见。

通过 WhatsApp 聊天