Docker: Патерн Compose, який запобігає 90% відмов у продуктивному середовищі

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

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

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

Патерн: Compose як гейтований, спостережуваний граф сервісів

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

Ось основна ідея:

  1. Кожен критичний сервіс має healthcheck, який відображає реальну готовність (не «процес існує»).
  2. Залежності гейтовані за станом здоров’я, а не за створенням контейнера.
  3. One-shot завдання явні (міграції, перевірки схеми, bootstrap) і ідемпотентні.
  4. Стан ізольований в іменованих томах (або керованих bind mount) з шляхами для бекапу/відновлення.
  5. Конфігурація незмінна для кожного розгортання (env + файли) і зміни робляться свідомо.
  6. Політика рестарту — це свідоме рішення, а не значення за замовчуванням. Постійні краші — це не «висока доступність».
  7. Логи обмежені, щоб «режим налагодження» не став «диск повністю заповнений».
  8. Існують обмеження ресурсів, щоб один сервіс не міг виснажити хост і забрати з собою решту.
  9. У вас є швидкий цикл діагностики: три команди, щоб визначити вузьке місце менш ніж за дві хвилини.

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

Цитата, яку варто тримати на видноті: Надія — не стратегія. — Джеймс Грин (поширено в опсом середовищах).
Якщо ви не впевнені в точності, сприйміть це як перефразовану думку; суть не змінюється.

Жарт №1: Хороше в «працює на моїй машині» — це правда. Погане — ваша машина не є продакшном.

Чим цей патерн не є

  • Не означає «перенести все в Kubernetes». Compose підходить для багатьох продакшн-систем.
  • Не означає «просто додати depends_on». Без гейтування по здоров’ю це театральний порядок.
  • Не означає restart: always. Це як перетворити помилку конфігурації на нескінченний цикл з чудовими метриками uptime для рантайму контейнерів.

Чому це працює: ви зменшуєте невизначеність

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

Цікаві факти та історичний контекст

  • Compose починався як Fig (ера 2013–2014), інструмент для розробника для визначення мультиконтейнерних додатків; підвищення надійності в продакшні з’явилось пізніше у вигляді патернів, а не налаштувань за замовчуванням.
  • Docker healthchecks з’явилися після того, як оператори масово винаходили скрипти «wait-for-it»; платформа визнала, що готовність — це першокласна потреба.
  • depends_on не означає «ready» за замовчуванням; історично це означає «запусти цей контейнер перед тим», що рідко є справжньою вимогою.
  • Політики рестарту — це не ретраї; це «продовжуй намагатися вічно». Багато постмортемів включають хвилю рестартів, яка приховала першу корисну помилку.
  • Контейнери не містять ядра; шумні сусіди все ще існують. Без обмежень CPU/пам’яті один сервіс може деградувати хост і все на ньому.
  • Локальні томи прості, але портативність — ні; іменовані томи зручні в визначенні, але життєвий цикл даних — ваша відповідальність.
  • Логуючі драйвери мають значення; json-file зручний, поки балакуча програма не перетворить диск на повільно зростаючий інцидент.
  • Compose — це не планувальник; він не переразподіляє навантаження між вузлами і не обробляє відмови вузлів як оркестратор. Дизайн має припускати один хост, якщо ви не побудуєте інакше.
  • «Init containers» існували як патерн задовго до того, як Kubernetes популяризував термін; одноразові bootstrap-завдання потрібні скрізь у розподілених системах.

Чому стеки Compose падають у продакшні (повторювані причини)

1) Порядок старту сприймають як готовність залежностей

Ваш API-контейнер стартує. Він намагається підключитися до Postgres. Postgres «вгору» в термінах Docker (PID існує), але все ще відтворює WAL,
виконує відновлення або просто ще не слухає порт. API падає, перезапускається, падає знову. У вас відмова, що виглядає як проблема API,
але насправді це проблема готовності.

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

2) Міграції трактують як побічний ефект замість окремого завдання

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

Зробіть міграції виділеним one-shot сервісом. Зробіть їх ідемпотентними. Зробіть так, щоб вони блокували старт додатку, поки не завершаться.
Ваше майбутнє «я» скаже вам «дякую». Ймовірно у вигляді меншої кількості пейджів.

3) Томи трактують як «якийсь каталог»

Станові сервіси не виходять зі збоя акуратно. Вони руйнуються, заповнюють диск або монтуються з неправильними правами.
Іменовані томи допомагають, бо вони відокремлюють шлях даних від випадкової структури файлової системи, але вони не замінюють:
бекапи, відновлення, перевірки ємності та контроль змін.

4) Політики рестарту маскують реальні збої

restart: always — грубий інструмент. Він буде вірно перезапускати контейнер з опечаткою в змінній середовища, відсутнім файлом секрету
або з проваленою міграцією. Ваш моніторинг бачить флапінг. Логи перетворюються на блендер. Тим часом корінна причина пробіжить раз у три секунди.

Використовуйте restart: unless-stopped або on-failure свідомо. Поєднуйте з healthchecks, щоб «running» не було брехнею.

5) Відсутні ліміти ресурсів → несподівана смерть хоста

Compose охоче дозволить одному контейнеру спожити всю пам’ять, викликати OOM killer і знищити не пов’язані сервіси.
Це не теоретично. Це найстаріший трюк із книги «чому все впало?».

6) Логи їдять диск

Лог-драйвер за замовчуванням json logging може рости без меж. Якщо диск хоста заповнюється, база може припинити запис,
додаток перестати створювати тимчасові файли, а сам Docker стати нестабільним.

Обмеження логів — це не приємна опція; це ремінь безпеки.

7) «Зручні» мережеві вибори створюють невидиме зчеплення

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

Використовуйте внутрішні мережі. Публікуйте лише те, що потрібно людям або зовнішнім системам. Тримайте схід-західній трафік всередині мережі Compose.

Зразковий файл Compose (коментарі, орієнтація на продакшн)

Це патерн, а не священний текст. Адаптуйте під себе. Важлива частина — взаємодія:
healthchecks, гейти, явні завдання, обмежені логи та довговічні томи.

cr0x@server:~$ cat docker-compose.yml
version: "3.9"

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "5"

networks:
  appnet:
    driver: bridge

volumes:
  pgdata:
  redisdata:

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: app
      POSTGRES_PASSWORD_FILE: /run/secrets/pg_password
    secrets:
      - pg_password
    volumes:
      - pgdata:/var/lib/postgresql/data
    networks: [appnet]
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d appdb -h 127.0.0.1"]
      interval: 5s
      timeout: 3s
      retries: 20
      start_period: 10s
    restart: unless-stopped
    logging: *default-logging

  redis:
    image: redis:7
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redisdata:/data
    networks: [appnet]
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 20
    restart: unless-stopped
    logging: *default-logging

  migrate:
    image: ghcr.io/example/app:1.9.3
    command: ["./app", "migrate", "up"]
    environment:
      DATABASE_URL_FILE: /run/secrets/db_url
    secrets:
      - db_url
    networks: [appnet]
    depends_on:
      postgres:
        condition: service_healthy
    restart: "no"
    logging: *default-logging

  api:
    image: ghcr.io/example/app:1.9.3
    environment:
      DATABASE_URL_FILE: /run/secrets/db_url
      REDIS_URL: redis://redis:6379/0
      PORT: "8080"
    secrets:
      - db_url
    networks: [appnet]
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      migrate:
        condition: service_completed_successfully
    ports:
      - "127.0.0.1:8080:8080"
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8080/healthz | grep -q ok"]
      interval: 10s
      timeout: 3s
      retries: 10
      start_period: 10s
    restart: unless-stopped
    logging: *default-logging
    deploy:
      resources:
        limits:
          memory: 512M

secrets:
  pg_password:
    file: ./secrets/pg_password.txt
  db_url:
    file: ./secrets/db_url.txt

Що варто запозичити з цього файлу

  • Healthchecks відповідають реальній готовності: pg_isready, redis-cli ping та HTTP-ендпоїнт, що перевіряє, чи сервіс обслуговує запити.
  • Гейтування включає «completed successfully» для міграцій. Якщо міграції провалюються, API залишається вимкненим і дає явну діючу помилку.
  • Секрети через файли, щоб паролі не опинялися у виводі docker inspect або історії shell.
  • Порти прив’язані до localhost для безпеки. Якщо потрібен зовнішній доступ — поставте зворотний проксі спереду.
  • Обмежені логи через ротацію. Це запобігає інцидентам, коли «debug log з’їв диск».
  • Іменовані томи для станових сервісів. Це не магія, але принаймні явність.

Що варто налаштувати негайно

  • Ліміти пам’яті/CPU залежно від розміру хоста та поведінки сервісу.
  • Логіка healthcheck, щоб відповідати реальному стану «готовий» вашого додатку (наприклад, підключення до БД + застосовані міграції).
  • Графік бекапів і процедура відновлення для томів. Якщо ви не можете відновити — у вас не бекапи, а дорогі сподівання.

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

Це не «приємна діагностика». Це кроки, які ви робите під час інциденту, коли годинник тікає, і кроки, які виконуєте у спокійний вівторок,
щоб запобігти інциденту.

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

cr0x@server:~$ docker compose ps
NAME                IMAGE                         COMMAND                  SERVICE     STATUS                    PORTS
stack-postgres-1     postgres:16                   "docker-entrypoint.s…"   postgres    Up 2 minutes (healthy)    5432/tcp
stack-redis-1        redis:7                       "docker-entrypoint.s…"   redis       Up 2 minutes (healthy)    6379/tcp
stack-migrate-1      ghcr.io/example/app:1.9.3      "./app migrate up"       migrate     Exited (0) 90 seconds ago
stack-api-1          ghcr.io/example/app:1.9.3      "./app server"           api         Up 2 minutes (healthy)    127.0.0.1:8080->8080/tcp

Що це означає: «Up» — недостатньо; ви хочете бачити (healthy) для сервісів з довгим життєвим циклом і Exited (0) для одноразових задач, як міграції.

Рішення: Якщо API в статусі Up, але не (healthy), діагностуйте готовність (логіку healthcheck, залежності, час старту). Якщо migrate повернув ненульовий код, зупиніться і виправте міграції перш за все.

Завдання 2: Знайти перший збій у логах (не найголосніший)

cr0x@server:~$ docker compose logs --no-color --timestamps --tail=200 api
2026-02-04T01:18:42Z api  | ERROR: could not connect to postgres: connection refused
2026-02-04T01:18:45Z api  | INFO: retrying in 3s
2026-02-04T01:18:48Z api  | ERROR: migration state not found

Що це означає: Ви бачите симптоми. Перший error — «connection refused», що вказує, що Postgres ще не слухає або зломався DNS/мережа.
Пізніший «migration state not found» може бути наслідком.

Рішення: Перевірте стан і логи Postgres далі; не зациклюйтеся лише на аплікації.

Завдання 3: Перевірити статус здоров’я контейнера детально

cr0x@server:~$ docker inspect --format '{{json .State.Health}}' stack-postgres-1
{"Status":"healthy","FailingStreak":0,"Log":[{"Start":"2026-02-04T01:18:21.112Z","End":"2026-02-04T01:18:21.189Z","ExitCode":0,"Output":"/var/run/postgresql:5432 - accepting connections\n"}]}

Що це означає: Healthcheck проходить і повідомляє «accepting connections». Добрий знак.

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

Завдання 4: Перевірити service discovery зсередини мережі

cr0x@server:~$ docker exec -it stack-api-1 getent hosts postgres
172.22.0.2   postgres

Що це означає: DNS резолвить postgres у IP контейнера в мережі Compose.

Рішення: Якщо це не працює, ймовірно мережа налаштована неправильно (сервіс не в тій же мережі, опечатка в імені мережі або використання host networking неправильно).

Завдання 5: Протестувати TCP-з’єднання до залежності з контейнера аплікації

cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'nc -vz postgres 5432'
Connection to postgres (172.22.0.2) 5432 port [tcp/postgresql] succeeded!

Що це означає: Мережевий шлях відкритий. Якщо аплікація все ще падає, ймовірно проблема в креденшелах, режимі SSL, імені бази або параметрах підключення.

Рішення: Перевірте вміст файлу секрету та його парсинг (акуратно), і логи аутентифікації Postgres.

Завдання 6: Підтвердити, яку конфігурацію контейнер реально отримав

cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'ls -l /run/secrets && sed -n "1p" /run/secrets/db_url'
total 4
-r--r----- 1 root root 74 Feb  4 01:17 db_url
postgres://app:REDACTED@postgres:5432/appdb?sslmode=disable

Що це означає: Секрет існує, права виглядають розумно, і URL вказує на postgres.

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

Завдання 7: Перевірити логи Postgres на предмет проблем аутентифікації та відновлення

cr0x@server:~$ docker compose logs --tail=120 postgres
postgres  | LOG:  database system is ready to accept connections
postgres  | FATAL:  password authentication failed for user "app"

Що це означає: Postgres працює; креденшели неправильні.

Рішення: Обертайте/виправляйте секрет пароля, потім рестартуйте уражені сервіси. Не «просто перезапускайте все» без виправлення кореневої причини.

Завдання 8: Швидко помітити петлі рестарту

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.RunningFor}}'
NAMES              STATUS                          RUNNING FOR
stack-api-1         Restarting (1) 2 seconds ago    3 minutes
stack-postgres-1    Up 5 minutes (healthy)          5 minutes
stack-redis-1       Up 5 minutes (healthy)          5 minutes

Що це означає: API флапає; ваші логи можуть бути обрізані між рестартами.

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

Завдання 9: Дізнатися код виходу та останню помилку для впалого контейнера

cr0x@server:~$ docker inspect --format '{{.State.ExitCode}} {{.State.Error}}' stack-api-1
1

Що це означає: Код виходу 1 загальний; треба використовувати логи та вивід самої аплікації, щоб знайти проблему.

Рішення: Якщо код виходу стабільно однаковий, ймовірно маєте детерміновану проблему конфігурації (секрети, env, міграція), а не транзієнну інфраструктурну.

Завдання 10: Перевірити тиск на диск хоста перед будь-якими «розумними» діями

cr0x@server:~$ df -h
Filesystem      Size  Used Avail Use% Mounted on
/dev/nvme0n1p2  200G  192G  8.0G  97% /

Що це означає: 97% використано. Ви у зоні ризику. Бази даних і Docker поводяться погано, коли місця мало.

Рішення: Зупиніть зростання логів (ротація, обережне обрізання), очистіть невикористані образи або розширте диск. Не розгортайте повторно і не погіршуйте ситуацію.

Завдання 11: Перевірити розподіл використання диска Docker

cr0x@server:~$ docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          28        6         35.2GB    23.4GB (66%)
Containers      14        4         1.1GB     920MB (83%)
Local Volumes   7         2         96.0GB    22.0GB (22%)
Build Cache     0         0         0B        0B

Що це означає: Томи великі (очікувано для баз даних). Образи можна звільнити.

Рішення: Спочатку почистьте невикористані образи/контейнери; не торкайтеся томів без плану бекапу/відновлення і чіткого розуміння того, що видаляєте.

Завдання 12: Безпечно почистити невикористані образи (коли ви впевнені)

cr0x@server:~$ docker image prune -a
Deleted Images:
deleted: sha256:4c2c6b1f8b7c...
Total reclaimed space: 18.6GB

Що це означає: Ви звільнили місце, видаливши невикористані образи.

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

Завдання 13: Визначити, хто їсть пам’ять і викликає OOM

cr0x@server:~$ docker stats --no-stream
CONTAINER ID   NAME              CPU %     MEM USAGE / LIMIT     MEM %     NET I/O           BLOCK I/O        PIDS
c0ffee12ab34   stack-api-1        180.23%   620MiB / 512MiB       121.1%    120MB / 95MB      2.1GB / 45MB    42
bada55aa9876   stack-postgres-1    35.01%    900MiB / 0B          0.0%      80MB / 110MB      8.2GB / 3.1GB   19

Що це означає: API перевищив ліміт пам’яті і, ймовірно, отримує kill; Postgres не має ліміту (0B показує «необмежено»).

Рішення: Збільшіть пам’ять API, якщо це законно, виправте витоки пам’яті і встановіть розумні ліміти для Postgres відповідно до RAM хоста і потреб кешування.

Завдання 14: Перевірити OOM kills в ядрі хоста

cr0x@server:~$ dmesg -T | tail -n 12
[Mon Feb  4 01:22:13 2026] Out of memory: Killed process 21134 (app) total-vm:2104820kB, anon-rss:682312kB, file-rss:104kB, shmem-rss:0kB, UID:0 pgtables:1820kB oom_score_adj:0

Що це означає: Ядро вбило ваш процес. Це не «контейнер упав»; це «хост витратив всю пам’ять».

Рішення: Додайте пам’ять, встановіть ліміти, зменшіть конкуренцію або виправте додаток. Ігнорувати OOM не варто — наступний інцидент буде гучнішим.

Завдання 15: Перевірити, чи ротація логів справді застосована

cr0x@server:~$ docker inspect --format '{{.HostConfig.LogConfig.Type}} {{json .HostConfig.LogConfig.Config}}' stack-api-1
json-file {"max-file":"5","max-size":"10m"}

Що це означає: Контейнер використовує обмежені json-file логи.

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

Завдання 16: Підтвердити томи та точки монтування (перевірка «де мої дані?»)

cr0x@server:~$ docker volume ls
DRIVER    VOLUME NAME
local     stack_pgdata
local     stack_redisdata
cr0x@server:~$ docker volume inspect stack_pgdata | sed -n '1,12p'
[
    {
        "CreatedAt": "2026-02-03T21:10:44Z",
        "Driver": "local",
        "Name": "stack_pgdata",
        "Mountpoint": "/var/lib/docker/volumes/stack_pgdata/_data",
        "Scope": "local"
    }
]

Що це означає: Дані живуть під mountpoint томів Docker на цьому хості.

Рішення: Якщо ви очікували дані в іншому місці (наприклад bind mount), узгодьте це зараз — до того, як заміна хоста стане випадковим видаленням даних.

Завдання 17: Швидко зробити логічний дамп Postgres (для невеликих/середніх БД)

cr0x@server:~$ docker exec -t stack-postgres-1 pg_dump -U app -d appdb | gzip -c > /var/backups/appdb_$(date +%F).sql.gz

Що це означає: Ви створили gzipped SQL дамп на хості.

Рішення: Якщо база велика, це може бути занадто повільно для реакції на інцидент. Плануйте фізичні бекапи або реплікацію; не відкривайте це під час відмови.

Завдання 18: Перевірити health endpoint з хоста

cr0x@server:~$ curl -fsS http://127.0.0.1:8080/healthz
ok

Що це означає: Сервіс доступний з хоста і повертає очікуване тіло.

Рішення: Якщо це не працює, але контейнер позначений як «healthy», ваш healthcheck бреше або прив’язка порта некоректна.

Швидкий план діагностики

Коли продуктивність впала, вам не потрібна філософія. Потрібна коротка петля, яка швидко ідентифікує вузьке місце і не дає вам
«виправити» не ту річ на швидку руку.

По-перше: це проблема готовності залежності чи баг у додатку?

  1. Перевірте статус графа:
    cr0x@server:~$ docker compose ps
    NAME                IMAGE                         COMMAND               SERVICE   STATUS                     PORTS
    stack-api-1          ghcr.io/example/app:1.9.3      "./app server"        api       Up 1 minute (unhealthy)    127.0.0.1:8080->8080/tcp
    stack-postgres-1     postgres:16                   "docker-entrypoint"   postgres  Up 1 minute (healthy)      5432/tcp
    

    Рішення: Якщо залежності healthy, а API unhealthy — фокус на конфігурації API та його шляху готовності.

  2. Прочитайте останні 200 рядків для проблемного сервісу:
    cr0x@server:~$ docker compose logs --tail=200 api
    api  | ERROR: missing required setting: JWT_PUBLIC_KEY
    

    Рішення: Відсутня конфігурація/секрет. Не перезапускайте по декілька разів. Виправте інжекцію конфігурації.

По-друге: чи хост «хворий» (диск, пам’ять, CPU, IO)?

  1. Диск:
    cr0x@server:~$ df -h /
    Filesystem      Size  Used Avail Use% Mounted on
    /dev/nvme0n1p2  200G  199G  1.0G  100% /
    

    Рішення: Сприймайте «100%» як «нічого не працює». Звільніть місце перед іншими діями.

  2. Пам’ять:
    cr0x@server:~$ free -h
                   total        used        free      shared  buff/cache   available
    Mem:            16Gi        15Gi       210Mi       120Mi       790Mi       420Mi
    Swap:            0B          0B          0B
    

    Рішення: Якщо доступної пам’яті мало і свапу немає, очікуйте OOM kills. Зменшуйте навантаження або додавайте пам’ять/ліміти.

  3. Хто споживає ресурси:
    cr0x@server:~$ docker stats --no-stream
    CONTAINER ID   NAME             CPU %     MEM USAGE / LIMIT     MEM %     NET I/O        BLOCK I/O      PIDS
    c0ffee12ab34   stack-api-1       220.14%   480MiB / 512MiB       93.8%     140MB / 98MB   1.8GB / 22MB  55
    

    Рішення: Якщо контейнер глухо навантажений CPU, підозрюйте tight loops, ретраї або thundering herd через провалені залежності.

По-третє: чи це мережа (DNS, порти, експозиція)?

  1. DNS з середини контейнера:
    cr0x@server:~$ docker exec -it stack-api-1 getent hosts postgres redis
    172.22.0.2   postgres
    172.22.0.3   redis
    

    Рішення: Якщо резолв не працює — проблема з приєднанням до мережі або іменами сервісів.

  2. Тести підключень:
    cr0x@server:~$ docker exec -it stack-api-1 bash -lc 'nc -vz postgres 5432; nc -vz redis 6379'
    Connection to postgres (172.22.0.2) 5432 port [tcp/postgresql] succeeded!
    Connection to redis (172.22.0.3) 6379 port [tcp/redis] succeeded!
    

    Рішення: Мережевий шлях добрий; фокус переходить на аутентифікацію/конфіг/міграції.

Мета плану — уникнути класичної дуги інциденту: «перезапустити все», потім «це все ще не працює», потім «чому логи зникли»,
потім «ми змінили три речі одночасно». Не будьте тією дугою.

Три короткі історії з практики

Міні-історія №1: Інцидент через невірне припущення

Середня SaaS-компанія виконувала стек Compose на одному хості: Postgres, Redis, API та worker. Вони були обережні — переважно.
Вони використовували depends_on, бо чули «це допомагає з порядком». Вони припустили, що порядок означає готовність.
Це припущення жило в продакшні місяцями, бо більшість рестартів були ручними і рознесені у часі.

Одного дня хост перезавантажився після рутинного патчу ядра. Postgres піднявся повільніше, бо мусив відтворити більше WAL, ніж зазвичай.
API-контейнер стартував негайно, спробував підключитись, провалився і перезапустився. Worker зробив те саме. Обидва були з restart: always.

Зовнішній монітор зафіксував HTTP 500 та таймаути. Внутрішні логи були як вогневий автомат з помилок підключення.
Інженерний лід спочатку підозрював корупцію Postgres, бо «це займає занадто багато часу». База була в порядку; вона була просто зайнята.
Але API і worker її завалили ретраями, зробивши її ще зайнятішою.

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

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

Міні-історія №2: Оптимізація, яка зіграла злий жарт

Команда фінансових сервісів була під тиском зменшити використання диска. Хости працювали інтенсивно, а директорії Docker росли.
Хтось помітив, що JSON логи величезні. Вони «оптимізували», перемкнувши кілька сервісів на мінімальну логіку і агресивне прибирання.
Зміна виглядала відповідально: менший ретеншн логів, більше автоматичного прибирання.

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

Команда спробувала тимчасово підвищити деталізацію. Це викликало інший проблему: тиск на диск спав, бо налаштування ротації логів були застосовані не однаково по сервісах. Деякі контейнеры роторувалися. Інші — ні. «Оптимізація» створила суміш політик,
і це саме те, як продакшн перетворює «розумне» в «хаос».

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

Урок: диск — це ресурс, логи — ресурс, а «оптимізувати» без спостережуваності — це обрізати очі в темряві.

Міні-історія №3: Нудна, але правильна практика, яка врятувала ситуацію

Компанія e‑commerce використовувала Compose для внутрішніх сервісів: оновлення каталогу, інгест цін і невеликий API. Нічого гламурного.
Але у них була одна річ, яку багато команд пропускають: задокументована процедура відновлення томів, протестована щоквартально.
Дріл був не просто «маємо бекапи». Він був «ми відновили бекап на чистому хості і довели, що сервіс працює».

Під час рутинної зміни інженер почистив каталог на хості — впевнений, що це «лише старі Docker файли».
Це не було так. Це був bind-mounted каталог, який використовувався станним сервісом. Контейнер все ще стартував. Навіть виглядав нормально кілька хвилин.
Потім почав повертати часткові дані і таймаути.

Вони не шукали винних. Виконали дріл. Зупинили стек, відновили з останнього бекапу в чистий іменований том,
підняли сервіси по тому ж Compose-файлу. Перевірили healthchecks, потім виконали job перевірки консистентності.
Вплив був обмежений, бо вони чітко знали, що означає «відновлення» у їхньому середовищі.

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

Урок: нудна практика — дріли відновлення — перетворює інциденти з даними з екзистенційних у незручні.

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

1) API флапає (рестарти кожні кілька секунд)

Симптом: Restarting (1) в docker ps, логи показують повторювані помилки підключення.

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

Виправлення: Додайте healthchecks; гейтніть depends_on за health; винесіть міграції в одноразовий сервіс; тимчасово вимкніть рестарти, щоб зафіксувати першу помилку.

2) Все «Up», але користувачі отримують 502/таймаути

Симптом: Compose показує сервіси як запущені; зовнішній проксі повертає 502 або таймаути.

Корінна причина: Healthcheck надто поверхневий (процес існує) або вказує на неправильний інтерфейс; аплікація працює, але не обслуговує трафік.

Виправлення: Нехай healthcheck звертається до реального ендпоїнта і перевіряє дійсну відповідь; переконайтеся, що сервіс прив’язаний до правильної адреси; узгодьте проксі upstream з портом контейнера.

3) База здорова, аплікація не може аутентифікуватися

Симптом: У логах Postgres видно password authentication failed.

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

Виправлення: Використовуйте патерн _FILE для секретів; стандартизуйте формат секретів; валідируйте секрети всередині контейнера; ротуйте креденшели свідомо.

4) Раптова відмова після увімкнення debug-логування

Симптом: Диск заповнюється; Docker і БД стають нестабільними; записи припиняються.

Корінна причина: Необмежені json-file логи; відсутня ротація на одному сервісі; занадто висока деталізація логування.

Виправлення: Нав’яжіть ротацію логів через anchors; обмежте деталізацію; додайте моніторинг диска; не покладайтеся на ручне прибирання.

5) Після перезавантаження сервіси піднімаються, але дані «зникли»

Симптом: Додаток поводиться як чиста інсталяція; БД без таблиць; Redis скинув кеш.

Корінна причина: Шлях bind mount змінився, права не дозволяють читати або сервіс використовує інший том, ніж очікувалося.

Виправлення: Віддавайте перевагу іменованим томам для стану; перевіряйте mounts через docker inspect; документуйте імена томів; проводьте drills відновлення.

6) Один сервіс спричиняє уповільнення системи

Симптом: Високе навантаження, OOM kills, IO wait; кілька контейнерів стають unhealthy.

Корінна причина: Відсутні ліміти ресурсів; runaway query; пакетна задача потрапила в піковий трафік.

Виправлення: Встановіть memory/CPU ліміти; розклад для важких завдань; обмежте конкуренцію; моніторте метрики хоста та статистику контейнерів.

7) «Працює локально, падає в прод» після зміни образів

Симптом: Нова версія образу відразу падає; стара версія працює.

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

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

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

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

Чекліст для продакшн Compose (зробіть це перед тим, як називати це «продакшн»)

  1. Healthchecks існують для кожного важливого сервісу (БД, кеш, API, проксі).
  2. Healthchecks чесні: вони перевіряють готовність, а не лише живучість.
  3. Залежності гейтовані з condition: service_healthy.
  4. Міграції — одноразова задача з restart: "no" і гейтом через service_completed_successfully.
  5. Секрети як файли (або інжектовані безпечно), а не вставлені в історію shell.
  6. Іменовані томи для стану, якщо немає вагомої причини використовувати bind mount.
  7. Є бекапи і відновлення тестуються. Заплануйте drill відновлення.
  8. Логи обмежені (розмір + кількість файлів) для кожного сервісу.
  9. Встановлені ресурсні ліміти, щоб один сервіс не міг вивести з ладу хост.
  10. Публікація портів мінімальна; внутрішній трафік залишається в внутрішніх мережах.
  11. Теги образів зафіксовані; оновлення — свідомі, не «latest» несподіванки.
  12. Є план відкату і він враховує зміни схеми бази даних.

Покроково: підсилити існуючий Compose стек за тиждень

  1. День 1: Інвентар і граф
    • Перелічіть сервіси, залежності і які з них станові.
    • Визначте, які ендпоїнти означають готовність.
  2. День 2: Додайте healthchecks
    • БД: pg_isready (або аналог).
    • API: /healthz ендпоїнт, що перевіряє критичні залежності.
    • Кеш: redis-cli ping або реальний тест читання/запису, якщо потрібно.
  3. День 3: Гейтніть залежності
    • Замініть наївне depends_on порядкування на умови здоров’я.
    • Введіть migrate як одноразовий сервіс і загейтайте API на нього.
  4. День 4: Стабілізуйте стан
    • Перенесіть станні сервіси в іменовані томи, якщо можливо.
    • Документуйте імена томів і точки монтування.
  5. День 5: Зробіть логування нудним
    • Налаштуйте ротацію логів через anchors.
    • Підтвердіть через docker inspect, що кожен контейнер підібрав налаштування.
  6. День 6: Додайте ліміти ресурсів і протестуйте під навантаженням
    • Почніть з консервативних лімітів пам’яті для балакучих аплікацій; переконайтесь, що УБД має достатній запас.
    • Слідкуйте за OOM і тротлінгом під реалістичним трафіком.
  7. День 7: Репетиція відмови
    • Рестарт хоста в maintenance-вікні і спостерігайте відновлення стека.
    • Симулюйте затримку залежності і переконайтесь, що гейти запобігають флапам.
    • Запустіть drill відновлення для тома бази даних.

Чекліст реакції на інцидент (коли ви вже впали)

  1. Запустіть docker compose ps і визначте перший unhealthy або exited сервіс.
  2. Перевірте df -h і free -h перед зміною конфігів.
  3. Витягніть логи для проблемного сервісу та його залежностей (--tail=200 з timestamp).
  4. Перевірте DNS і з’єднання зсередини проблемного контейнера (getent, nc).
  5. Якщо петля рестарту: тимчасово відключіть рестарти, відтворіть раз, зафіксуйте першу помилку, потім виправляйте.
  6. Не чистіть томи під час інциденту, якщо не відновлюєте з відомих добрих бекапів.

Поширені запитання

1) Чи гарантує depends_on, що моя база готова?

Не за замовчуванням. Він лише впливає на порядок старту. Використовуйте healthchecks і гейтніть за condition: service_healthy, або реалізуйте явну логіку готовності.

2) Чи достатньо healthchecks, щоб запобігти проблемам зі стартом?

Вони необхідні, але не достатні. Healthchecks запобігають «занадто ранньому старту», але вам все ще потрібні ідемпотентні міграції, коректні секрети, розумні ретраї і ліміти ресурсів.

3) Чому не просто покласти міграції в команду старту аплікації?

Бо рестарти трапляються. Коли аплікація перезапускається, міграції запустяться знову, якщо ви їх явно не зробили безпечними й ідемпотентними. One-shot сервіс для міграцій робить життєвий цикл видимим і гейтованим.

4) Використовувати іменовані томи чи bind mounts для баз даних?

За замовчуванням обирайте іменовані томи для ясності і портативності в життєвому циклі Docker. Використовуйте bind mounts лише за вагомої операційної причини і коли дисципліновано керуєте правами, бекапами й шляхами хоста.

5) Як тримати секрети поза docker inspect?

Уникайте простих змінних оточення для сирих секретів. Використовуйте файлові секрети і нехай аплікація читає через *_FILE env vars (або еквівалент). Також уникайте розміщення секретів у лейблах Compose або командних рядках.

6) Чи restart: always коли-небудь корисний?

Іноді — для сервісів з crash-only поведінкою, де збої справді транзієнтні і є сильний моніторинг. У більшості бізнес-додатків це ховає детерміновані проблеми конфігурації і створює галасливі хвилі рестартів.

7) Як робити «rolling updates» у Compose?

Compose не є повним оркестратором. Ви можете апроксимувати безпечні оновлення, запустивши кілька інстансів за проксі, оновлюючи по одному і використовуючи healthchecks для гейту трафіку. Якщо потрібні справжні rolling updates між вузлами — вам потрібен планувальник.

8) Чому прив’язувати порти API до 127.0.0.1?

Бо більшість внутрішніх сервісів не потребують публічної доступності. Прив’яжіть до localhost і поставте зворотній проксі або правила фаєрволу попереду. Це зменшує випадкову експозицію і конфлікти портів.

9) Контейнери healthy, але аплікація повільна. Що далі?

Health — бінарна; продуктивність — ні. Перевірте IO wait хоста, місце на диску, тиск пам’яті і статистику контейнерів. Потім профілюйте додаток і запити до БД. Більшість «повільних» інцидентів — це конкуренція ресурсів, а не питання Compose.

10) Чи можна відповідально запускати Compose у продакшні на одному хості?

Так — якщо ви приймаєте межі відмови і будуєте відповідно: бекапи, drills відновлення, моніторинг хоста, планування ємності і документований процес відновлення. Compose не врятує вас від фізики одного хоста.

Висновок: наступні кроки на цей тиждень

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

Наступні кроки, що дійсно мають значення:

  1. Додайте чесні healthchecks до кожного критичного сервісу і перевірте, що вони падають, коли сервіс не справді готовий.
  2. Перетворіть міграції на одноразовий Compose сервіс і загейтайте старт аплікації на його успіх.
  3. Застосуйте ротацію логів скрізь через Compose anchor і перевірте це за допомогою docker inspect.
  4. Встановіть ліміти пам’яті для найбільших «порушників» і слідкуйте за сигналами OOM; відкоригуйте за реального навантаження.
  5. Проведіть drill відновлення для тома бази даних на чистому хості. Якщо ви не можете цього зробити — не називайте це «забекапленим».

Зробіть ці п’ять кроків — і ви запобігнете більшості відмов, які здаються «проблемами Docker», але насправді є просто
«ми не описали систему, яку думали мати».

← Попередня
«Доступ заборонено» до власних файлів після переінсталяції: виправлення власності
Наступна →
Гальмування гри на швидкому ПК: основи DPC-латентності (і шлях виправлення)

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