
# 让 PM2 在服务器重启后自己活过来（两半缺一不可）

服务器重启了，网站没回来。一个小时后有人跑了一次部署，网站才恢复。这看起来像是部署的问题，其实不是：那次部署只不过是又手动执行了一遍 `pm2 start`。

要让应用自己开机启动，需要两件互相独立的事情，缺了任何一件，另一件都没用。

## 这两半是什么

**`pm2 save`** 把当前的进程列表写进 `~/.pm2/dump.pm2`。PM2 只会恢复这个文件里有的东西，别的一概不管。一个只做 `pm2 delete` 和 `pm2 start`、从不保存的部署脚本，留下的是一份过期的 dump；如果从来没保存过，那就是空的。

**`pm2 startup`** 安装一个 systemd 单元，开机时执行 `pm2 resurrect`，读的正是那份 dump。

只有其中一半，你得到的要么是一次什么都不启动的开机，要么是一份没人读的列表。

## 那个坑

用非特权用户执行 `pm2 startup`，**什么都不会被安装**。它检测到 systemd，算出那条能安装单元的命令，把它打印出来，然后退出。输出很长、是绿色的，末尾还有一行提示你复制这条命令。看上去像是成功了。其实什么都没发生。

所以：用 root 执行，然后用 systemd 来确认，而不是看 PM2 打印了什么。

## 正确的顺序

保存之前应用必须是在运行的，否则你保存下来的是一份空列表。

```bash
# 以 <user> 身份 —— 确认应用在跑
pm2 status
```

```bash
# 以 root 身份 —— 为该用户安装单元
env PATH=$PATH:/usr/bin pm2 startup systemd -u <user> --hp /home/<user>
```

前面的 `env PATH=$PATH:/usr/bin` 不是装饰。root 的 `PATH` 里通常没有 Node，而单元会把生成时的路径原样记下来。少了它，单元是存在的，但开机时找不到 `node`。

```bash
# 再切回 <user> —— 写入 dump
pm2 save
```

## 两半分开验证

它们是分别失效的，所以要分别检查。

```bash
systemctl is-enabled pm2-<user>
```

必须输出 `enabled`。其他任何结果，无论是 `disabled` 还是 `Failed to get unit file state`，都说明单元根本没装上，不管 PM2 当时说了什么。

```bash
grep -o '"name":"[^"]*"' /home/<user>/.pm2/dump.pm2
```

这里必须出现 `<app>`。如果文件不存在，或者 grep 什么都没打印，说明 `pm2 save` 从来没有在应用运行的状态下跑过。

## 不重启也能测

要知道这套是否管用，不必去重启一台生产机器。

```bash
# 以 <user> 身份
pm2 kill
```

```bash
# 以 root 身份
systemctl start pm2-<user>
```

```bash
# 以 <user> 身份
pm2 status
curl -sf http://127.0.0.1:3000/health
```

如果 `<app>` 重新在线，两半都是好的。开机时发生的正是这一串。

**绝对不要在 `pm2 kill` 之后执行 `pm2 save`。** 守护进程被杀掉时进程列表是空的，一保存就把这份空白写进了 dump，恰好毁掉你正在测试的东西。如果顺手做了，把应用重新启动起来，再跑一次 `pm2 save`。

## 把 `pm2 save` 写进部署脚本

只要某次部署动了进程，dump 就会开始对不上。如果你的脚本是先删再起，那它必须在最后保存：

```bash
pm2 delete <app> || true
pm2 start ecosystem.config.cjs --only <app>
pm2 save
```

`|| true` 在第一次运行时才见效：那时没有东西可删，`pm2 delete` 会以非零码退出，在 `set -e` 下会直接把整个脚本终止掉。

少了最后那一行，上面的一切照样能用，一直用到第一次改了应用名、改了实例数或换了入口文件的部署为止。从那以后，dump 描述的是一个已经不存在的进程，而下一次重启会把它复活。

## 关于启动顺序

生成出来的单元带的是 `After=network.target`，它并没有排在你的数据库之后。冷启动时，应用可能在 MariaDB 或 PostgreSQL 开始接受连接之前就起来了，第一次查询就失败，然后退出。

多数情况下这会自己解决：PM2 重启进程，第二次或第三次尝试时数据库已经就绪，网站就上来了。识别的线索是一个本身健康的进程带着大于零的重启计数，在 `pm2 status` 的 `↺` 那一列。

如果它确实恢复不了，再显式地排一下顺序：

```bash
sudo systemctl edit pm2-<user>
```

```ini
[Unit]
After=network.target mariadb.service
```

只有在你亲眼见过它失败时才加。把单元排在一个很慢的服务后面，或者排在一个跑在别的机器上的服务后面，你换来的是一次卡住不动的开机，而不是原来那次会重试的开机。

[检查你的网站](/zh) · [更多文章](/zh/blog) · [关于 AgentReady](/zh/about)
