Node.js в продакшене на VDS — это не только про «запустить сервер на порту 3000». Чтобы приложение переживало рестарты, умело выкатываться без простоя, получало трафик через TLS и адекватно логировалось, нужна минимальная, но системная обвязка. В этой статье разбираем практическую схему: Nginx как reverse proxy, PM2 или systemd для управления процессами, корректный логинг и автозапуск.
Архитектурный обзор: роли Nginx, Node.js, PM2 и systemd
Классическая прод-схема строится по принципу разделения обязанностей:
- Nginx принимает внешние HTTP(S)-запросы, завершает TLS, распределяет трафик и проксирует на локальный Node.js.
- Node.js отвечает за бизнес-логику и отдачу API/рендеринга.
- PM2 или systemd управляет жизненным циклом процессов: автозапуск, перезапуск при сбоях, обновления без простоя.
В проде Node.js не должен слушать публичный интерфейс. Прячьте приложение за Nginx и слушайте только 127.0.0.1. Это закрывает доступ напрямую, упрощает фаерволл и TLS.
PM2 или systemd: когда что выбрать
Обе опции жизнеспособны, но закрывают разные кейсы.
PM2: просто, быстро, многофункционально
Подходит, если хочется минимум системной возни и максимум удобства разработчика: кластерный режим по CPU, graceful reload, встроенная ротация логов через модуль, мониторинг и ресуррект процессов после перезагрузки. PM2 сам интегрируется с systemd (командой pm2 startup), так что автозапуск после ребута системы решается в один шаг.
Чистый systemd: предсказуемо и нативно
Сильная сторона — нативная интеграция с ОС, прозрачные права и политики, journald для логов, единое место управления сервисами. Хороший вариант для минималистичных окружений, контейнеров и команд, где уже стандартизован systemd. Мягкие рестарты зависят от реализации в приложении (сигналы, socket activation), зато меньше «магии» вокруг.
Практика: для большинства небольших и средних Node.js-проектов начинается с PM2. Когда пайплайн и мониторинг стабилизируются, часть команд мигрирует на чистый systemd ради унификации.

Подготовка окружения и базовое приложение
Установите LTS-версию Node.js из родного репозитория дистрибутива или официальных сборок. Создайте пользователя без прав root для приложения, выделите рабочую директорию, не храните секреты в репозитории.
# пример: создание пользователя и директории
sudo useradd -r -m -d /opt/myapp -s /usr/sbin/nologin myapp
sudo mkdir -p /opt/myapp
sudo chown -R myapp:myapp /opt/myapp
Минимальный HTTP-сервер для проверки:
// file: /opt/myapp/server.js
const http = require('http');
const port = process.env.PORT || 3000;
const server = http.createServer((req, res) => {
if (req.url === '/health') {
res.writeHead(200, {'Content-Type': 'application/json'});
return res.end(JSON.stringify({status: 'ok'}));
}
res.writeHead(200, {'Content-Type': 'text/plain'});
res.end('Hello from Node.js on VDS\n');
});
server.listen(port, '127.0.0.1', () => {
console.log(`listening on 127.0.0.1:${port}`);
});
Запуск через PM2: кластер, автозапуск, ротация логов
Установите PM2 глобально и создайте конфиг окружения.
sudo npm i -g pm2
Конфигурация ecosystem.config.js с кластерным режимом по числу CPU:
// file: /opt/myapp/ecosystem.config.js
module.exports = {
apps: [
{
name: 'myapp',
script: './server.js',
cwd: '/opt/myapp',
instances: 'max',
exec_mode: 'cluster',
env: {
NODE_ENV: 'production',
PORT: 3000
},
max_memory_restart: '512M',
out_file: '/var/log/myapp/app.log',
error_file: '/var/log/myapp/error.log',
merge_logs: false,
time: true
}
]
};
sudo mkdir -p /var/log/myapp
sudo chown -R myapp:myapp /var/log/myapp
Старт, сохранение состояния и автозапуск:
sudo -u myapp pm2 start /opt/myapp/ecosystem.config.js
sudo -u myapp pm2 status
sudo -u myapp pm2 save
sudo pm2 startup systemd -u myapp --hp /opt/myapp
Резервирование «оживления» после ребута системой выполнит связка pm2 save + pm2 startup. Для обновления кода без даунтайма используйте pm2 reload myapp.
Ротация логов через модуль:
sudo -u myapp pm2 install pm2-logrotate
sudo -u myapp pm2 set pm2-logrotate:max_size 50M
sudo -u myapp pm2 set pm2-logrotate:retain 14
sudo -u myapp pm2 set pm2-logrotate:compress true
sudo -u myapp pm2 set pm2-logrotate:dateFormat YYYY-MM-DD
sudo -u myapp pm2 save
Если требуется централизованный сбор логов, направляйте их в stdout/stderr и забирайте через journald или агент. PM2 позволяет отключить файлы и писать в консоль.
Запуск через systemd: юнит, переменные окружения, перезапуски
Создайте файл окружения с переменными и ограничьте права доступа:
sudo tee /etc/myapp.env > /dev/null << 'EOF'
NODE_ENV=production
PORT=3000
EOF
sudo chown root:root /etc/myapp.env
sudo chmod 600 /etc/myapp.env
Юнит-файл сервиса:
# file: /etc/systemd/system/myapp.service
[Unit]
Description=My Node.js app
After=network.target
StartLimitIntervalSec=0
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
EnvironmentFile=/etc/myapp.env
ExecStart=/usr/bin/node /opt/myapp/server.js
Restart=always
RestartSec=2
# полезные лимиты
LimitNOFILE=65536
# логирование в journald
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
[Install]
WantedBy=multi-user.target
Активация и проверка:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
sudo systemctl status myapp
journalctl -u myapp -f
Для бесшовных релизов реализуйте graceful shutdown: ловите SIGTERM, перестаньте принимать новые соединения, дождитесь активных запросов и завершите процесс. Тогда
systemctl restart myappпройдет мягко.

Nginx как reverse proxy с SSL и WebSocket
Nginx принимает публичный трафик и шифрует его. Node.js слушает только локальный интерфейс. Покажем две секции: редирект HTTP на HTTPS и основную HTTPS-конфигурацию. Если у вас ещё нет сертификата — оформите SSL-сертификаты, а домен можно подключить через нашу регистрация доменов.
# file: /etc/nginx/sites-available/myapp.conf
server {
listen 80;
listen [::]:80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/myapp/fullchain.pem;
ssl_certificate_key /etc/ssl/myapp/privkey.pem;
# базовые настройки TLS, шифры, протоколы и OCSP stapling задайте в общем include
# защита от слишком больших загрузок
client_max_body_size 20m;
# лог формат по желанию: можно использовать json
access_log /var/log/nginx/myapp.access.log;
error_log /var/log/nginx/myapp.error.log warn;
# прокси до локального Node.js
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
proxy_send_timeout 60s;
proxy_connect_timeout 5s;
# при долгих SSE можно отключить буферизацию
# proxy_buffering off;
}
location = /health {
access_log off;
proxy_pass http://127.0.0.1:3000/health;
}
}
Активируйте конфиг и перезапустите Nginx:
sudo ln -s /etc/nginx/sites-available/myapp.conf /etc/nginx/sites-enabled/myapp.conf
sudo nginx -t
sudo systemctl reload nginx
Для HSTS включайте заголовок только после уверенного запуска HTTPS, чтобы не запереть клиентов на нерабочем домене. Подробно о редиректах и HSTS мы писали в заметке: миграция с 301 и HSTS.
Логирование: Nginx, Node.js, PM2/systemd
Комбинируйте уровни логирования: баланс удобства и объема.
- Nginx: access/error в отдельные файлы, краткая ротация.
- Node.js: структурированные JSON-логи в stdout/stderr, чтобы их подхватывал journald или PM2.
- PM2 или systemd: ответственность за хранение/ротацию и просмотр.
Пример расширенной ротации для Nginx:
# file: /etc/logrotate.d/nginx-myapp
/var/log/nginx/myapp.access.log /var/log/nginx/myapp.error.log {
daily
rotate 14
missingok
compress
delaycompress
notifempty
sharedscripts
postrotate
systemctl reload nginx >/dev/null 2>&1 || true
endscript
}
Если используете PM2-файлы логов, модуль pm2-logrotate уже покрывает задачи ротации. Альтернатива — писать только в stdout/stderr:
// фрагмент ecosystem.config.js
module.exports = {
apps: [
{
name: 'myapp',
script: './server.js',
out_file: '/dev/stdout',
error_file: '/dev/stderr',
time: true
}
]
};
Структурные логи в приложении:
// пример простого JSON-логирования
function log(level, msg, extra) {
const rec = Object.assign({
ts: new Date().toISOString(),
level,
msg
}, extra || {});
console.log(JSON.stringify(rec));
}
log('info', 'server start', {port: process.env.PORT});
Автозапуск и устойчивость к сбоям
С PM2:
pm2 saveсохраняет список процессов.pm2 startupгенерирует юнит и включает автозапуск для пользователя.pm2 resurrectподнимает процессы после перезагрузки вручную, если нужно.
С systemd:
systemctl enable myappдля автозапуска при старте ОС.Restart=alwaysиRestartSecв юните для автоматического рестарта при ошибке.StartLimitIntervalSecиStartLimitBurstконтролируют флэппинг.
Следите за лимитами дескрипторов (
LimitNOFILE) и используйте health-check для внешнего мониторинга. Рестарты без потерь требуют корректной обработки сигналов в приложении.
WebSocket и sticky-сессии: что важно знать
Если приложение держит WebSocket-соединения, проверьте:
- В Nginx проставлены
Upgrade/Connectionзаголовки (см. конфиг выше). - Таймауты
proxy_read_timeoutувеличены, если соединения долгие. - PM2 в
cluster-режиме принимает один порт и сам делит соединения между воркерами, что обычно достаточно для WS.
Если вы запускаете несколько независимых экземпляров приложения на разных портах и балансируете их в Nginx через upstream, для sticky-сессий можно использовать ip_hash или проксировать маркер сессии в бэкенд и реализовать привязку самостоятельно.
Обновления без даунтайма
С PM2 все просто:
pm2 reload myapp— graceful перезагрузка воркеров по одному.pm2 deploy— можно собрать простой деплой-хук без простоя.
С systemd используйте свой механизм graceful shutdown в приложении и стратегию смены версии:
- Запустите новую версию на другом порту (
myapp@3001), - переключите прокси на новый порт (
nginx reload), - корректно завершите старую версию.
Даже если у вас stateless API, планируйте миграции БД с учетом совместимости схемы и двустороннего отката. Про перенос проектов без простоя читайте также: миграция без даунтайма.
Безопасность и эксплуатационные мелочи
- Запускайте приложение от отдельного пользователя без shell-доступа.
- Слушайте только
127.0.0.1; внешне открыты лишь 80/443. - Секреты храните в
EnvironmentFileс правами600; не коммитьте их в репозиторий. - Ограничьте
client_max_body_sizeи проверьте обработку загрузок. - Добавьте
/healthили/readyendpoint, чтобы отделить проверку готовности от факта «живости» процесса. - Резервное копирование конфигов Nginx, systemd-юнитов и env-файлов — часть базового бэкапа.
Траблшутинг: типичные грабли
- 502 Bad Gateway: проверьте, слушает ли Node.js
127.0.0.1:PORT, совпадает ли порт в Nginx, нет ли SELinux/AppArmor блокировок. - WebSocket не коннектится: заголовки Upgrade/Connection, HTTP/1.1 к апстриму, таймауты.
- Слетает после ребута: для PM2 выполните
pm2 saveиpm2 startup; для systemd —systemctl enable. - Логи растут без меры: включите pm2-logrotate или logrotate для Nginx, настройте ретеншен.
- Нет IP клиента в приложении: используйте
X-Forwarded-Forи читайте его в приложении.
Чек-лист продовой сборки
- Nginx слушает 80/443, редиректит на HTTPS, проксирует на 127.0.0.1:PORT.
- Сертификаты валидны и продлеваются автоматически, HSTS подключается осознанно.
- Node.js стартует через PM2 или systemd, перезапускается автоматически.
- Логи ротируются, хранение и формат согласованы.
- Health-check отдает 200 быстро и стабильно.
- Таймауты и лимиты настроены, апстрим держит WebSocket/SSE.
- Релизы проходят без простоя по отработанной процедуре.
Итог: минимальная, но правильная обвязка — это Nginx как reverse proxy с TLS, PM2 или systemd для управления процессами, четкая стратегия логирования и автозапуска. Такая схема укладывается в несколько конфигов, легко поддерживается и масштабируется по мере роста нагрузки.


