Каждый деплой — рулетка: повезёт или 502?
Каждый деплой — рулетка: повезёт или 502?

Каждый пуш в 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?

Оглавление

  1. Масштаб проблемы: не только у вас

  2. Боль: что происходит при «наивном» деплое

  3. Цена простоя: почему 502 — это не просто неудобство

  4. PM2 cluster mode: основы

  5. Главная ловушка: restart vs reload

  6. Вторая ловушка: rm -rf .next

  7. Рабочий рецепт: ecosystem.config + CI/CD

  8. PM2 vs Kubernetes: честное сравнение

  9. Заключение

Масштаб проблемы: не только у вас {#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):

Stack Overflow:

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

Боль: что происходит при «наивном» деплое {#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

Четыре строчки, три проблемы:

  1. pm2 stop — убивает все воркеры до начала сборки

  2. rm -rf .next — удаляет скомпилированные файлы, пока старые процессы (если бы они работали) пытаются их отдать

  3. 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 restart

Убивает все воркеры → поднимает заново

Да, есть

pm2 reload

Поднимает новый воркер → убивает старый → повторяет

Нет

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. Но вот что происходит:

  1. rm -rf .next — удаляет скомпилированные файлы

  2. pnpm build — начинает сборку (1-2 минуты)

  3. Старые воркеры всё ещё работают и пытаются отдавать страницы из .next/

  4. .next/ не существует → 500/502 на каждый запрос

  5. 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

pm2 reload

Rolling update (встроено)

Автоскейлинг

Нет (ручной)

Да (HPA)

Self-healing

Перезапуск процесса

Перезапуск пода + ноды

Мультисервер

Сложно

Нативно

Кривая обучения

Пологая

Крутая

Файлы конфигурации

2 (ecosystem + workflow)

5-10 (deployment, service, ingress, ...)

Мониторинг

pm2 monit

Prometheus + Grafana

Rollback

git revert + редеплой

kubectl rollout undo

Сложность отладки

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:

  1. Cluster mode — запускайте несколько воркеров (instances: 4)

  2. reload вместо restart — воркеры перезапускаются по одному

  3. Не удаляйте .next перед сборкой — старые воркеры должны иметь файлы для отдачи

Весь путь от «502 при каждом деплое» до «пользователи не замечают обновления» занял у меня один вечер и изменение трёх строк в CI/CD конфигурации. Не нужен Kubernetes, не нужен Docker, не нужен Nginx с upstream switching — достаточно правильно настроенного PM2.

Для небольших проектов на одном сервере PM2 — это разумный минимум, который закрывает 90% потребностей в процесс-менеджменте и zero-downtime деплое. Когда проект перерастёт один сервер — тогда и стоит смотреть в сторону контейнеризации и оркестрации.


Полезные ссылки


Хабы: DevOps, Node.js, Системное администрирование, Серверное администрирование
Время чтения: ~10 мин
Тип: Кейс / How-to


Если было полезно — ставьте плюс. Если у вас свой рецепт zero-downtime без Kubernetes — пишите в комментариях, интересно сравнить подходы.