Как настроить Node.js-приложение на VPS так, чтобы оно работало стабильно и без сюрпризов? PM2, мониторинг, автоматические перезапуски — собрали всё в одной инструкции!
Когда стартап впервые выкатывает API, обычно хватает одной команды — node app.js — чтобы запустить сервис. Пока трафик измеряется десятками запросов в минуту, всё идёт гладко: процесс держится в памяти, а логи выводятся в терминал. Как только нагрузка возрастёт или в код прокрадётся TypeError, приложение аварийно завершится, клиенты получат «502 Bad Gateway», а разработчик в панике будет наблюдать бесконечную бегущую строку в консоли.
В этом материале рассказали, как правильно настроить приложение на VPS, чтобы избежать таких сбоев: как развернуть актуальную LTS-версию Node.js, установить PM2 через npm, подключить его к systemd для автозапуска, настроить ротацию логов, экспортировать метрики в Prometheus и добиться бесшовного перезапуска без потери соединений.
Зачем нужен отдельный процесс-менеджер
Node.js обрабатывает все запросы в единственном потоке. Если внутри цикла событий происходит необработанное исключение или переполнение памяти, процесс завершается — клиентские подключения обрываются, а стандартный запуск node их не восстанавливает. На практике это означает потерянные транзакции и ошибки оплаты даже при кратком сбое.
PM2 решает проблему в два этапа: он поддерживает таблицу процессов и мгновенно поднимает новый экземпляр, когда предыдущий выходит с ненулевым кодом, а также сохраняет эту таблицу на диск и интегрирует её с systemd. После перезагрузки VPS список приложений восстанавливается автоматически, без ручного вмешательства администратора.
Дополнительно PM2 умеет передавать открытый сокет между версиями приложения, поэтому обновление кода проходит без разрыва активных соединений. Журналы stdout и stderr пишутся раздельно, встроенный модуль сжимает их в архивы и удаляет старые файлы, чтобы не переполнить диск. При росте нагрузки достаточно запустить приложение с параметром --instances max, и менеджер создаст столько воркеров, сколько ядер доступно, равномерно распределяя запросы.
Подготовка VPS: обновления, учётная запись, базовая защита
Допустим, вы только что арендовали виртуальный сервер с Ubuntu 22.04 LTS и подключились к нему по SSH. Консоль приветствует вас строкой Welcome to Ubuntu, но это не гарантирует, что ядро и библиотеки свежие. Первая задача — подтянуть пакеты безопасности. Введите:
sudo apt update && sudo apt upgrade -y
Где apt update спрашивает у репозиториев, какие версии доступны, а apt upgrade устанавливает их. Ключ -y избавляет от лишних вопросов, иначе система будет ждать вашего подтверждения после каждой библиотеки. Завершив обновление, перезагрузите сервер командой sudo reboot, чтобы новое ядро стало активным.
Следующий шаг — переход от всесильного root к обычному пользователю. Работать под root опасно: одна опечатка rm -rf / может стереть систему. Создайте новую учётную запись и дайте ей права администратора:
sudo adduser deploy
sudo usermod -aG sudo deploy
Теперь настройте вход по ключу. На домашнем компьютере выполните ssh-keygen без пароля, а затем:
ssh-copy-id deploy@<IP-VPS>
С этого момента сервер пустит вас только с тем самым публичным ключом. Чтобы окончательно отключить пароли, откройте файл
/etc/ssh/sshd_config
Найдите строку:
PasswordAuthentication yes
Замените на no и перезапустите демон:
sudo systemctl restart sshd
Если ошибётесь в конфигурации, текущая сессия останется активной, поэтому вы успеете поправить файл.
Пока сервер ещё голый, закройте ненужные порты. Ubuntu поставляется с удобным брандмауэром ufw, включите его командой:
sudo ufw default deny incoming
sudo ufw allow ssh
sudo ufw enable
Здесь: default deny incoming запрещает всё, что не разрешено явно, allow OpenSSH разрешает порт 22. Когда приложение будет слушать, например, 3000-й порт, добавьте правило:
sudo ufw allow 3000/tcp
Проверить список открытых портов можно так:
sudo ufw status numbered
Чтобы защитить SSH от автоматического подбора паролей (брутфорса), достаточно установить Fail2Ban и запустить его службу. В файле:
/etc/fail2ban/jail.d/ssh.conf
По умолчанию уже есть правило, блокирующее IP после пяти неверных попыток входа.
Убедитесь, что на сервере выставлено точное время: задержка даже в пару минут может привести к тому, что валидный TLS-сертификат будет считаться просроченным, а JWT-токены отклоняться. Проверьте, что служба синхронизации времени запущена командой:
timedatectl status
Если требуется другой часовой пояс, задайте его:
sudo timedatectl set-timezone Russia/Moscow
Установка Node.js нужной версии
Node.js живёт по чёткому расписанию: каждый октябрь выходит новая основная ветка, а предыдущая становится LTS — «долго-живущей». Разработчики поддерживают такую ветку три года, публикуя ежемесячные патчи с пометкой Security или Performance. Обновление приходит по тем же каналам, что и для OpenSSL, поэтому администратору не нужно мониторить отдельный блог.
Перед запуском проверьте на сайте NodeSource, какая ветка сейчас имеет статус LTS. Если, например, актуальной стала 24-я, замените значение «22» в ссылке setup_22.x на соответствующий номер. Чтобы получить именно LTS, подключите репозиторий NodeSource. Скрипт ниже скачает ключ подписи, положит .list-файл в:
/etc/apt/sources.list.d
и обновит кеш пакетов:
curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs
Команда node -v выдаст что-то вроде v22.14.0. В тот же пакет входит npm, проверяем его наличие — npm -v. Если вместо цифр сервер ругается command not found, проверьте, не забыли ли вы добавить /usr/bin в переменную PATH.
Иногда проекту требуется сразу две версии Node.js — старая для монолита, новая для микросервиса. В таких случаях удобно установить nvm в домашний каталог пользователя deploy.
Запустите:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
Затем выполните команду, чтобы текущая сессия «увидела» nvm:
source ~/.nvm/nvm.sh
Теперь можно установить нужную версию:
nvm install 24
nvm use 24
Команда node -v покажет 24.x, а переключиться обратно на LTS можно через nvm use 22.
Утилита nvm не меняет системный /usr/bin/node, поэтому скрипты cron, работающие от root, продолжат использовать стабильную ветку.
Ещё одна тонкость: некоторые npm-пакеты содержат модули на C, которые при установке преобразуются в машинный код (компилируются) — без установленного компилятора процесс прервётся ошибкой «make: not found». Поставьте набор инструментов однажды:
sudo apt install -y build-essential
На сервере с 512 МБ оперативной памяти Node.js по умолчанию резервирует под heap почти половину объёма, из-за чего при пиковых нагрузках системный OOM-киллер может завершить процесс. Чтобы этого не допустить, задайте ограничение через переменную окружения, например:
export NODE_OPTIONS="--max-old-space-size=256"
Добавьте эту строку в /etc/environment и перезагрузите оболочку.
Установка и запуск PM2
Версию PM2 из стандартных репозиториев Ubuntu и Debian замораживают при выпуске дистрибутива, поэтому она быстро устаревает и не получает свежих исправлений. Установка из npm лишена этого недостатка: пакет обновляется сразу после релиза, сохраняя совместимость с LTS-веткой Node.js. Достаточно одной команды:
sudo npm install -g pm2
Исполняемый файл PM2 попадает в /usr/bin, уже включённый в PATH, то есть команду pm2 можно запускать откуда угодно. Чтобы убедиться, что всё установилось, наберите в терминале pm2 -v — это выведет текущую версию PM2. Обновление до самой свежей версии выполняется той же командой:
npm install -g pm2@latest
Здесь npm — это менеджер пакетов для Node.js, опция -g означает глобальную установку, а @latest указывает на самый последний релиз.
Теперь подготовим проект. Пусть код хранится на GitHub, а работать мы будем от имени пользователя deploy:
cd /home/deploy
git clone git@github.com:username/myapi.git
cd myapi
npm ci
Здесь npm ci устанавливает зависимости строго по package-lock.json. Переменные окружения (ключи API, строка подключения к базе) храните в .env и не добавляйте этот файл в репозиторий.
Запускаем сервис под управлением PM2:
pm2 start app.js --name myapi --env production
Флаг --name присваивает процессу понятное имя, чтобы его было легко найти в списке. Параметр --env production внутри приложения устанавливает NODE_ENV=production, то есть говорит ему работать в боевом режиме. После запуска проверьте статус всех процессов командой pm2 list. В колонке status у нужного процесса должно быть «online». А чтобы посмотреть текущие логи конкретного приложения, выполните команду:
pm2 logs myapi. myapi
Здесь myapi — это имя вашего процесса, заданное через --name.
Чтобы приложение стартовало после перезагрузки VPS, интегрируем PM2 с systemd:
pm2 startup systemd -u deploy --hp /home/deploy
pm2 save
Здесь pm2 startup создаёт сервис-юнит; pm2 save сохраняет текущий список процессов. После этого даже экстренный ребут сервера не прервёт работу myapi: systemd поднимет PM2, а менеджер восстановит службу автоматически.
Управление жизненным циклом с PM2
При обновлении кода важно, чтобы пользователи не замечали перезапуска процессов. Команда pm2 reload решает задачу: менеджер поднимает вторую копию приложения, проверяет, что она успешно забрала порт, и только затем завершает старую. Полный цикл обновления конфигурации или зависимостей сводится к одной строке:
git pull && npm ci && pm2 reload myapi --update-env
Параметр --update-env передаёт новому процессу свежие переменные окружения, не влияя на текущие соединения.
Лог-ротация без внешних скриптов
Журналы out.log и error.log могут вырасти до сотен мегабайт за несколько дней. Встроенный модуль pm2-logrotate обрезает и сжимает их без участия cron. Подключите расширение:
pm2 install pm2-logrotate
Затем настройте предел размера, число архивов и сжатие:
pm2 set pm2-logrotate:max_size 100M
pm2 set pm2-logrotate:retain 10
pm2 set pm2-logrotate:compress true
pm2 save
После перезапуска PM2 новые параметры попадут в дамп, который systemd читает при каждом старте. Логи больше не переполнят диск, а поиск ошибок упростится: разверните последний архив и просмотрите его через grep.
Настройка метрик и алертов

Когда приложение работает под PM2, важно следить за его состоянием: потреблением памяти, задержкой event-loop и частотой перезапусков. Встроенный агент PM2 с флагом --metrics формирует поток данных (JSON) на локальный порт 9615. Экспортёр pm2-prometheus-exporter переводит эти метрики в формат Prometheus, после чего их можно сразу подключить к Grafana.
На дашборде отображаются ключевые показатели: объём памяти (RSS), частота сборок мусора, глубина очереди событий и счётчик рестартов. Обычно алерт срабатывает, если процесс перезапускается трижды за 10 минут. Для event-loop можно задать правило: если задержка стабильно выше 100 мс в течение пяти минут, скорее всего, приложение столкнулось с синхронным кодом или медленной базой данных.
Уведомления отправляются через Alertmanager в Slack или Telegram. Чтобы облегчить разбор инцидента, добавьте в сообщение ссылку на pm2 logs и график Grafana за последние три часа — это позволяет открыть логи и увидеть рост нагрузки буквально одной кнопкой.
Продвинутая оптимизация: graceful shutdown и обработчик SIGINT
PM2 завершает процесс корректно: перед остановкой он посылает сигнал SIGINT. Чтобы запросы не обрывались, приложению нужно корректно закрыть открытые порты и соединения с базой.
Добавьте обработчик:
process.on('SIGINT', async () => {
try {
await server.close();
await pool.end();
} catch (err) {
console.error('Ошибка при завершении приложения:', err);
} finally {
process.exit(0);
}
});
Команда server.close() позволяет Node.js сначала обслужить все текущие запросы. Новые подключения в это время уже принимает второй экземпляр, который PM2 поднял командой reload.
Если приложение использует несколько портов или WebSocket-подписок, храните их в массиве servers и закрывайте так:
await Promise.all(servers.map(s => s.close()));
Чтобы избежать петли, когда процесс бесконечно падает и тут же запускается заново, установите задержку с постепенным увеличением времени между рестартами:
pm2 start app.js --name myapi --exp-backoff-restart-delay=200
Параметр --exp-backoff-restart-delay задаёт начальную задержку в миллисекундах между аварийными перезапусками. В данном примере процесс подождёт 200 мс перед первым рестартом, затем постепенно увеличит паузу, если сбои продолжатся.
Каждый новый аварийный выход увеличит паузу, сервис остынет, а очередь запросов разрядится.
После любых изменений сохраняйте конфигурацию pm2 save. Файл ~/.pm2/dump.pm2 держите в регулярном бекапе вместе с репозиторием: при сбое диска менеджер восстановит все процессы одной командой pm2 resurrect.
Keymetrics — это облачная панель, которая помогает следить за приложениями, запущенными через PM2. Достаточно подключить сервер, чтобы увидеть тепловую карту нагрузки (какие процессы нагружены сильнее всего) и начать получать уведомления в Telegram, если процесс начал расходовать слишком много памяти или система зафиксировала критическую ошибку (panic).
В бесплатной версии доступны основные уведомления и история работы за последние 24 часа. Этого обычно хватает, чтобы заметить проблемы на ранней стадии и исправить их до того, как начнут жаловаться пользователи.
Проверка устойчивости до релиза
Перед публикацией поднимите локальный wrk и дайте приложению двести параллельных соединений, постепенно увеличивая время теста до десяти минут. Одновременно расширьте ulimit -n до двадцати тысяч, чтобы убедиться, что система способна открыть нужное количество файловых дескрипторов. Следом остановите базу данных: если используется PostgreSQL в Docker-контейнере, достаточно выполнить:
docker stop postgres
PM2 обязан зафиксировать рост счётчика рестартов, а после возобновления БД приложение должно вернуться в состояние online, сохранив все воркеры. Завершите испытание симуляцией перегрева — запустите фоновый stress --cpu 4, поднимите температуру ядра, посмотрите, не начинает ли event-loop стабильно превышать 100 миллисекунд. Только убедившись, что алерты отработали, а время отклика вернулось к норме, стоит переносить код в продуктивную ветку.
Заключение
В итоге даже небольшая виртуальная машина может стать надёжной площадкой для запуска Node.js-приложения. Грамотно настроенный стек от автозапуска и обновлений до логирования и мониторинга помогает избегать сбоев, заранее видеть возможные проблемы и спокойно управлять сервисом без авралов и ночных звонков.
Читайте в блоге:
- Безопасность Telegram-бота на VPS в 2025 году: профессиональная защита от атак и утечек данных
- Настройка VPS на Debian 11: пошаговое руководство
- Как безопасно работать с паролями с помощью BcryptJS в JavaScript