Перейти до вмісту
Telegram[email protected]

Деплой без простою на одному сервері: що насправді потрібно

Євгеній28 липня 2026 р.9 хв читання

Більшість статей про деплой без простою починаються з кластера на п’ять нод. Але 90% продуктів живуть на одному сервері з docker compose, і саме там простій болить найбільше — бо нікому підхопити трафік. Хороша новина: на одному сервері zero-downtime теж досяжний, і головна робота там не в оркестраторі, а в тому, як ви пишете міграції.

Чому падає навіть простий деплой

Класичний сценарій: docker compose up -d --build. Старий контейнер зупиняється, новий піднімається за 8–20 секунд, поки Nuxt прогріває свій кеш. Усі запити в цьому вікні отримують 502 від nginx. Якщо в цей момент користувач відправляв форму — заявка втрачена, і ви про це ніколи не дізнаєтесь.

Друга причина гірша: міграція. Ви перейменували колонку, застосували міграцію, і поки новий образ збирається, старий код продовжує писати в колонку, якої вже немає. Помилки в логах, дані втрачені.

Міграції, сумісні в обидва боки

Це найважливіша частина, і вона не про інфраструктуру. Правило одне: міграція має працювати і зі старим кодом, і з новим. З цього випливає expand/contract у три релізи:

  1. Expand. Додаєте нову колонку, nullable, з дефолтом. Старий код її не бачить і не ламається. Новий код пише в обидві.
  2. Migrate. Бекфіл даних фоновою задачею, не в міграції. Мільйон рядків у міграції — це блокування таблиці й той самий простій, тільки під іншою назвою.
  3. Contract. Наступним релізом прибираєте стару колонку, коли жоден інстанс уже її не читає.

Три деплої замість одного здаються надмірними, поки ви вперше не відкотите реліз о другій ночі. Перейменування колонки одним кроком — це деплой, який неможливо відкотити: міграцію вниз ніхто ніколи не тестував.

Міграція, яку не можна відкотити, — це не міграція, а односторонні двері. Проходьте в них свідомо, а не випадково.

Правильний порядок кроків

Порядок має значення більше, ніж інструмент:

1. build       зібрати новий образ (старий продовжує обслуговувати трафік)
2. migrate     застосувати сумісні міграції
3. start       підняти новий контейнер поруч зі старим
4. wait        чекати, поки /health/ready віддасть 200
5. switch      перемкнути nginx upstream
6. drain       дати старому 10 секунд дообслужити запити
7. stop        зупинити старий контейнер

Кроки 3–5 — це те, що docker compose сам не робить. Найпростіший спосіб отримати їх без Kubernetes: два сервіси в compose (app_blue і app_green) і nginx, який дивиться на upstream з файлу, що підмінюється й перечитується через nginx -s reload. Reload у nginx не рве наявні з’єднання — старі воркери дообслуговують, нові беруть нову конфігурацію.

Health-check, який каже правду

Найпоширеніша помилка — health-check, який повертає 200 одразу після старту процесу. Тоді ви перемикаєте трафік на контейнер, який ще не має з’єднання з базою, і отримуєте той самий простій, тільки тепер він виглядає як 500 замість 502.

Розділяйте два ендпоїнти:

  • /health/live — процес живий. Якщо не відповідає, контейнер треба перезапустити.
  • /health/ready — з’єднання з базою є, Redis відповідає, черга доступна. Якщо не відповідає, контейнер треба прибрати з балансувальника, але не перезапускати.

Плутанина між ними — причина каскадних падінь: база гальмує, readiness падає, оркестратор перезапускає всі контейнери, і при старті вони одночасно б’ють у базу, яка вже й так гальмувала.

Прогрів

SSR-застосунок після старту компілює маршрути на першому запиті, а кеш контенту порожній. Перший користувач після деплою отримує сторінку за 3 секунди замість 300 мілісекунд. Формально простою немає — фактично сайт повільний рівно тоді, коли на нього дивляться.

Лікується це десятьма рядками: після того як контейнер відповів на readiness, але до перемикання трафіку, зробіть кілька запитів до найважливіших сторінок і до ендпоїнтів, які наповнюють кеш. Прогрів кешу — це частина деплою, а не оптимізація на потім.

Відкат, який справді працює

Відкат — це не «повернути попередній образ». Це «повернути попередній образ і переконатися, що він працює з поточною схемою бази». Саме тому expand/contract важливіший за скрипт деплою: якщо міграції сумісні в обидва боки, відкат — це одна команда. Якщо ні — це відновлення з бекапу.

Мінімум, який варто мати: тег образу з SHA коміту (не latest), збережений попередній тег, і смоук-тест після перемикання. Якщо смоук-тест червоний — перемикаємо upstream назад, поки старий контейнер ще живий. Тому крок 7 і стоїть в кінці.

Що зробити цього тижня

  • Розділіть liveness і readiness, якщо у вас один /health.
  • Перевірте останні п’ять міграцій: чи пережив би їх старий код?
  • Додайте прогрів після старту й смоук-тест після перемикання.
  • Замініть latest на тег із SHA коміту.

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

Головне
  • Zero-downtime визначається міграціями, а не оркестратором
  • Expand / migrate / contract — три релізи замість одного ризикованого
  • Liveness і readiness — різні ендпоїнти з різними наслідками
  • Прогрів кешу — частина деплою, інакше перший користувач бачить повільний сайт
Коротко — питання й відповіді
Чи можна досягти нуля простою на одному сервері?

Так. Потрібні два контейнери, які працюють паралельно кілька секунд, nginx з reload замість restart і readiness-check, який чекає на базу. Оркестратор дає це «з коробки», але сам механізм не залежить від нього.

Скільки часу займає налаштування?

Скрипт деплою з blue/green і прогрівом — приблизно день роботи. Привести міграції до expand/contract — це зміна звички команди, а не разова задача: перші кілька релізів здаються повільнішими, далі це стає нормою.

Євгеній
Фронтенд і фулстек, інтерфейси, продуктова розробка
Потрібен такий пайплайн