
Каждый пуш в main — и ты на 2 минуты зажмуриваешься, обновляя страницу. Работает? Не работает? 502 Bad Gateway? Знакомо? У меня так было каждый деплой, пока я не разобрался, как правильно готовить PM2 для zero-downtime доставки фронтенда.
Судя по количеству тикетов и обсуждений в сообществе — я далеко не один. Только в репозитории Next.js на GitHub — десятки issues и discussions с тегом «502 Bad Gateway» (#55422, #80558, #25354). На Reddit регулярно появляются посты вроде «Is there a way to build without bringing down the app?» и «Approach to zero downtime deployment when not using Vercel?». На Stack Overflow вопросы про «pm2 reload zero downtime» — стабильно в топе по тегу Node.js.
В этой статье — реальный путь от «деплою и молюсь» до rolling restart без единого 502. С кодом, граблями, конкретными расчётами сэкономленного времени и денег, и честным сравнением: а может, проще было взять Kubernetes?
Оглавление
Масштаб проблемы: не только у вас {#scale}
Прежде чем перейти к решению, давайте оценим масштаб. Это не редкий edge-case — это системная проблема всех, кто деплоит Node.js/Next.js на VPS без контейнерной оркестрации.
GitHub Issues & Discussions:
Next.js репозиторий: #55422 «Nginx 502 Bad Gateway after v13.4.15» — десятки затронутых разработчиков
Discussion #80558 «Zero Downtime deployment» — автор описывает ровно ту же проблему: PM2 reload + Next.js = всё равно 502
Discussion #30899 «How to avoid downtime using PM2» — длинная цепочка, где каждый приходит со своим workaround'ом
PM2 репозиторий: #4974 «pm2 deploy with zero downtime» — проблема существует годами
Reddit (r/nextjs, r/node):
«Is there a way to build without bringing down the app?» — классический крик о помощи
«Approach to zero downtime deployment when not using Vercel?» — целый тред с решениями разной степени костыльности
«Next.js Deployment Script for Zero Downtime on VPS with PM2» — гайд набрал сотни апвоутов
«Is PM2 still the way to go in 2024?» — 110 комментариев, community split 50/50
«How to Achieve Zero-Downtime Deployments with PM2 in GitHub Actions?» — вопрос повторяется раз в несколько месяцев
Stack Overflow:
«How to achieve rolling update with pm2?», «How to achieve zero-downtime deployment with pm2?» — одни из самых популярных вопросов по теме
Если суммировать: сотни разработчиков сталкиваются с этой проблемой, каждый изобретает свой велосипед. При этом решение — буквально замена одной команды и удаление одной строки. Ирония.
Боль: что происходит при «наивном» деплое {#pain}
Типичный сценарий деплоя Next.js на VPS выглядит примерно так:
ssh root@server cd /var/www/my-app git pull npm install pm2 stop my-app rm -rf .next npm run build # 1-3 минуты тишины... pm2 start my-app
Что видит пользователь в эти 1-3 минуты? 502 Bad Gateway. Nginx проксирует запрос на порт 4000, а там — никого. Процесс убит, идёт сборка.
Вот как выглядел мой GitHub Actions workflow:
script: | cd /root/my-app git fetch --force origin main git reset --hard origin/main pnpm install --frozen-lockfile pm2 stop my-app || true rm -rf .next pnpm build pm2 start my-app
Четыре строчки, три проблемы:
pm2 stop— убивает все воркеры до начала сборкиrm -rf .next— удаляет скомпилированные файлы, пока старые процессы (если бы они работали) пытаются их отдатьpm2 start— поднимает все воркеры одновременно, создавая пик нагрузки
Результат: от 1 до 3 минут полного даунтайма на каждый деплой. При 5-10 деплоях в день — это до 30 минут простоя.
Цена простоя: почему 502 — это не просто неудобство {#cost}
Может показаться, что пара минут — ерунда. Но цифры говорят иное.
Деньги. По данным ITIC (2024), стоимость минуты простоя для малого бизнеса составляет от $137 до $427. Для среднего — уже более $5 600 в минуту (Gartner). Три минуты деплоя × 5 деплоев в день × 20 рабочих дней = 300 минут простоя в месяц. Даже по нижней планке это $41 000/мес потенциальных потерь.
Пользователи. 61% пользователей не вернутся на сайт, если столкнулись с проблемой доступа (Google). Для SaaS-продукта, где каждый визит — потенциальная конверсия, это критично.
SEO. Google не обрушит ваши позиции из-за одного короткого даунтайма. Но если Googlebot регулярно натыкается на 502 при обходе — сайт начнёт терять позиции. Как отмечает StatusCake: «Сайт, который часто недоступен, будет оценён как предоставляющий неоптимальный пользовательский опыт».
Доверие. Это сложнее измерить, но проще потерять. Пользователь, который видит 502 в момент оплаты или заполнения формы, с высокой вероятностью уйдёт к конкуренту.
Считаем экономию: конкретные цифры {#math}
Давайте посчитаем для типичного SaaS-проекта на VPS.
Вводные:
Среднее время сборки Next.js: 1.5–3 минуты (для проектов среднего размера, по данным сообщества)
Частота деплоев: 3–10 в день (типично для стартапов и малых команд, DORA metrics)
Берём средние значения: 2 минуты билда × 5 деплоев/день
Без PM2 reload (stop → build → start):
Метрика | Расчёт | Итого |
|---|---|---|
Даунтайм за день | 2 мин × 5 деплоев | 10 минут |
Даунтайм за месяц | 10 мин × 22 рабочих дня | 220 мину�� (~3.7 часа) |
Даунтайм за год | 220 мин × 12 месяцев | 2 640 минут (44 часа) |
С PM2 reload:
Метрика | Расчёт | Итого |
|---|---|---|
Даунтайм за день | 0 сек × 5 деплоев | 0 |
Даунтайм за месяц | 0 | 0 |
Даунтайм за год | 0 | 0 |
Сэкономленное время: 44 часа в год. Почти 2 полных рабочих суток.
Теперь в деньгах. Для малого бизнеса стоимость минуты простоя — $137–427 (Pingdom). Для среднего — до $5 600/мин (Gartner/ITIC 2024)).
Размер бизнеса | Стоимость минуты | Потери за год (44ч = 2640 мин) |
|---|---|---|
Маленький стартап | $137 | $361 680 |
Малый бизнес | $427 | $1 127 280 |
Средний бизнес | $5 600 | $14 784 000 |
Разумеется, это максимальная оценка — не у каждого стартапа каждая минута простоя стоит $137. Но даже если ваш SaaS зарабатывает $1000/день, 10 минут даунтайма в день — это ~$7 потерянного дохода. $154/месяц. $1 848/год. За настройку, которая заняла 30 минут.
Стоимость внедрения: 0 рублей, 30 минут времени. PM2 бесплатный, изменение — 3 строки в CI/CD конфигурации.
PM2 cluster mode: основы {#cluster}
PM2 — менеджер процессов для Node.js, который умеет запускать приложение в нескольких экземплярах (cluster mode), используя встроенный модуль cluster Node.js.
Базовая конфигурация:
// ecosystem.config.cjs module.exports = { apps: [ { name: "my-app", script: "node_modules/next/dist/bin/next", args: "start -p 4000", cwd: "/root/my-app", instances: 4, // 4 воркера exec_mode: "cluster", // кластерный режим env: { NODE_ENV: "production", PORT: 4000, }, max_memory_restart: "512M", }, ], };
Что даёт cluster mode:
4 воркера обрабатывают запросы параллельно
PM2 работает как балансировщик нагрузки (round-robin)
Если один воркер упал — остальные продолжают работать
Можно перезапускать воркеры по одному — и вот тут начинается магия
Количество воркеров обычно равно числу ядер CPU. Для 2-ядерного VPS — 2-4 инстанса, для 4-ядерного — 4. Больше инстансов, чем ядер, обычно не имеет смысла.
Важно: Next.js отлично работает в cluster mode через PM2. Каждый воркер — независимый Node.js-процесс со своим
next start.
Главная ловушка: restart vs reload {#trap}
Это ключевое различие, которое решает всё:
Команда | Что делает | Даунтайм |
|---|---|---|
| Убивает все воркеры → поднимает заново | Да, есть |
| Поднимает новый воркер → убивает старый → повторяет | Нет |
pm2 reload выполняет rolling restart: перезапускает воркеры по одному. Пока новый воркер поднимается, старые продолжают обслуживать запросы. Когда новый готов — старый убивается, и PM2 переходит к следующему.
Визуализация для 4 воркеров:
Время → Worker 1: [████ старый ████][░░ перезапуск ░░][████ новый ████████████] Worker 2: [██████████ старый ██████████][░░ перезапуск ░░][████ новый ██] Worker 3: [████████████████ старый ████████████████][░░ перезапуск ░░][█] Worker 4: [████████████████████████ старый ████████████████████████][░░░] Запросы: ✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓✓ Ни одного 502 — всегда есть кому ответить
Для более надёжного rolling restart добавьте параметры ожидания в ecosystem config:
module.exports = { apps: [ { name: "my-app", script: "node_modules/next/dist/bin/next", args: "start -p 4000", instances: 4, exec_mode: "cluster", // Rolling restart настройки listen_timeout: 10000, // ждём до 10 сек, пока воркер поднимется kill_timeout: 5000, // даём 5 сек на graceful shutdown max_memory_restart: "512M", env: { NODE_ENV: "production", PORT: 4000, }, }, ], };
listen_timeout— сколько PM2 ждёт, пока новый воркер начнёт принимать подключения. Если Next.js стартует медленно (холодный кеш, прогрев SSR) — увеличьте до 15-20 секунд.kill_timeout— сколько времени даётся старому воркеру на завершение текущих запросов (graceful shutdown). Если у вас долгие API-запросы — увеличьте.
Вторая ловушка: rm -rf .next {#trap2}
Это грабли, на которые я наступил даже после перехода на pm2 reload. Деплой-скрипт выглядел так:
pnpm install --frozen-lockfile rm -rf .next # ← Вот эта строка pnpm build pm2 reload my-app --update-env
Казалось бы, всё правильно: чистим старый билд, собираем новый, делаем reload. Но вот что происходит:
rm -rf .next— удаляет скомпилированные файлыpnpm build— начинает сборку (1-2 минуты)Старые воркеры всё ещё работают и пытаются отдавать страницы из
.next/.next/не существует → 500/502 на каждый запросpm2 reload— перезапуск, но пользователи уже ушли
Это коварный баг: вы настроили rolling restart, но rm -rf превращает его в полный даунтайм ещё ДО перезапуска.
Решение: не удалять .next. Команда next build перезаписывает содержимое .next/ поверх существующих файлов. Старые воркеры продолжают работать с предыдущей версией, а после pm2 reload подхватывают новую.
pnpm install --frozen-lockfile # БЕЗ rm -rf .next! pnpm build pm2 reload my-app --update-env
Примечание: В теории,
next buildможет не удалить устаревшие файлы из предыдущей сборки, и.nextбудет расти. На практике за месяцы работы это ни разу не стало проблемой — разница в размере директории минимальна. Если хотите перестраховаться, добавьте периодическую полную пересборку (например, раз в неделю сrm -rf .next), запланированную на ночь.
Рабочий рецепт: ecosystem.config + CI/CD {#recipe}
Итоговый ecosystem config:
// ecosystem.config.cjs module.exports = { apps: [ { name: "my-app", script: "node_modules/next/dist/bin/next", args: "start -p 4000", cwd: "/root/my-app", instances: 4, exec_mode: "cluster", listen_timeout: 10000, kill_timeout: 5000, max_memory_restart: "512M", log_date_format: "YYYY-MM-DD HH:mm:ss", merge_logs: true, env: { NODE_ENV: "production", PORT: 4000, }, }, ], };
Итоговый GitHub Actions workflow:
name: Deploy Frontend on: push: branches: [main] paths: - 'src/**' - 'package.json' jobs: deploy: runs-on: ubuntu-latest steps: - name: Deploy to server uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_HOST }} username: root key: ${{ secrets.SERVER_SSH_KEY }} command_timeout: 5m script: | set -e cd /root/my-app git fetch --force origin main git reset --hard origin/main pnpm install --frozen-lockfile # Сборка поверх — старые воркеры продолжают работать NODE_OPTIONS="--max-old-space-size=1536" pnpm build # Rolling reload: воркеры перезапускаются по одному pm2 reload my-app --update-env sleep 3 pm2 list | grep my-app
Что изменилось:
Нет
pm2 stop— воркеры работают во время сборкиНет
rm -rf .next— сборка перезаписывает файлы поверхpm2 reloadвместоrestart— rolling restart без даунтайма--update-env— подхватывает обновлённые переменные окружения
Результат: 0 секунд даунтайма. Пользователи не замечают деплой.
Проверка работоспособности
После настройки стоит убедиться, что всё работает:
# Посмотреть статус воркеров pm2 list # Наблюдать за reload в реальном времени pm2 logs my-app --lines 20 # Проверить, что reload действительно rolling pm2 reload my-app && watch -n 0.5 'pm2 jlist | python3 -c " import sys, json for p in json.load(sys.stdin): print(f\"{p[\"name\"]}:{p[\"pm_id\"]} uptime={p[\"pm2_env\"][\"pm_uptime\"]} status={p[\"pm2_env\"][\"status\"]}\") "'
PM2 vs Kubernetes: честное сравнение {#k8s}
«А почему не Kubernetes?» — закономерный вопрос. Давайте разберёмся.
Что даёт Kubernetes
Kubernetes решает ту же задачу (zero-downtime deployment) через rolling update стратегию. Процесс похож:
# deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: my-app spec: replicas: 4 strategy: type: RollingUpdate rollingUpdate: maxUnavailable: 1 maxSurge: 1 template: spec: containers: - name: my-app image: my-registry/my-app:latest readinessProbe: httpGet: path: /api/health port: 4000 initialDelaySeconds: 5 periodSeconds: 5
Kubernetes делает rolling update «из коробки», с readiness probes и автоматическим откатом.
Сравнительная таблица
Параметр | PM2 + VPS | Kubernetes |
|---|---|---|
Время настройки | 30 минут | 1-3 дня (с нуля) |
Стоимость инфра | $5-20/мес (VPS) | $50-150/мес (managed K8s) |
Zero-downtime |
| Rolling update (встроено) |
Автоскейлинг | Нет (ручной) | Да (HPA) |
Self-healing | Перезапуск процесса | Перезапуск пода + ноды |
Мультисервер | Сложно | Нативно |
Кривая обучения | Пологая | Крутая |
Файлы конфигурации | 2 (ecosystem + workflow) | 5-10 (deployment, service, ingress, ...) |
Мониторинг |
| Prometheus + Grafana |
Rollback |
|
|
Сложность отладки | SSH + логи | kubectl logs + port-forward |
Когда PM2 — правильный выбор
1 сервер, 1-3 приложения
Команда из 1-3 человек
Бюджет ограничен
Нет выделенного DevOps-инженера
Быстрый старт важнее масштабируемости
Как отметил один разработчик на Hacker News: «Docker/Kubernetes — это нормально для вещей, которые приносят деньги, но для небольших проектов это слишком много сложности. PM2 справляется с меньшими усилиями».
А HalodocTech делится интересным наблюдением: «Частые рестарты подов в Kubernetes при DDoS или пиках трафика занимали ~2 минуты каждый, что влияло на аптайм. PM2 cluster mode оказался стабильнее для нашего кейса».
Когда пора в Kubernetes
Микросервисная архитектура (5+ сервисов)
Нужен автоскейлинг под нагрузку
Несколько серверов / зон доступности
Есть DevOps-инженер или готовность его нанять
Бюджет позволяет managed Kubernetes ($50-150/мес за GKE/EKS/AKS)
Промежуточный вариант: Docker без Kubernetes
Если PM2 стало мало, а Kubernetes — слишком сложно, есть промежуточный путь:
FROM node:20-alpine WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN npm install -g pnpm && pnpm install --frozen-lockfile COPY . . RUN pnpm build CMD ["pnpm", "start"]
# Сборка и перезапуск контейнера docker build -t my-app:new . docker stop my-app-old docker run -d --name my-app-new -p 4000:4000 my-app:new
Или Docker Compose с blue-green deployment через Nginx upstream переключение. Но это уже тема для отдельной статьи.
Заключение {#conclusion}
Итого, три правила zero-downtime деплоя с PM2:
Cluster mode — запускайте несколько воркеров (
instances: 4)reloadвместоrestart— воркеры перезапускаются по одномуНе удаляйте
.nextперед сборкой — старые воркеры должны иметь файлы для отдачи
Весь путь от «502 при каждом деплое» до «пользователи не замечают обновления» занял у меня один вечер и изменение трёх строк в CI/CD конфигурации. Не нужен Kubernetes, не нужен Docker, не нужен Nginx с upstream switching — достаточно правильно настроенного PM2.
Для небольших проектов на одном сервере PM2 — это разумный минимум, который закрывает 90% потребностей в процесс-менеджменте и zero-downtime деплое. Когда проект перерастёт один сервер — тогда и стоит смотреть в сторону контейнеризации и оркестрации.
Полезные ссылки
Хабы: DevOps, Node.js, Системное администрирование, Серверное администрирование
Время чтения: ~10 мин
Тип: Кейс / How-to
Если было полезно — ставьте плюс. Если у вас свой рецепт zero-downtime без Kubernetes — пишите в комментариях, интересно сравнить подходы.
