Оновлення Docker Compose без простоїв: міф, реальність і робочі патерни

Було корисно?

У вас невеликий «продакшн» стек на Docker Compose. Він працював місяцями. Потім рутинне оновлення перетворюється на двохвилинний простій,
в чаті з’являються «він впав?», і ви дивитеся на docker compose up -d, ніби це особисте зрада.

Compose цілком здатний запускати реальні робочі навантаження. Але «оновлення без простоїв на Compose» — це не включення якоїсь функції, це набір
операційних виборів, які ви реалізуєте, тестуєте і іноді жалкуєте про них. Давайте відокремимо маркетинговий міф від інженерної реальності, а потім
побудуємо патерни, які реально витримають, коли клієнти підключені, запити в польоті, а база даних має свої емоції.

Міф проти реальності: що Compose може і не може

Міф: «Compose робить rolling updates»

Docker Compose сам по собі не є оркестратором. Він не виконує рідною мірою rolling updates із врахуванням дренування трафіку, сервіс-дискавері
між хостами або автоматичною заміною при збоях. Коли ви запускаєте docker compose up -d, Compose погоджує бажаний стан на одному хості.
Він може перезапустити контейнери. Він може зупиняти й запускати їх. Але він не координує «зберегти старе, запустити нове, перенаправити трафік,
потім вивести старе», якщо ви не побудували ці механізми навколо нього.

Реальність: «Compose може наблизити до нульового простою з проксі і дисципліною»

Ви можете наблизитися до нульового простою для багатьох веб-навантажень. Хитрість — перестати вважати «контейнер додатку» власником сокета.
Ваш стабільний вхідний пункт має бути зворотним проксі (або L4-проксі), який залишається працювати, поки ви підміняєте бекенди за ним. Далі додаєте:

  • Healthchecks, що відображають готовність, а не лише життєздатність.
  • Акуратне завершення (сигнали зупинки та таймаути), щоб завершити запити в польоті.
  • Метод деплою, який запускає нові інстанси перед зупинкою старих.
  • Дисципліну у змінах бази даних (expand/contract, сумісність назад, уникати «зупинки світу» при міграціях).

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

Одна цитата, яку варто тримати в голові: Надія — не стратегія — часто атрибується колам надійності; ставтеся до неї як до переказаної думки,
а не як до судового доказу.

Цікаві факти та історичний контекст (чому це заплутано)

  1. Compose починався як «Fig» (2014). Він був спроектований для робочих процесів розробника, а не для production‑деплойментів із SLO.
  2. Docker пізніше представив «Swarm mode» з rolling updates, перепризначенням за health та абстракціями сервісів — функціями, які сам Compose
    так і не отримав.
  3. depends_on ніколи не означало «чекати готовності». Це порядок запуску, а не блокування за готовністю. Це непорозуміння
    спричинило більше «працює на моєму ноуті» інцидентів, ніж багато хто визнає.
  4. Healthchecks з’явилися пізніше (епоха Docker 1.12). Раніше люди використовували «sleep 10» як стратегію готовності. Це й досі популярно,
    з причин, які переважно психологічні.
  5. Політики перезапуску — не оркестрація. restart: always — це ремінь безпеки, а не автопілот.
  6. Nginx довгий час підтримує graceful reload — головна причина, чому він став типовими «вхідними дверима» для DIY деплоїв без простоїв.
  7. Linux додав SO_REUSEPORT (близько 2013 року в масовому використанні), що дозволяє кільком процесам прив’язати той самий порт у деяких схемах,
    але це не вирішує магічно координацію деплоїв для контейнерів.
  8. Blue/green деплоймент існував до контейнерів. Операційні команди робили це з парами віртуальних машин і балансувальниками ще до популярності Docker.
  9. Міграції баз даних — справжня фабрика простоїв. Контейнери додатків — прості; зміни схем із ексклюзивними блокуваннями — там, де мрії вмирають.

Визначте «нульовий простій» по‑дорослому

«Нульовий простій» означає зовсім різні речі залежно від того, хто потіє. Визначте це перед будь‑якими змінами. Ось поширені варіанти:

  • Жодного простою прийому TCP: порт ніколи не перестає приймати з’єднання. Клієнти все одно можуть бачити помилки, якщо бекенд не готовий.
  • Жодного спайку 5xx: запити продовжують успішно виконуватися. Допустимо деяке збільшення затримки.
  • Жодного видимого для користувача переривання: сесії зберігаються, websocket виживають, довгі опитування тривають. Це складніше, ніж здається.
  • Не витрачати бюджет помилок через деплой: зміни можуть викликати проблеми, але процес деплою — ні.

Для стеків на Compose реалістичною ціллю зазвичай є: відсутність спайка 5xx і відсутність простою на публічній точці входу, з обмеженим
збільшенням затримки під час підміни. Якщо ви обслуговуєте websocket, переозначте успіх: ви можете бути «без простоїв» і все ж розірвати з’єднання клієнтів,
якщо не реалізуєте явне дренування з’єднань і стіккінг роутингу.

Жарт №1: Якщо хтось каже, що має «істинний нульовий простій» на Compose з одним контейнером, запитайте, як вони подорожують у часі.

Режими відмов, які ви насправді зустрічали в продакшні

1) Прив’язка порту — одинична точка болю

Якщо ваш контейнер програми прив’язує 0.0.0.0:443 на хості, ви не зможете запустити нову версію, поки стара не звільнить порт.
Це створює розрив. Навіть 200 мс — це розрив. Під навантаженням клієнти погано відкочуються, ретраї падають каскадом, і раптом «мілісекунди»
перетворюються на «чому у нас не пройшла оплата».

2) «Контейнер запущено» ≠ «сервіс готовий»

Багато додатків швидко запускають процес, а потім 5–30 секунд роблять міграції, прогрівають кеші або чекають залежностей. Якщо проксі спрямовує
трафік рано, ви віддасте помилки. Якщо блокуєте маршрутизацію до готовності — буде добре, за умови, що готовність вимірюється правильно.

3) Обробка SIGTERM не опціональна

Docker спочатку надішле SIGTERM, а потім SIGKILL після таймауту зупинки. Якщо ваш додаток ігнорує SIGTERM, ви втратите запити в польоті.
Якщо stop timeout занадто короткий, ви також обріжете запити в процесі. Якщо ви припиняєте проксі першим — ви втратите все.

4) Міграції бази даних, що блокують таблиці

Звичайний простій під час Compose‑деплою — не вина Docker. Це міграція, що бере ексклюзивний лок або перезаписує стовпець, або побудова індексу без
конкурентності. Додаток перестає відповідати, healthcheck падає, Compose перезапускає його — і ви отримуєте самостворений DOS.

5) Станові сервіси за «бездержавними» патернами

Ви можете зробити blue/green для веб‑рівня. Ви не можете легко blue/green для одноінстанційної бази даних простим «запустити інший контейнер»,
якщо ваші історія зберігання, реплікації та фейловера не зрілі. Compose може запускати Postgres. Compose — не рішення для HA Postgres.

6) Тихий вбивця: connection pool і застарілий DNS

Якщо ви ротуєте бекенди, змінюючи IP контейнера й очікуючи, що клієнти «автоматично перепідключаться», ви виявите, що connection pool‑и й кешування DNS
мають власну думку. Деякі драйвери кешують резольв довше, ніж ви очікуєте. Деякі додатки взагалі не перепідключаються, якщо їх не перезавантажити.
Ось чому стабільні імена сервісів (проксі‑фронтєнди) мають значення.

Робочі патерни для майже нульового простою на Compose

Патерн A: Стабільний зворотний проксі + версійовані сервіси додатку (blue/green‑подібно)

Це патерн, який я рекомендую найчастіше, бо він відповідає сильним сторонам Compose: простий, локальний, детермінований. Ви тримаєте стабільний
проксі-контейнер, прив’язаний до портів хоста (80/443). Ваш додаток працює за ним у user-defined мережі. Під час деплою ви запускаєте новий сервіс
додатку (green) поруч зі старим (blue), перевіряєте готовність, потім переключаєте upstream у проксі і акуратно виводите старе.

Ключові характеристики:

  • Проксі — єдине, що прив’язується до портів хоста.
  • Обидві версії додатку можуть одночасно працювати в внутрішній мережі.
  • Перемикання трафіку — це перезавантаження конфігу, а не пересоздання контейнера.
  • Відкат — перемикання проксі назад і зупинка поганої версії.

Проксі можна реалізувати на Nginx, HAProxy, Traefik, Caddy. Виберіть те, чим ви вмієте оперувати. «Оперувати» означає: ви вмієте безпечно його перезавантажувати,
читати логи і пояснити, що станеться, коли бекенд впаде.

Патерн B: Масштабування з --scale + дренаж + пересоздання (обмежений rolling)

Compose підтримує масштабування сервісу в кілька реплік на одному хості. Це саме по собі не дає rolling updates, але дає простір для маневру:
підніміть додаткові репліки нової версії, маршрутизуй трафік до них, потім прибирай старі репліки. Обмеження в тому, що Compose не реалізує
«порядок оновлення» нативно так, як оркестратор. Ви пишете плейбук.

Це найкраще працює коли:

  • Ваш додаток бездержавний або стан сесії зовнішній.
  • Ваш проксі/балансувальник може визначати здоров’я бекендів і перестати маршрутизувати.
  • Вам підходить ручна послідовність дій або невеликий скрипт деплою.

Патерн C: Socket activation / володіння портом на рівні хоста (просунуто, гострі грані)

Якщо ви наполягаєте на уникненні контейнерного проксі, можна дозволити systemd володіти сокетом і передавати його поточному інстансу додатку (socket
activation). Це може працювати. Але також це може стати «артізанським» генератором простоїв, якщо ваш додаток не підтримує це чисто або ви не тестуєте
поведінку перезавантаження під навантаженням.

Для більшості команд, що використовують Compose, стабільний проксі — це золота середина. Менше хитромудрості. Більше надійності.

Патерн D: «Нудні міграції» + деплой додатку (прихована вимога)

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

  • Expand/contract зміни схеми (спочатку додати нові колонки/таблиці, деплоїти код, потім видаляти старе).
  • Читання та запис сумісні в обох версіях під час переходу.
  • Побудова індексів онлайн, де можливо.
  • Фіч-флаги, коли зміна не може бути миттєвою.

Compose не заважає вам робити це. Compose також не нагадує вам. Ви нагадуєте собі.

Жарт №2: Міграція з ексклюзивним локом — єдина річ, яка може вивести ваш додаток з ладу швидше, ніж ваш CEO, що «допомагає з деплоєм».

Практичні завдання: команди, виводи та рішення (12+)

Ось перевірки, які я насправді виконую на хості Compose, коли намагаюся зробити «нульовий простій» реальністю. Кожне завдання включає: команду, типовий
вивід, що це означає і яке рішення слідує.

Завдання 1: Підтвердити, що Compose вважає запущеним

cr0x@server:~$ docker compose ps
NAME                      IMAGE                     COMMAND                  SERVICE   CREATED        STATUS                  PORTS
prod-proxy-1              nginx:1.25-alpine          "/docker-entrypoint.…"   proxy     2 weeks ago     Up 2 weeks              0.0.0.0:80->80/tcp, 0.0.0.0:443->443/tcp
prod-app-blue-1           registry/app:1.9.3         "/app/start"             app-blue  2 weeks ago     Up 2 weeks (healthy)
prod-app-green-1          registry/app:1.9.4         "/app/start"             app-green  3 minutes ago   Up 3 minutes (healthy)

Що це означає: У вас дві версії додатку в мережі та стабільний проксі.
Рішення: Якщо green здоровий — готові перемикати трафік, змінюючи upstream в проксі, а не зупиняючи blue.

Завдання 2: Перевірити статус healthcheck і час

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' prod-app-green-1
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-01-03T10:12:01.123Z","End":"2026-01-03T10:12:01.187Z","ExitCode":0,"Output":"ok\n"}]}

Що це означає: Контейнер повідомляє про готовність (за умови, що ваш healthcheck має значення).
Рішення: Якщо статус starting або unhealthy, не перемикайте трафік. Виправте готовність або старт додатку перш ніж рухатись далі.

Завдання 3: Переконатися, що проксі бачить обидва бекенди і який активний

cr0x@server:~$ docker exec -it prod-proxy-1 nginx -T | sed -n '1,120p'
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
# configuration content:
upstream app_upstream {
    server prod-app-blue-1:8080 max_fails=2 fail_timeout=5s;
    server prod-app-green-1:8080 max_fails=2 fail_timeout=5s;
}
server {
    listen 80;
    location / {
        proxy_pass http://app_upstream;
    }
}

Що це означає: Проксі налаштований балансувати навантаження між обома.
Рішення: Якщо вам потрібен строгий відріз (без балансування), налаштуйте ваги або переключіть на єдину ціль upstream для відрізу.

Завдання 4: Перевірити, що перезавантаження Nginx — graceful (без загублених воркерів)

cr0x@server:~$ docker exec -it prod-proxy-1 nginx -s reload && docker exec -it prod-proxy-1 tail -n 5 /var/log/nginx/error.log
2026/01/03 10:15:22 [notice] 1#1: signal process started
2026/01/03 10:15:22 [notice] 1#1: reconfiguring
2026/01/03 10:15:22 [notice] 1#1: using the "epoll" event method

Що це означає: Reload пройшов успішно; Nginx не перезапустився з нуля.
Рішення: Якщо reload видає помилки — не деплойте. Виправте генерацію конфігів і запустіть nginx -t перед reload.

Завдання 5: Підтвердити stop timeout і поведінку на сигнали (грейсфул shutdown)

cr0x@server:~$ docker inspect --format 'StopSignal={{.Config.StopSignal}} StopTimeout={{.Config.StopTimeout}}' prod-app-blue-1
StopSignal=SIGTERM StopTimeout=30

Що це означає: Docker надішле SIGTERM, потім чекатиме 30s перед SIGKILL.
Рішення: Якщо вашому додатку потрібно 60s для дрену — встановіть stop_grace_period: 60s. Якщо він ігнорує SIGTERM — виправте додаток. Жодний YAML вас не врятує.

Завдання 6: Спостерігати з’єднання в польоті під час вікна дрену

cr0x@server:~$ docker exec -it prod-app-blue-1 ss -Hnt state established '( sport = :8080 )' | wc -l
47

Що це означає: 47 встановлених TCP-з’єднань до додатку.
Рішення: Якщо кількість не падає після видалення бекенда з проксі, у вас можуть бути довгоживучі з’єднання (websocket) і потрібен довший план дрену.

Завдання 7: Підтвердити, який контейнер справді приймає трафік

cr0x@server:~$ docker logs --since=2m prod-app-green-1 | tail -n 5
10.0.2.5 - - [03/Jan/2026:10:16:01 +0000] "GET /healthz HTTP/1.1" 200 2 "-" "kube-probe/1.0"
10.0.2.5 - - [03/Jan/2026:10:16:04 +0000] "GET /api/orders HTTP/1.1" 200 431 "-" "Mozilla/5.0"

Що це означає: Green приймає реальні запити.
Рішення: Якщо до green надходять лише health check‑и — ви ще не переключили продакшн‑трафік; змініть маршрутизацію або ваги в проксі свідомо.

Завдання 8: Виявити, чи випадково ви пересоздаєте проксі (генератор простоїв)

cr0x@server:~$ docker compose up -d --no-deps proxy
[+] Running 1/0
 ✔ Container prod-proxy-1  Running

Що це означає: Compose не пересоздав контейнер проксі.
Рішення: Якщо тут виводиться «Recreated», ви перезапускаєте вхідні двері. Припиніть. Фіксуйте зміни конфігу проксі і робіть reload всередині контейнера.

Завдання 9: Порівняти digest образів (уникайте сюрпризів з :latest)

cr0x@server:~$ docker images --digests registry/app | head -n 5
REPOSITORY     TAG     DIGEST                                                                    IMAGE ID       CREATED        SIZE
registry/app   1.9.4   sha256:8b9a2f6d3c1e8f...                                                   4a1f2c3d4e5f   2 days ago     156MB
registry/app   1.9.3   sha256:1c2d3e4f5a6b7c...                                                   7b6a5c4d3e2f   2 weeks ago    155MB

Що це означає: Ви можете точно ідентифікувати, що розгорнуто.
Рішення: Якщо ви використовуєте :latest без digest‑ів — припиніть так робити. Тегуйте релізи й майте відомий rollback‑образ.

Завдання 10: Перевірити події Docker під час деплою (хто що перезапустив)

cr0x@server:~$ docker events --since 10m --until 0m | tail -n 12
2026-01-03T10:12:10.000000000Z container start 2f1a... (name=prod-app-green-1, image=registry/app:1.9.4)
2026-01-03T10:14:02.000000000Z container health_status: healthy 2f1a... (name=prod-app-green-1)
2026-01-03T10:15:22.000000000Z container exec_start: nginx -s reload 9aa2... (name=prod-proxy-1)

Що це означає: Хронологія того, що насправді сталося, а не того, що ви пам’ятаєте.
Рішення: Якщо під час деплою бачите події перезапуску/пересоздання проксі — у вашій історії «нульового простою» є діра.

Завдання 11: Перевірити тиск ресурсів на рівні ядра (CPU throttling і IO wait)

cr0x@server:~$ vmstat 1 5
procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 2  0      0  51200  21000 320000    0    0   120    80  600 1200 25 10 55 10  0
 3  1      0  48000  20500 318000    0    0  2200   300  900 1700 30 12 38 20  0

Що це означає: Другий зразок показує вищий IO wait (wa) і заблоковані процеси (b).
Рішення: Якщо IO wait стрибає під час деплою (pull образів, міграції) — плануйте деплой в поза піковий період, перемістіть образи в локальний регістр/кеш або виправляйте продуктивність сховища.

Завдання 12: Визначити, який процес володіє портом хоста (зловити випадкові binds)

cr0x@server:~$ sudo ss -Htlpn '( sport = :443 )'
LISTEN 0 511 0.0.0.0:443 0.0.0.0:* users:(("docker-proxy",pid=2143,fd=4))

Що це означає: Docker proxy володіє опублікованим портом. Якщо контейнер пересоздасться, бінд буде флапати.
Рішення: Тримайте стабільний проксі-контейнер. Не публікуйте порти додатків напряму, якщо хочете безшовні підміни.

Завдання 13: Підтвердити підключення до мереж (чи проксі в тій мережі?)

cr0x@server:~$ docker inspect --format '{{range .NetworkSettings.Networks}}{{.NetworkID}} {{end}}' prod-proxy-1
a8c1f0d3e2b1c4d5e6f7a8b9c0d1e2f3

Що це означає: Проксі принаймні в одній user-defined мережі.
Рішення: Якщо проксі і додаток не в одній мережі, резольвінг імен як prod-app-green-1 не працюватиме. Виправте мережі перед тим, як звинувачувати Docker.

Завдання 14: Перевірити блокування бази під час міграції (курильна зброя простою)

cr0x@server:~$ docker exec -it prod-db-1 psql -U postgres -d app -c "select pid, wait_event_type, wait_event, state, query from pg_stat_activity where state <> 'idle' order by pid;"
 pid  | wait_event_type |  wait_event   | state  |                 query
------+-----------------+---------------+--------+----------------------------------------
 2412 | Lock            | relation      | active | ALTER TABLE orders ADD COLUMN foo text;
 2550 | Lock            | transactionid | active | UPDATE orders SET foo = 'x' WHERE ...

Що це означає: Активні сесії чекають локів. Ваш «простій при деплої» може бути спричинений блокуванням схеми.
Рішення: Зупиніть міграцію, якщо вона небезпечна, або переробіть її (онлайн‑підхід, батчі, конкурентні індекси). Не перезапускайте додатки в надії, що це вирішить проблему.

Завдання 15: Підтвердити, що Compose не змінив контейнери через дрейф конфігу

cr0x@server:~$ docker compose config | sed -n '1,120p'
name: prod
services:
  app-blue:
    image: registry/app:1.9.3
    healthcheck:
      test:
      - CMD
      - /bin/sh
      - -c
      - curl -fsS http://localhost:8080/ready || exit 1
      interval: 5s
      timeout: 2s
      retries: 12
    stop_grace_period: 60s
  proxy:
    image: nginx:1.25-alpine
    ports:
    - mode: ingress
      target: 80
      published: "80"
      protocol: tcp
    - mode: ingress
      target: 443
      published: "443"
      protocol: tcp

Що це означає: Це остаточний відрендерений конфіг, який застосовує Compose.
Рішення: Якщо вивід config відрізняється від того, що ви думаєте, — виправте управління змінними оточення і зафіксуйте значення. «Сюрприз‑конфіг» — найкращий друг простоїв.

Три корпоративні міні‑історії з передової

Міні‑історія 1: Інцидент через неправильне припущення («depends_on означає готовність»)

Середня SaaS‑команда запускала продакшн‑стек на одному VM з Docker Compose. Це був свідомий вибір: менше рухомих частин, трафік вміщався на одному хості.
Система мала API, воркер, Redis і Postgres.

Вони зробили рутинне оновлення: новий образ API, невелика міграція, рестарт. Плейбук деплою був один рядок: docker compose up -d.
Вони припустили, що depends_on означає, що API чекатиме Postgres. Не означало.

Postgres стартував довше, ніж зазвичай, бо VM також вантажив образи й робив запис на диск. API стартував, не зміг підключитись, вийшов, перезапустився, знову впав.
Політика перезапуску перетворила повільний старт у швидкий цикл. Тим часом зворотний проксі маршрутизував трафік на флапаючий бекенд. Користувачі бачили переривчасті 502 кілька хвилин — достатньо, щоб з’явились тікети в сапорт і гра в «хто винен».

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

Урок був неприємно нудний: Compose впорядковує запуск контейнерів; він не гарантує готовність залежностей. Вони перестали вважати «запущено» рівнозначним «готово», і деплой перестав бути драмою.

Міні‑історія 2: Оптимізація, що повернулася бумерангом (швидкі деплоя, повільні диски)

Інша організація запускала Compose на потужному хості з NVMe — поки закупівля не замінила його на «подібний» сервер з достатнім CPU і RAM, але посереднім сховищем.
Команда оптимізувала деплоя, завжди підвантажуючи свіжі образи прямо перед оновленням. Швидші релізи, менше розходжень з CI. На папері.

Під навантаженням деплой підвантажив великий набір шарів образів, наситив IO. Postgres чекпоінтинг сповільнився. Латентність API зросла. Healthcheck‑и почали таймаутитись.
Проксі викидав бекенди як нездорові. Раптом «швидше деплоїти» стало «деплой викликає каскадний brownout».

Вони відреагували, посиливши пороги healthcheck‑ів, щоб «бути більш чутливими», що було саме невірним рішенням. Це змушувало проксі ще швидше відкидати бекенди, підсилюючи простій.
Їх SLO різко страждали, і всі навчилися різниці між «виявити збій» і «спровокувати збій».

Остаточне виправлення: підвантажувати образи в спокійні періоди, обмежувати IO‑вплив (через планування на рівні хоста чи просто дисципліну), робити healthcheck‑и толерантнішими до коротких сплесків.
Також перемістили Docker data‑директорію на швидший носій і по можливості розділили IO для БД і витягування образів.

Урок оптимізації: швидкість не безкоштовна. Якщо ви прискорюєте деплой, перемістивши роботу в критичне вікно — платять користувачі.

Міні‑історія 3: Нудна, але правильна практика, що врятувала день (дві версії + простий відкат)

Регульована корпоративна команда — багато процесів, купа документів — запускала клієнтський портал на Compose. Вони не були модними. Вони рідко падали.
Їх метод деплою виглядав старомодно: стабільний проксі, два сервіси додатку («blue» і «green»), явні health‑гейти і ручне переключення трафіку.

Одної п’ятниці новий реліз пройшов тести, але мав тонку витік пам’яті, що тригерилась рідкісним патерном запиту. Через двадцять хвилин після переключення RSS почав зростати.
Затримки піднялись. On‑call спостерігав це з холодною спокоєм людини, що репетирувала цей сценарій.

Вони перемкнули трафік назад на blue шляхом перезавантаження конфігу проксі, потім зупинили green. Відкат зайняв менше часу, ніж пояснення, чому він потрібен.
Клієнти майже не помітили: невелике зростання затримки, без банера «сервіс недоступний», без паніки.

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

Урок: надійність — це переважно не гламурна повторюваність. Якщо ваш деплой вимагає мужності — ви вже програли.

Швидкий план діагностики: швидко знайти вузьке місце

Коли «деплой без простоїв» викликає спалах, у вас немає часу на філософські дебати. Потрібен швидкий триаж, що звужує причину до: проксі/переключення трафіку, готовності додатку, бази даних або ресурсів хоста.

Перш за все: чи стабільний вхідний пункт?

  • Перевірте, чи проксі контейнер пересоздавався або перезапускався під час деплою (docker events, docker ps uptime).
  • Перевірте безперервність прив’язки портів хоста (ss -ltnp для 80/443).
  • Перевірте логи проксі на upstream‑помилки та помилки reload.

Якщо проксі флапає — це ваш простій. Виправте процес так, щоб проксі залишався живим і лише перезавантажував конфіг.

Друге: чи бекенди здорові та справді готові?

  • Перевірте статус health для нових контейнерів.
  • Вдарте endpoint готовності зсередини мережі проксі.
  • Підтвердьте, що проксі маршрутизує до очікуваного набору бекендів.

Якщо healthcheck каже «healthy», але додаток все одно падає під навантаженням — ваш healthcheck брехливий. Зробіть його більш репрезентативним.

Третє: чи база даних блокує світ?

  • Перевірте активні запити і локи в вікна міграцій.
  • Шукайте помилки підключення/таймаути в логах додатку.
  • Перевірте IO wait і поведінку чекпоінтів у базі (метрики хоста допомагають).

Якщо локи накопичуються — припиніть перезапуски. Виправте міграції. Схемні блокування не підкоряються оптимізму.

Четверте: чи хост під тиском ресурсів?

  • CPU steal/throttle, IO wait, тиск пам’яті.
  • Pull образів Docker і активність витягування шарів під час деплою.
  • Насичення диска для Docker data‑директорії і томів бази даних.

Якщо хост задихається, найкращий патерн деплою вам не допоможе. Надійність починається з нудної місткості.

Поширені помилки: симптом → корінна причина → виправлення

1) Симптом: короткий простій при кожному деплої (кілька секунд 502)

Корінна причина: Контейнер додатку володіє портом хоста; його перезапуск/пересоздання знімає бінд порту.

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

2) Симптом: нова версія стартує, негайно отримує трафік і повертає 500 протягом 10–30 секунд

Корінна причина: Немає gating за готовністю; healthcheck тестує лише «процес існує» або відсутній.

Виправлення: Реалізуйте справжній endpoint /ready, який перевіряє залежності; використовуйте Docker healthcheck і проксі‑гейтинг на його основі.

3) Симптом: деплой викликає цикл перезапусків; в логах помилки підключення до БД

Корінна причина: БД не готова, йде міграція або конфлікт локів; політика перезапуску підсилює проблему.

Виправлення: Додайте start period; уникайте циклів перезапуску під час міграцій; відокремте міграції від стартапу додатку; робіть міграції онлайн і ітеративно.

4) Симптом: з’єднання обриваються під час деплою, хоча проксі живий

Корінна причина: Додаток не обробляє SIGTERM; stop timeout занадто малий; довгоживучі з’єднання не дренуються.

Виправлення: Реалізуйте graceful shutdown; збільшіть stop_grace_period; налаштуйте проксі перестати маршрутизувати перед зупинкою контейнерів.

5) Симптом: відкат займає довше, ніж деплой, і ризиковий

Корінна причина: Нема паралельного запуску; деплой замінює in-place; зміни бази не сумісні назад.

Виправлення: Blue/green сервіси; тримайте попередню версію доступною; використовуйте expand/contract міграції і фіч‑флаги.

6) Симптом: «в стейджингу працювало», але продакшн‑деплой викликає сплески латентності

Корінна причина: Ресурсна конкуренція в продакшні (IO, CPU) під час pull образів/міграцій; healthcheck надто агресивні.

Виправлення: Передзавантажуйте образи; плануйте важкі операції; тонко налаштуйте таймаути/повтори healthcheck; розділяйте IO‑шляхи для БД і Docker, де можливо.

7) Симптом: проксі маршрутизує до мертвих бекендів після деплою

Корінна причина: Конфіг upstream проксі використовує статичні IP або застарілі імена контейнерів; невідповідність мережевого підключення.

Виправлення: Використовуйте сервіс‑дискавері через Docker DNS в user-defined мережі; посилайтеся на імена сервісів; переконайтеся, що проксі в тій же мережі.

8) Симптом: Compose «up -d» пересоздає більше, ніж очікували

Корінна причина: Дрейф конфігу (зміни env, томів, тегів образів) викликає пересоздання; проксі не закріплений.

Виправлення: Заблокуйте env; використовуйте docker compose config для інспекції фінального конфига; уникайте зміни контейнера проксі без потреби; робіть reload конфігів замість пересоздання.

Чеклісти / покроковий план

Чекліст 1: Мінімальний життєздатний стек Compose для «майже нульового простою»

  • Стабільний проксі-контейнер, що публікує порти хоста 80/443.
  • Бекенди додатку не публікуються на порти хоста; доступні лише у внутрішній мережі.
  • Healthchecks для готовності (не «процес існує»).
  • Акуратне завершення: обробка SIGTERM + достатній stop grace period.
  • Міграції БД відокремлені від старту додатку та спроектовані для online‑змін.
  • План відкату: тримайте попередню версію запускаємою і маршрутизованою, поки нова не доведе себе.

Чекліст 2: Покроковий деплой (blue/green зі switch проксі)

  1. Підвантажте образ, щоб уникнути IO‑сплесків у критичному вікні.

    cr0x@server:~$ docker pull registry/app:1.9.4
    1.9.4: Pulling from app
    Digest: sha256:8b9a2f6d3c1e8f...
    Status: Downloaded newer image for registry/app:1.9.4
    

    Рішення: Якщо pull займає забагато часу або насичує IO — робіть це раніше або виправляйте сховище.

  2. Запустіть новий бекенд поряд зі старим.

    cr0x@server:~$ docker compose up -d app-green
    [+] Running 1/1
     ✔ Container prod-app-green-1  Started
    

    Рішення: Якщо це пересоздає blue або proxy — ваша модель Compose неправильна. Зупиніться і ізолюйте сервіси.

  3. Чекайте готовності green.

    cr0x@server:~$ docker inspect --format '{{.State.Health.Status}}' prod-app-green-1
    healthy
    

    Рішення: Якщо unhealthy — відкотіть, зупинивши green, і вивчіть логи.

  4. Перемкніть маршрутизацію проксі (ваги або одна ціль upstream), потім reload.

    cr0x@server:~$ docker exec -it prod-proxy-1 nginx -t
    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful
    
    cr0x@server:~$ docker exec -it prod-proxy-1 nginx -s reload
    2026/01/03 10:20:12 [notice] 1#1: signal process started
    

    Рішення: Якщо nginx -t падає — не reload; виправте генерацію конфігів спочатку.

  5. Спостерігайте трафік і помилки протягом soak‑періоду.

    cr0x@server:~$ docker exec -it prod-proxy-1 tail -n 10 /var/log/nginx/access.log
    10.0.1.10 - - [03/Jan/2026:10:20:15 +0000] "GET /api/orders HTTP/1.1" 200 431 "-" "Mozilla/5.0"
    10.0.1.11 - - [03/Jan/2026:10:20:16 +0000] "POST /api/pay HTTP/1.1" 200 1024 "-" "Mozilla/5.0"
    

    Рішення: Якщо бачите upstream 502/504 — перевірте готовність бекенда, локи БД і таймаути проксі.

  6. Drain і зупиніть blue після впевненості.

    cr0x@server:~$ docker compose stop app-blue
    [+] Running 1/1
     ✔ Container prod-app-blue-1  Stopped
    

    Рішення: Якщо вам потрібен миттєвий відкат — не видаляйте blue одразу: тримайте його зупиненим, але доступним, або залиште працювати, але без маршрутизації.

Чекліст 3: Дисципліна змін бази даних для Compose‑деплоїв

  • Ніколи не поєднуйте ризикові міграції з деплоєм, який не можна відкотити.
  • Переважайте додавання змін спочатку (нова nullable колонка, нова таблиця, новий індекс конкурентно якщо підтримується).
  • Бекфіл у батчах через job/worker з rate‑limiting.
  • Перемикайте читання/запис на нову схему через фіч‑флаг або версіонований кодовий шлях.
  • Лише потім видаляйте старі колонки/таблиці в наступному деплої.

Питання та відповіді

1) Чи може Docker Compose робити істинні деплоя без простоїв?

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

2) Чому не просто використати docker compose up -d і довіритись йому?

Тому що Compose узгоджує бажаний стан шляхом пересоздання контейнерів при виявленні змін. Якщо контейнер володіє портом хоста — пересоздання означає блинт
прив’язки порту. Це не злоба; це дизайн.

3) Чи гарантує depends_on, що мій додаток почекає базу даних?

Ні. Воно забезпечує порядок старту, а не готовність. Використовуйте healthcheck‑и, явну логіку очікування в додатку або процес деплою, що перевіряє готовність залежностей.

4) Який найпростіший працездатний патерн?

Стабільний зворотний проксі на портах хоста + два сервіси додатку (blue/green) в user‑defined мережі. Запустіть green, перевірте health, перезавантажте проксі, щоб маршрутизувати на green, потім дренуйте і зупиніть blue.

5) Чи варто використовувати Traefik для цього?

Traefik підходить, якщо ви вмієте його оперувати. Він сильний в динамічній конфігурації через лейбли контейнерів. Але «динамічний» не означає «безпечний»;
вам все одно потрібні healthcheck‑и, поведінка дренування і план відкату.

6) Що з websocket і довгоживучими з’єднаннями?

Плануйте дренування з’єднань. Багато websocket‑клієнтів відпадають при рестарті бекенду. Ви можете зменшити біль, збільшивши grace‑періоди, припиняючи маршрутизацію перед зупинкою
і проєктуючи клієнтів на коректне перепідключення. «Нульовий простій» може все одно означати «декілька перепідключень».

7) Чи можна робити rolling updates з --scale?

Можна написати ручний плейбук: масштабувати нову версію, маршрутизувати трафік, потім зменшити стару. Compose цього не координує за вас, тож ви маєте написати й протестувати кроки.

8) Яка найбільша прихована причина простих під час Compose‑деплоїв?

Міграції баз даних, що блокують таблиці або засмічують IO. Замінити контейнери зазвичай простіше; зміни схеми — бос‑файт.

9) Чи безпечніше запускати міграції при старті контейнера?

Зазвичай ні. Це зв’язує успіх деплою з успіхом міграції, заохочує «перезапускай, поки не вийде», і може спричинити thundering herd, якщо декілька реплік стартують одночасно.
Краще контролювати міграцію окремо й явно.

10) Коли варто припинити використовувати Compose у продакшні?

Коли вам потрібне розміщення на кількох хостах, самовідновлення між машинами, автоматичні rolling updates з управлінням трафіком або надійний розподіл секретів/конфігів.
У такому разі вам потрібен оркестратор, а не дуже дисциплінований bash‑скрипт.

Висновок: практичні наступні кроки

Compose не дає вам нульового простою сам по собі. Він дає чистий, читабельний опис того, що має працювати на хості. Решта — зміщення трафіку, готовність,
дренування і дисципліна в схемах — на вашому боці. Це не скарга. Це контракт.

Якщо ви хочете робочі оновлення з майже нульовим простоєм на Compose, зробіть наступне:

  1. Поставте стабільний зворотний проксі перед усім і перестаньте публікувати порти додатків напряму.
  2. Додайте реальний endpoint готовності і підключіть його до Docker healthcheck і маршрутизації проксі.
  3. Реалізуйте graceful shutdown: обробка SIGTERM, адекватний stop grace period і дренування перед зупинкою.
  4. Відділіть міграції від старту додатку й прийміть expand/contract підхід до змін схеми.
  5. Напишіть плейбук деплою, який ви зможете виконати о 3‑ій ночі — і відрепетируйте відкат, поки він не стане нудним.

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

← Попередня
WordPress повільний на мобільних: що оптимізувати насамперед
Наступна →
Docker buildx multi-arch: припиніть випускати неправильні бінарні файли

Залишити коментар