Как заставить PM2 пережить перезагрузку сервера

Сервер перезагрузился, а сайт не поднялся. Он вернулся через час, когда кто-то запустил деплой. Выглядит как проблема деплоя, но это не она: деплой всего лишь ещё раз выполнил pm2 start руками.

Чтобы приложение поднималось само, нужны две отдельные вещи, и каждая из них без другой бесполезна.

Две половины

pm2 save записывает текущий список процессов в ~/.pm2/dump.pm2. PM2 поднимает только то, что лежит в этом файле, и ничего больше. Скрипт деплоя, который делает pm2 delete и pm2 start, но никогда не сохраняет, оставляет устаревший дамп — или пустой, если тот вообще ни разу не записывался.

pm2 startup ставит юнит systemd, который при загрузке выполняет pm2 resurrect и читает как раз этот дамп.

Одно без другого даёт либо загрузку, которая ничего не поднимает, либо сохранённый список, который никто не читает.

Ловушка

Запуск pm2 startup от непривилегированного пользователя не устанавливает вообще ничего. Команда определяет systemd, собирает строку, которая поставила бы юнит, печатает её и завершается. Вывод длинный, зелёный, и заканчивается строкой о том, что команду можно скопировать. Читается как успех. Не произошло ничего.

Значит: запускать от root, а проверять через systemd, а не по тому, что напечатал PM2.

Правильный порядок

Приложение должно работать до сохранения, иначе вы сохраните пустой список.

# от <user> — убедиться, что приложение работает
pm2 status
# от root — поставить юнит для этого пользователя
env PATH=$PATH:/usr/bin pm2 startup systemd -u <user> --hp /home/<user>

Префикс env PATH=$PATH:/usr/bin — не украшение. В PATH root обычно нет Node, а юнит запоминает тот путь, с которым был сгенерирован. Без префикса юнит существует, но при загрузке не находит node.

# снова от <user> — записать дамп
pm2 save

Проверяйте половины по отдельности

Ломаются они независимо, поэтому и проверять их надо независимо.

systemctl is-enabled pm2-<user>

Должно вывести enabled. Всё остальное, будь то disabled или Failed to get unit file state, означает, что юнит так и не был установлен, что бы вам ни сообщал PM2.

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

Здесь должен появиться <app>. Если файла нет или grep ничего не печатает, значит pm2 save ни разу не выполнялся при запущенном приложении.

Проверка без перезагрузки

Чтобы узнать, работает ли это, перезагружать боевую машину не нужно.

# от <user>
pm2 kill
# от root
systemctl start pm2-<user>
# от <user>
pm2 status
curl -sf http://127.0.0.1:3000/health

Если <app> снова в статусе online, работают обе половины. При загрузке происходит ровно это же.

Никогда не выполняйте pm2 save после pm2 kill. При убитом демоне список процессов пуст, и сохранение записывает эту пустоту в дамп, уничтожая ровно то, что вы проверяли. Если сделали по привычке — поднимите приложение и выполните pm2 save заново.

Добавьте pm2 save в скрипт деплоя

Дамп расходится с реальностью, как только деплой меняет процесс. Если ваш скрипт удаляет и запускает заново, он обязан сохранить после этого:

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

|| true важен при первом запуске, когда удалять нечего и pm2 delete завершается ненулевым кодом, а под set -e это убивает весь скрипт.

Без последней строки всё вышеописанное продолжает работать — ровно до первого деплоя, который переименует приложение, изменит число инстансов или поменяет точку входа. После этого дамп описывает процесс, которого больше нет, и следующая перезагрузка воскрешает именно его.

Ещё о порядке загрузки

Сгенерированный юнит содержит After=network.target. Он не выстроен после вашей базы данных. При холодном старте приложение может подняться раньше, чем MariaDB или PostgreSQL начнут принимать соединения, упасть на первом же запросе и завершиться.

Обычно это решается само: PM2 перезапускает процесс, ко второй или третьей попытке база готова, и сайт поднимается. Признак — счётчик перезапусков больше нуля у в остальном здорового процесса, колонка в pm2 status.

Если восстановления не происходит, задайте порядок явно:

sudo systemctl edit pm2-<user>
[Unit]
After=network.target mariadb.service

Добавляйте это только если сами видели сбой. Поставив юнит за медленным сервисом или за сервисом на другой машине, вы меняете загрузку, которая повторяет попытку, на загрузку, которая просто зависает.

Проверить сайт · Другие статьи · Об AgentReady