RE:NODE

PM2: ecosystem-конфиг, автозапуск, логи и reload

Автор Команда RE:NODE
15 мин чтения


PM2 - это менеджер процессов для Node.js: небольшой фоновый демон, который запускает ваше приложение, перезапускает его при падении, пишет вывод в лог-файлы и поднимает всё после перезагрузки машины. Если использовать его правильно, он сводится к четырём вещам: ecosystem.config.js в репозитории, pm2 start с этим файлом, pm2-logrotate, чтобы логи не заполнили диск, и pm2 save вместе с pm2 startup, чтобы машина помнила, что нужно запускать. Большинство проблем с PM2 возникает, когда пропускают один из этих четырёх шагов или запускают PM2 там, где за процессом уже следит кто-то другой.

Последний пункт определяет, нужен ли он вам вообще. На обычном виртуальном или выделенном сервере ничто не перезапустит ваше приложение, пока вы сами это не настроите, и PM2 - разумный выбор. Внутри контейнера на хостинг-панели супервизором уже является платформа, а второй супервизор скрывает от неё ваши падения. Этот случай подробно разобран в статье PM2 и хостинг-панель, здесь он не повторяется. Это руководство рассчитано на сервер, которым вы управляете сами, и описывает, как правильно настроить PM2 именно там.

Что такое PM2 и как он работает#

PM2 состоит из двух частей. Демон - это постоянно работающий процесс Node, которому принадлежат ваши приложения: он их запускает, следит за ними, перезапускает и пишет их стандартный вывод и стандартный поток ошибок в файлы. CLI - команда pm2 - это клиент, который общается с демоном через сокет. Когда вы вводите pm2 start, CLI запускает демон, если тот ещё не работает, просит его запустить приложение, печатает таблицу и завершается. Приложение продолжает работать, потому что работает демон.

Всё, что знает PM2, хранится в PM2_HOME, по умолчанию это ~/.pm2 пользователя, который его запустил. В этом каталоге лежат логи (~/.pm2/logs), PID-файлы, собственный лог демона (~/.pm2/pm2.log) и сохранённый список процессов (~/.pm2/dump.pm2). Важное следствие: демон создаётся для каждого пользователя отдельно. Если вы запустили приложение от пользователя deploy, а потом набрали sudo pm2 ls, вы обращаетесь к демону root, который о нём ничего не знает, и получаете пустую таблицу. Выберите одного пользователя, обычно непривилегированного и владеющего каталогом приложения, и не смешивайте.

Установка - один глобальный пакет:

bash
$ npm install -g pm2$ pm2 --version

Команды и ключи конфигурации PM2 стабильны уже много лет, но значения по умолчанию иногда меняются между мажорными версиями, поэтому, если какое-то число из этого руководства важно для вас, сверьте его с pm2 --version и официальной документацией к вашему релизу.

Запуск приложения: pm2 start, имена и интерпретаторы#

В минимальной форме передаются скрипт и имя. Всегда задавайте имя: по умолчанию это имя файла, а управлять четырьмя приложениями, которые все называются index, неприятно.

bash
$ pm2 start dist/server.js --name api$ pm2 start dist/server.js --name api -- --port 3000   # args after -- go to your app$ pm2 start npm --name web -- start                    # runs "npm start"$ pm2 ls

pm2 ls (также pm2 list или pm2 status) выводит по строке на каждый процесс: id, имя, режим (fork или cluster), счётчик перезапусков, статус, CPU и память. Следить нужно за счётчиком перезапусков. Если число растёт, хотя вы ничего не деплоите, значит приложение падает, а PM2 тихо поднимает его снова.

Запуск через npm start работает, но ставит npm между PM2 и вашим приложением: лишний процесс, занимающий память, и лишний шаг для сигналов при остановке. Где возможно, лучше указывать PM2 настоящий входной файл.

PM2 не ограничен JavaScript. Он выбирает интерпретатор по расширению файла - .py получает python, .sh получает bash, .rb получает ruby, .php получает php - и вы можете задать его явно через --interpreter. Для скомпилированного бинарника или чего угодно, что само является исполняемым файлом, интерпретатор - none:

bash
$ pm2 start worker.py --name worker --interpreter python3$ pm2 start ./bin/server --name go-api --interpreter none$ pm2 start /srv/app/.venv/bin/gunicorn --name web --interpreter none \    -- app:app --bind 127.0.0.1:8000 --workers 2

Две вещи, которые нужно знать о Python под PM2. Во-первых, во многих современных дистрибутивах есть python3, но нет python, поэтому интерпретатор по умолчанию для расширения не находится и вы получаете ошибку. Задайте интерпретатор явно и лучше укажите бинарник из виртуального окружения, чтобы нужные пакеты были в пути поиска. Во-вторых, Python буферизует стандартный вывод, когда он не подключён к терминалу, а под PM2 это означает, что строки лога появляются пачками или не появляются вовсе, пока процесс не завершится. Задайте PYTHONUNBUFFERED=1 в окружении или запускайте с -u. Само окружение разобрано в статье requirements и виртуальные окружения Python, а сервер перед ним - в статье деплой FastAPI или Flask.

Ecosystem-файл#

Флаги командной строки живут в чьей-то истории оболочки. Ecosystem-файл живёт в репозитории, проходит ревью и запускает всё одинаково на любой машине. pm2 init simple создаёт заготовку, а реалистичный файл выглядит так:

ecosystem.config.js
module.exports = {  apps: [    {      name: "api",      script: "dist/server.js",      cwd: "/srv/api",      instances: 2,      exec_mode: "cluster",      max_memory_restart: "400M",      node_args: "--max-old-space-size=320",      watch: false,      time: true,      kill_timeout: 10000,      env: { NODE_ENV: "development", PORT: 3000 },      env_production: { NODE_ENV: "production", PORT: 3000 },    },    {      name: "worker",      script: "worker.py",      cwd: "/srv/api",      interpreter: "/srv/api/.venv/bin/python",      exec_mode: "fork",      env: { PYTHONUNBUFFERED: "1" },    },  ],};
bash
$ pm2 start ecosystem.config.js --env production$ pm2 reload ecosystem.config.js --env production   # after a deploy$ pm2 start ecosystem.config.js --only worker       # one app from the file

Важные ключи и что они делают на самом деле:

КлючЧто делает
nameИдентификатор для всех остальных команд
scriptВходной файл или бинарник, путь относительно cwd
argsАргументы, передаваемые вашему приложению
cwdРабочий каталог; задавайте его явно
interpreterПо умолчанию node; путь или none
node_argsФлаги для самого Node, например размер heap
instancesСколько процессов; max - по одному на каждый CPU
exec_modefork (по умолчанию) или cluster
env / env_<name>Окружение, выбирается через --env <name>
КлючЧто делает
max_memory_restartПерезапуск процесса при превышении этого объёма памяти, например 400M
autorestartПо умолчанию true; false для разовых задач
max_restartsСколько подряд нестабильных перезапусков, прежде чем PM2 сдастся
min_uptimeСколько процесс должен проработать, чтобы считаться запущенным
restart_delayФиксированная пауза перед каждым перезапуском, в мс
exp_backoff_restart_delayРастущая пауза между перезапусками, в мс
kill_timeoutОжидание после сигнала остановки до SIGKILL, в мс
cron_restartПерезапуск по cron-выражению
timeДобавлять метку времени к каждой строке лога

Некоторые из них заслуживают большего, чем ячейка таблицы.

`env` и `env_production`. Обычный блок env применяется всегда; --env production накладывает env_production поверх него. Это удобно, но это не хранилище секретов: ecosystem-файл лежит в репозитории. Храните учётные данные в файле вне репозитория с ограниченными правами и подгружайте его в начале ecosystem-файла (например, через require("dotenv").config({ path: "/etc/api/api.env" })) или пусть приложение читает его само. Почему это различие важно, объясняет статья переменные окружения и секреты.

`max_memory_restart`. PM2 проверяет память с некоторым интервалом, а не при каждом выделении, поэтому это способ подчищать медленные утечки, а не жёсткий потолок. К тому же процесс перезапускается резко, посреди запроса. Держите значение выше обычного пикового потребления и задайте ниже него лимит heap для V8, чтобы Node падал так, чтобы это можно было прочитать, раньше, чем вмешаются PM2 или ядро. Расчёты, включая то, почему у каждого worker в кластере свой heap, есть в статье лимиты памяти Node.

`max_restarts` и `min_uptime`. Процесс, завершившийся раньше min_uptime, считается нестабильным; после max_restarts нестабильных запусков подряд PM2 перестаёт пытаться и помечает его как errored. По документации значения по умолчанию - 16 перезапусков и 1000 мс. Если ваше приложение падает не сразу, а через несколько секунд - например, ожидая соединения с базой данных, - оно никогда не считается нестабильным, и PM2 перезапускает его бесконечно. Лучше использовать exp_backoff_restart_delay: 100: пауза растёт с каждым падением и сбрасывается, когда приложение остаётся работать, так что недоступная база данных не превращается в бесконечный цикл.

`kill_timeout`. При остановке PM2 отправляет SIGINT и ждёт это время перед SIGKILL. По умолчанию это 1600 мс, что меньше, чем длится медленный запрос. Если ваше приложение при остановке дожидается завершения соединений, увеличьте значение так, чтобы оно покрывало самый долгий запрос, и убедитесь, что обработчик слушает SIGINT, а не только SIGTERM. Сторона приложения описана в статье корректное завершение и health check.

Cluster-режим и instances#

exec_mode: "cluster" использует модуль cluster из Node: PM2 запускает несколько копий приложения, и они делят один прослушиваемый порт, а входящие соединения распределяются между ними. Это единственный способ заставить одно приложение на Node использовать больше одного ядра CPU, не написав форкинг самостоятельно, и именно он делает возможными reload без простоя.

Три факта, чтобы выставить ожидания:

  • Cluster-режим работает только с Node. Процесс на Python, Go или Bun не может делить порт таким образом. Для них instances больше 1 в fork-режиме запускает несколько копий, которые все пытаются занять один и тот же порт, и все, кроме первой, падают. Используйте собственные worker-процессы сервера приложений - --workers у Gunicorn, --workers у Uvicorn - и оставьте PM2 с одним экземпляром.
  • `instances: "max"` считает логические CPU машины. На VDS это число ваших vCPU, и это нормально. 0 означает то же самое, а -1 - все, кроме одного, оставляя ядро под базу данных на одной машине.
  • Каждый экземпляр - полная копия вашего приложения. Память на экземпляр умножается, состояние в памяти не разделяется, а всё, что запланировано внутри приложения, выполняется по разу на каждый экземпляр. Компромиссы - sticky-сессии, задачи, запускающиеся четыре раза, подбор числа worker под реальное число ядер - изложены в разделе о кластере в статье PM2 и хостинг-панель.

Увеличить или уменьшить работающий кластер можно без перезапуска:

bash
$ pm2 scale api 4$ pm2 scale api +1

Изменение действует до следующего запуска из ecosystem-файла, где по-прежнему указано instances: 2. Если вы хотите изменить это насовсем, отредактируйте файл.

Restart, reload, stop и delete#

Эти четыре команды - самая путаная часть PM2, и разница между ними не косметическая.

КомандаЧто происходит
pm2 restart apiУбивает все экземпляры, затем запускает их снова
pm2 reload apiВ cluster-режиме заменяет экземпляры по одному
pm2 stop apiОстанавливает; остаётся в списке
pm2 delete apiОстанавливает и убирает из списка

У restart есть разрыв: на мгновение ничто не слушает порт, и запросы в этот момент завершаются ошибкой. reload в cluster-режиме запускает новый экземпляр, ждёт его готовности, затем останавливает один из старых и повторяет, пока все экземпляры не станут новыми, - так что всегда есть кто-то, принимающий соединения. В fork-режиме менять по цепочке нечего, и reload просто сводится к перезапуску.

"Готов" по умолчанию означает, что новый экземпляр начал слушать порт. Если приложению нужно прогреть кэш или открыть пул соединений с базой, прежде чем оно сможет обслуживать запросы, скажите PM2 дождаться явного сигнала:

ecosystem.config.js (excerpt)
wait_ready: true,listen_timeout: 10000,
server.js
server.listen(process.env.PORT, () => {  if (process.send) process.send("ready");});

Если сигнал не пришёл за listen_timeout, PM2 всё равно продолжает, так что неудачный релиз всё же заменяет работающий. Reload - это удобство, а не проверка работоспособности; что ещё должно выполняться, чтобы деплой был действительно без разрыва, описано в статье деплой без простоя на небольшом сервере.

Ещё одна ловушка. PM2 запоминает окружение при первом запуске процесса и использует его повторно. Если изменить переменную окружения и выполнить pm2 restart api, останется старое значение. Добавьте --update-env или перезапускайте из ecosystem-файла:

bash
$ pm2 reload ecosystem.config.js --env production --update-env

А различие между stop и delete важно из-за следующего раздела: остановленный процесс остаётся в списке и будет сохранён как остановленный; удалённый исчезает лишь до тех пор, пока вы не восстановите старый дамп, в котором он ещё есть.

Логи и pm2-logrotate#

Каждый процесс получает два файла: ~/.pm2/logs/<name>-out.log для стандартного вывода и ~/.pm2/logs/<name>-error.log для стандартного потока ошибок. В cluster-режиме каждый экземпляр пишет собственную пару с id в имени, если не задать merge_logs: true.

bash
$ pm2 logs                    # all apps, streaming$ pm2 logs api --lines 200    # last 200 lines, then stream$ pm2 logs api --err          # error stream only$ pm2 flush api               # empty this app's log files

Задайте time: true в ecosystem-файле, если ваш логгер сам не пишет метки времени. Без них лог-файл - это список событий, про которые неизвестно, когда они произошли.

По умолчанию эти файлы растут бесконечно. Разговорчивое приложение, пишущее несколько сотен килобайт в минуту, заполняет небольшой диск за недели, а полный диск утащит за собой и базу данных. Решение - модуль PM2:

bash
$ pm2 install pm2-logrotate$ pm2 set pm2-logrotate:max_size 20M$ pm2 set pm2-logrotate:retain 14$ pm2 set pm2-logrotate:compress true$ pm2 conf pm2-logrotate
ПараметрПо умолчаниюЧто делает
max_size10MРотация файла, как только он превысит этот размер
retain30Сколько ротированных файлов хранить для каждого лога
compressfalseСжимать ротированные файлы через gzip
rotateInterval0 0 * * *Дополнительная ротация по этому cron, по умолчанию раз в сутки

Модуль работает как отдельный процесс внутри демона и виден в pm2 ls, это нормально. Настройки, как и всё в PM2, действуют для каждого пользователя отдельно. Если вы предпочитаете хранить логи в системном журнале, systemd делает это без всяких модулей; о том, что вообще стоит писать в логи, рассказывает статья логи, которые стоит хранить.

pm2 save и pm2 startup: переживаем перезагрузку#

PM2 сам не запускается при загрузке системы. Это исправляют две команды, и у них разные задачи.

pm2 startup создаёт unit-файл systemd, который при загрузке запускает демон PM2 для вашего пользователя. Если запустить её от обычного пользователя, она ничего не устанавливает, а печатает команду, которая это сделает, потому что для установки unit нужен root:

bash
$ pm2 startup[PM2] To setup the Startup Script, copy/paste the following command:sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u deploy --hp /home/deploy

Скопируйте эту строку без изменений и выполните её. Она создаёт и включает unit с именем pm2-deploy.service (для пользователя deploy). Пути в ней зависят от того, где на вашей машине установлен Node, поэтому не копируйте их из чужих инструкций.

pm2 save записывает текущий список процессов в ~/.pm2/dump.pm2. При загрузке unit запускает демон и выполняет pm2 resurrect, который перезапускает всё, что было в этом файле. Поэтому последовательность такая:

  1. Запустите всё, что должно работать: pm2 start ecosystem.config.js --env production.
  2. Проверьте: pm2 ls, все в статусе online, счётчики перезапусков равны нулю.
  3. Сохраните: pm2 save.
  4. Один раз установите загрузочный unit: pm2 startup, затем выполните напечатанную строку с sudo.
  5. Проверьте: sudo reboot, войдите снова, pm2 ls.

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

Если путь к Node меняется - например, после новой версии через nvm, - unit всё ещё указывает на старый бинарник. Повторите всю пару: pm2 unstartup systemd, затем pm2 startup и напечатанную им команду, затем pm2 save. После обновления самого пакета PM2 через npm install -g pm2 выполните pm2 update, чтобы работающий демон был заменён новой версией, а не остался на старой.

Unit можно проверить как любой другой: systemctl status pm2-deploy. Раз уж вы всё равно читаете вывод systemd, стоит знать, что приложение можно запустить как собственный unit вообще без PM2 - это описано в статье сервисы systemd для ваших приложений, и для одного процесса это более лёгкий вариант.

Наблюдение: pm2 monit, describe и счётчик перезапусков#

bash
$ pm2 monit          # terminal dashboard: CPU, memory, logs per process$ pm2 describe api   # full detail: paths, restarts, uptime, env$ pm2 reset api      # zero the restart counter

pm2 monit - это живая панель в терминале со списком процессов, CPU и памятью по каждому и областью логов. Она удобна во время деплоя или нагрузочного теста и бесполезна в четыре часа утра, потому что показывает что-либо, только пока на неё кто-то смотрит. pm2 describe - команда для разбора после аварии: она выводит путь к скрипту, интерпретатор, расположение лог-файлов, аптайм, число перезапусков и число нестабильных перезапусков.

Счётчик перезапусков - самый дешёвый алерт из доступных. Сбрасывайте его после намеренного деплоя (pm2 reset api), и любое ненулевое число после этого означает падение, которое вы не вызывали. Если на него никто не будет смотреть, небольшой cron-скрипт, который запускает pm2 jlist (он выводит список процессов в JSON) и сообщает, когда меняется restart_time или status, - это дюжина строк кода, и он ловит циклы падений задолго до пользователей. У PM2 есть и платный облачный дашборд; для этого руководства он не нужен.

Частые ошибки#

Watch-режим в production. watch: true перезапускает приложение при любом изменении файла в его каталоге. В production это значит, что каждая запись в лог внутри проекта, каждая загрузка, каждый файл кэша вызывает перезапуск, а перезапуск пишет строку в лог, которая вызывает следующий. Держите watch: false вне разработки. Если используете его локально, добавьте ignore_watch: ["node_modules", "logs", "uploads"].

Два демона. Один запущен от root, другой от вашего пользователя, и у каждого половина приложений. Выполните pm2 ls и sudo pm2 ls один раз и убедитесь, что только в одном из них что-то есть. Затем удалите всё из другого и выполните для него pm2 kill.

Забытый `pm2 save`. Всё работает до первой перезагрузки. Сделайте сохранение частью вашего скрипта деплоя.

Два супервизора. PM2 внутри контейнера, платформа которого уже сама перезапускает процесс, или PM2, запущенный из написанного вами unit-файла systemd, в то время как pm2 startup тоже установил свой. Выберите один супервизор на процесс.

`instances: "max"` с кодом, хранящим состояние. Сессии в памяти, WebSocket-соединения в Map, планировщик внутри приложения. Всё это ломается или умножается, как только появляется второй экземпляр.

Логи Python, которые не появляются. Небуферизованный вывод по умолчанию выключен. PYTHONUNBUFFERED=1.

Аргументы не на своём месте. В pm2 start server.js --name api -- --port 3000 всё после -- уходит вашему приложению. Флаги Node вроде --max-old-space-size должны быть в node_args или --node-args, а не после двойного дефиса, иначе они молча игнорируются.

`max_memory_restart` как лекарство. Он прячет утечку, перезапуская процесс по расписанию, которое вы не выбирали. Найдите утечку.

FAQ#

PM2 бесплатный?

Да. Менеджер процессов с открытым исходным кодом, и всё в этом руководстве использует только бесплатный CLI и бесплатный модуль pm2-logrotate. Платная часть - необязательный облачный дашборд мониторинга, который не нужен для работы PM2.

Что выбрать: PM2 или systemd?

Для одного процесса на сервере systemd уже установлен и справляется без лишнего демона. PM2 оправдывает себя, когда нужен cluster-режим Node с плавными reload или несколько небольших приложений, которыми вы управляете одной командой. Оба варианта хороши; плохо запускать оба для одного и того же приложения.

Может ли PM2 запускать Python, Go или shell-скрипт?

Да. Задайте interpreter как бинарник Python (лучше из виртуального окружения), как none для скомпилированного бинарника или оставьте для файлов .sh bash по умолчанию. Cluster-режим и pm2 reload без простоя работают только с Node, поэтому другие рантаймы получают один экземпляр и собственную модель worker-процессов.

Где PM2 хранит мои логи?

В ~/.pm2/logs у пользователя, запустившего приложение, в виде <name>-out.log и <name>-error.log. pm2 describe <name> печатает точные пути. Установите pm2-logrotate, пока логи не стали проблемой для диска.

Почему моё приложение не вернулось после перезагрузки?

Либо pm2 startup так и не был установлен, либо pm2 save не выполнили после запуска приложения, либо сохранение произошло под другим пользователем. Проверьте systemctl status pm2-<user>, затем pm2 ls от этого пользователя, потом запустите, сохраните и перезагрузитесь снова.

Нужен ли PM2 на хостинг-панели?

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