【PM2 教程】从零开始的 PM2 进程管理实战:让你的 Node.js 应用永不掉线
文章目录

1. 引言
你大概率遇到过这样的场景:写完一个 Node.js 服务,SSH 连上服务器,敲下 node app.js,一切正常。然后你关掉终端下班回家,第二天发现服务挂了——因为终端退出时,进程也跟着死了。
或者更糟的:凌晨三点服务因为一个未捕获的异常崩了,没人知道,直到用户投诉才慌忙重启。
这些问题的本质是一样的:直接用 node 命令启动的服务是"裸奔"的——没有守护、没有自愈、没有监控。就像让一个没有社保、没有医保的员工直接上岗,出了事只能靠天命。
PM2 就是来解决这个问题的。它负责一件很朴素的事:让你的 Node.js 应用始终在线,挂了就拉起来,日志帮你记好,还能充分利用多核 CPU。
读完这篇文章,你将掌握:
- PM2 是什么以及它解决了什么问题
- 用 PM2 启动、停止、重启、监控 Node.js 应用
- 配置集群模式榨干多核性能
- 让应用在服务器重启后自动启动
- 用一个真实的 Express 项目完成从零到生产的部署

2. PM2 是什么
一句定义
PM2(Process Manager 2)是一个 Node.js 生产环境进程管理器。它由 Keymetrics 团队维护,在 GitHub 上有超过 40k Star,是 Node.js 生态中事实上的进程管理标准。
GitHub地址:
GitHub - Unitech/pm2: Node.js Production Process Manager with a built-in Load Balancer.
它本质上是一个跑在后台的守护进程(Daemon),帮你看着所有应用进程——谁挂了就重启谁,谁出错了就把日志记下来,谁来请求了就帮你在多个进程间分配。
PM2 的 7 大核心能力
| 能力 | 一句话说明 |
|---|---|
| 进程守护 | 应用崩溃自动重启,不用你半夜爬起来救火 |
| 负载均衡 | 启动多个进程实例,自动把请求分给不同实例 |
| 日志管理 | 自动收集 stdout/stderr,支持日志轮转防止硬盘写满 |
| 零停机重载 | pm2 reload 逐个重启进程,更新代码时服务不中断 |
| 开机自启 | 服务器重启后自动拉起所有应用 |
| 实时监控 | 终端仪表盘,CPU、内存、请求量一目了然 |
| 配置文件驱动 | 一份 ecosystem.config.js 描述所有应用,适合团队和 CI/CD |
PM2 适合谁
你不需要是一个运维老手才能用 PM2。只要你的工作涉及以下任意一种场景,PM2 就对你有用:
- 在云服务器上部署 Node.js 后端服务
- 跑一个 Nuxt/Next 的全栈应用
- 管理多个微服务进程
- 需要一个比
nohup &靠谱的"后台运行"方案 - 想在单台机器上充分利用多核 CPU

守护进程 PM2 Daemon 在中央,管理着多个应用进程(app1、app2…),收集日志到统一位置,通过 pm2 命令行工具进行交互
3. 安装与第一个应用
3.1 安装 PM2
PM2 是一个 npm 全局包,一行命令搞定:
npm install -g pm2
安装完成后验证一下:
pm2 --version
# 输出类似:5.4.2
3.2 准备一个测试应用
我们先写一个最简 HTTP 服务来玩。创建文件 app.js:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
message: 'Hello from PM2!',
pid: process.pid, // 进程ID,后面讲集群模式时会很有趣
time: new Date().toISOString()
}));
});
server.listen(3000, () => {
console.log(`Server running on port 3000, PID: ${process.pid}`);
});
3.3 用 PM2 启动它
pm2 start app.js --name my-app
--name my-app 给应用取个名字,后续管理时比用数字 ID 方便得多。
启动后你应该看到类似输出:
[PM2] Starting /home/user/app.js in fork_mode (1 instance)
[PM2] Done.
┌─────┬──────────┬─────────────┬─────────┬─────────┬──────────┬────────┬──────┬───────────┬──────────┬──────────┬──────────┬──────────┐
│ id │ name │ namespace │ version │ mode │ pid │ uptime │ ↺ │ status │ cpu │ mem │ user │ watching │
├─────┼──────────┼─────────────┼─────────┼─────────┼──────────┼────────┼──────┼───────────┼──────────┼──────────┼──────────┼──────────┤
│ 0 │ my-app │ default │ 1.0.0 │ fork │ 12345 │ 0s │ 0 │ online │ 0% │ 25.3mb │ user │ disabled │
└─────┴──────────┴─────────────┴─────────┴─────────┴──────────┴────────┴──────┴───────────┴──────────┴──────────┴──────────┴──────────┘
如图我启动的server服务:

现在用浏览器或 curl 访问 http://localhost:3000,你会看到 JSON 响应。
更有意思的是:关掉终端再打开一个新终端,访问 http://localhost:3000,服务还在。 这就是 PM2 守护进程在起作用——它不是挂在你那个终端会话下面的,而是被 PM2 Daemon 接管了。
3.4 理解 pm2 list 的输出
用 pm2 list 随时查看所有应用的状态。关键列含义:
| 列名 | 含义 |
|---|---|
| id | PM2 内部的进程编号 |
| name | 你给应用起的名字 |
| mode | fork(单进程)或 cluster(集群模式) |
| pid | 操作系统级别的进程 ID |
| status | online(正常)/ stopped(停止)/ errored(异常) |
| ↺ | 重启次数——这个数字如果疯涨,说明代码有问题 |
| uptime | 运行时长,刚启动显示 0s |
| cpu / mem | 当前 CPU 和内存占用 |
实用技巧:如果你看到某个应用的重启次数(↺)在快速上涨,说明它反复崩溃——赶紧去看日志。

4. 核心功能详解
4.1 进程守护与自动重启
这可能是 PM2 最重要的功能:崩溃自愈。
试试故意让我们的应用崩溃。修改 app.js,加一个定时炸弹:
const http = require('http');
const server = http.createServer((req, res) => {
// 每处理 5 个请求就模拟一次崩溃
if (Math.random() < 0.2) {
process.exit(1); // 模拟致命错误
}
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'ok' }));
});
server.listen(3000);
用 PM2 重启应用后,连续发几次请求:
pm2 restart my-app
# 连续发 10 个请求
for i in {1..10}; do curl -s http://localhost:3000; echo; done
你会发现前几个请求还能正常响应,随机到 process.exit(1) 时服务瞬间挂掉——但下一秒又好了。这就是 PM2 在检测到进程退出后立刻重新拉起。
用 pm2 list 看一眼,↺ 列的数字涨了。
防止无限重启:如果代码写了死循环的 bug,PM2 不会无限重启下去。它默认的机制是:如果 15 秒内重启超过 15 次,就放弃,把进程标为 errored。你可以通过参数调整:
pm2 start app.js --max-restarts 5 --restart-delay 3000
# 最多重启5次,每次间隔至少3秒
4.2 常用管理命令速查
所有管理命令的入口都是 pm2 <action> <app-name-or-id>:
# 启动
pm2 start app.js --name my-app # 指定名称启动
pm2 start app.js -i 2 --name api # 集群模式启动2个实例
pm2 start ecosystem.config.js # 用配置文件启动(推荐)
# 查看
pm2 list # 应用列表(最常用)
pm2 show my-app # 单个应用详情:路径、日志位置、环境变量
pm2 monit # 实时仪表盘——CPU、内存、请求量都在滚动
# 控制
pm2 stop my-app # 停止(保留在 PM2 列表中)
pm2 restart my-app # 重启
pm2 delete my-app # 从 PM2 列表中彻底移除
pm2 reload my-app # 零停机重载(逐个重启实例,集群模式下特别有用)
# 全局
pm2 kill # 杀掉 PM2 Daemon 本身,所有应用全部停止
pm2 flush # 清空所有日志
pm2 save # 保存当前进程列表,下次 PM2 启动时自动恢复
restartvsreload的区别很重要:restart是直接全部杀掉再启动(会短暂中断),reload是一个一个重启(服务不中断)。生产环境更新代码时,用reload。
pm2 monit 实时仪表盘如下

4.3 日志管理
PM2 自动接管了应用的标准输出(stdout)和标准错误(stderr),存到统一位置:
pm2 logs # 实时查看所有应用日志(tail -f 的效果)
pm2 logs my-app # 只看指定应用
pm2 logs --lines 200 # 看最近200行
pm2 logs --err # 只看错误日志
日志文件默认位置在 ~/.pm2/logs/:
~/.pm2/logs/
├── my-app-out.log # 标准输出日志
├── my-app-error.log # 错误日志
└── ...
日志轮转:线上跑久了日志文件会变得巨大。安装 PM2 日志轮转模块:
pm2 install pm2-logrotate
# 配置(可选)
pm2 set pm2-logrotate:max_size 10M # 单个日志文件最大 10MB
pm2 set pm2-logrotate:retain 30 # 保留最近 30 个日志文件
pm2 set pm2-logrotate:compress true # 压缩旧日志
一旦日志文件超过 max_size,PM2 会自动切分、归档,旧的会自动删除——不用担心硬盘被日志写满。
4.4 集群模式:榨干多核性能
Node.js 默认是单线程的,一个进程只能跑在一个 CPU 核上。如果你的服务器有 4 核,只开一个 Node 进程意味着另外 3 个核在摸鱼。
PM2 的 集群模式(Cluster Mode) 就是答案。它在底层使用了 Node.js 原生的 cluster 模块,自动 fork 出多个工作进程,并把请求分发给它们:
# 启动与 CPU 核数相等的进程
pm2 start app.js -i max
# 或者指定数量
pm2 start app.js -i 4
再看看 pm2 list,你会发现同一个应用出现了多个进程实例,mode 列显示 cluster。
注意:集群模式要求应用是无状态的(或共享状态)。如果应用依赖内存中的本地变量(非数据库/Redis),不同请求打到不同的工作进程时会拿到不同的数据。生产环境建议用 Redis 或数据库来共享状态。
cluster 和 fork 模式的对比:
| 维度 | fork 模式 | cluster 模式 |
|---|---|---|
| 进程数 | 1 个 | N 个(通常等于 CPU 核数) |
| CPU 利用 | 单核 | 多核 |
| 负载均衡 | 无 | PM2 内置 |
| 适用场景 | 开发调试、简单脚本、cron job | 生产环境 HTTP 服务 |
| 端口 | 直接监听 | 多进程共享同一端口 |
4.5 开机自启
服务器总会因为各种原因重启——安全补丁、机房维护、或是电源闪断。你不可能每次重启都手动 SSH 上去 pm2 start 一遍。
PM2 的开机自启方案很优雅,一条命令搞定:
# 1. 生成并配置启动脚本(会根据你的系统是 systemd/upstart/launchd 自动适配)
pm2 startup
# 上面命令会输出一条 sudo 命令,复制执行它
# 示例输出:sudo env PATH=$PATH:/usr/bin pm2 startup systemd -u youruser --hp /home/youruser
# 2. 把当前运行的应用列表保存为"需要开机恢复"的快照
pm2 save
此后每次服务器重启,PM2 会自动启动,然后根据 pm2 save 存的快照把所有应用拉起来。
验证是否配置成功(以 systemd 为例):
systemctl status pm2-$(whoami)
# 或
systemctl status pm2-root
5. 配置文件生态(Ecosystem File)
5.1 为什么需要配置文件
前面所有操作都靠命令行传参。这在探索阶段没问题,但生产环境不行:
- 不可复现:换个服务器还得重新敲一遍命令,忘了某个参数服务就跑得不对
- 团队协作差:同事不知道你启动时传了什么参数
- CI/CD 不友好:自动化脚本拼命令行参数又丑又脆
- 多环境无法管理:开发、测试、生产环境参数不同,命令行根本管不过来
PM2 的答案是 Ecosystem File——一个 JavaScript 配置文件,完整描述你的所有应用和运行参数。
5.2 生成模板
pm2 ecosystem
这条命令会在当前目录生成一个 ecosystem.config.js 模板文件。PM2 5.x 默认生成 ESM 格式(export default),如果你需要旧的 CommonJS 格式(module.exports),用:
pm2 ecosystem --cjs
5.3 配置文件结构详解
下面是一个生产级的 ecosystem.config.js 示例,部署一个 Express API 服务:
module.exports = {
apps: [
{
// ---- 基本 ----
name: 'api-server', // 应用名称,pm2 list 中显示
script: './src/server.js', // 入口文件
cwd: '/var/www/api-server', // 工作目录
// ---- 集群 ----
instances: 'max', // 'max' = CPU核数,或指定数字如 2
exec_mode: 'cluster', // 'fork' 或 'cluster'
// ---- 环境变量 ----
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
},
env_staging: {
NODE_ENV: 'staging',
PORT: 3001,
},
// ---- 日志 ----
error_file: './logs/err.log', // 错误日志路径
out_file: './logs/out.log', // 输出日志路径
log_date_format: 'YYYY-MM-DD HH:mm:ss Z', // 日志时间戳格式
merge_logs: true, // 集群模式下合并所有实例的日志
// ---- 重启策略 ----
max_restarts: 10, // 最多重启次数
restart_delay: 2000, // 两次重启的最小间隔(ms)
min_uptime: '10s', // 至少运行10秒才算启动成功
// ——防止有bug的代码在无限快速重启循环
max_memory_restart: '500M', // 内存超过500M自动重启
// ---- 更新 ----
watch: false, // 生产环境关掉文件监听
ignore_watch: ['node_modules', 'logs'],
// ---- 其他 ----
autorestart: true, // 崩溃自动重启
kill_timeout: 5000, // 强杀前的等待时间(ms)
}
],
// 部署配置(可选,用于 pm2 deploy)
deploy: {
production: {
user: 'deploy',
host: '192.168.1.100',
ref: 'origin/main',
repo: 'git@github.com:user/api-server.git',
path: '/var/www/production',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
},
},
};
5.4 用配置文件启动
# 默认环境(env)
pm2 start ecosystem.config.js
# 指定生产环境(使用 env_production)
pm2 start ecosystem.config.js --env production
# 重载生产环境(零停机更新!)
pm2 reload ecosystem.config.js --env production
有了配置文件,整条部署链路就通顺了:拉代码 → 装依赖 → pm2 reload ——一行搞定,服务不中断。
6. 实战:部署一个 Express 应用
让我们把前面学的串起来,完成一次完整的 PM2 部署流程。
6.1 项目准备
项目目录结构:
express-api/
├── src/
│ └── server.js
├── package.json
└── ecosystem.config.js
server.js——一个简单的 Express API,返回当前时间和服务的进程 PID:
const express = require('express');
const app = express();
// 一个简单的监控端点
app.get('/health', (req, res) => {
res.json({ status: 'ok' });
});
// 主业务接口
app.get('/api/time', (req, res) => {
res.json({
time: new Date().toISOString(),
pid: process.pid, // 可以看到请求被分发到了哪个工作进程
env: process.env.NODE_ENV || 'development',
});
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}, PID: ${process.pid}`);
});
ecosystem.config.js:
module.exports = {
apps: [{
name: 'express-api',
script: './src/server.js',
instances: 2, // 启动2个实例
exec_mode: 'cluster',
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
},
max_restarts: 5,
min_uptime: '5s',
error_file: './logs/err.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
merge_logs: true,
}],
};
6.2 启动与验证
# 安装依赖
npm install
# 用 PM2 启动
pm2 start ecosystem.config.js
# 查看状态——应该看到两个 online 的进程
pm2 list
验证服务:
# 多请求几次,观察 pid 的变化——你会发现 pid 在两个值之间轮换
curl http://localhost:3000/api/time
curl http://localhost:3000/api/time
curl http://localhost:3000/api/time
这说明 PM2 的负载均衡在正常工作——请求被轮流分发给两个工作进程。
6.3 模拟崩溃与自愈
# 查看当前两个进程的 PID
pm2 list
# 手动杀掉其中一个进程(kill 它的 pid)
kill <pid>
# 再看一眼——被杀的那个进程 pid 变了(被重启了),↺ 次数 +1
pm2 list
6.4 零停机更新
假设你改了代码,需要更新线上服务:
git pull
npm install
pm2 reload ecosystem.config.js --env production
reload 会逐个重启实例:先起一个新实例,等它就绪后,再杀掉一个旧实例,循环直到全部替换。整个过程中,剩余实例继续处理请求,用户无感知。
7. 常见问题与踩坑
Q1:pm2 start 后提示端口被占用?
大概率是上一个 PM2 实例已经运行了但你没注意到。先用 pm2 list 确认,不要同时用 node 和 pm2 启动同一端口的服务。
如果确认端口被其他程序占用:
# Windows
netstat -ano | findstr :3000
# Linux / macOS
lsof -i :3000
Q2:环境变量不生效?
两个常见陷阱:
- 写错位置了。
env_xxx的xxx需要和--env xxx精确匹配。env_production对应--env production,不是--env prod。 - 在
app.js中console.log(process.env)没看到变量。在 ecosystem 中环境变量只对入口脚本及其子进程生效,你在 ecosystem 外面先export再pm2 start是无效的。
正确姿势是先在配置文件的 env 中声明,然后用 --env 指定环境。
Q3:升级 Node.js 后 PM2 应用挂了?
PM2 保存的是 Node.js 的绝对路径。你升级 Node.js(比如用 nvm 切版本),路径变了,PM2 还在找旧的 Node 二进制文件,自然启动不了。
解决方案:
# 先删掉旧进程
pm2 delete all
# 重新生成启动脚本
pm2 unstartup # 清理旧的
pm2 startup # 用新 Node 路径重新生成
# 重新启动应用
pm2 start ecosystem.config.js
pm2 save
Q4:在 Docker 容器里还要不要用 PM2?
这是个经典问题,答案取决于场景:
| 场景 | 建议 |
|---|---|
| 单容器单进程 | 不用 PM2,让 Docker 本身做进程管理和重启(--restart always) |
| 单容器需要多核利用 | 用 PM2 cluster 模式,在容器内 fork 多进程 |
| 单容器跑多个不同服务 | 可以但不推荐——Docker 哲学是一容器一进程 |
如果决定在容器里用 PM2,记得以 pm2-runtime(而不是 pm2)作为入口,它专为容器设计,不会后台化:
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
一个判断标准:如果你需要在一个容器里跑多个 Node 实例来利用多核,PM2 cluster 是正确的选择。如果只需要单进程,Docker 自带的 restart 策略就够了。
8. 总结
PM2 的核心价值可以浓缩为四个字:守护、集群、日志、自启。
| 能力 | 一命令速查 |
|---|---|
| 启动并守护 | pm2 start app.js --name xxx |
| 查看状态 | pm2 list / pm2 monit |
| 集群多核 | pm2 start app.js -i max |
| 查看日志 | pm2 logs xxx |
| 零停机更新 | pm2 reload xxx |
| 开机自启 | pm2 startup → pm2 save |
| 配置文件 | pm2 start ecosystem.config.js --env production |
参考
更多推荐


所有评论(0)