1. Що означає «PostgreSQL готова»: процес живий ≠ база готова
Ми вже побачили, що короткий depends_on не розв’язує проблему таймінгу сам по собі: застосунку просто немає на що спиратися, крім факту «контейнер postgres уже стартував». Отже, наступний крок простий: нам потрібен спостережуваний сигнал від самої PostgreSQL — чи готова вона приймати з’єднання.
Якщо ви раніше запускали PostgreSQL локально, не в контейнері, то, найімовірніше, діяли «по-людськи»: запустили — зачекали кілька секунд — відкрили клієнт — працює. У контейнерному світі «кілька секунд» несподівано перетворюються на неявний контракт, який живе лише у вашій голові й регулярно ламається у колеги, на CI або просто зранку в понеділок. Саме тому нам потрібне чітке визначення того, що таке readiness для бази.
Для PostgreSQL readiness — це не «контейнер у статусі Up» і навіть не «процес postgres існує». У нашому контексті readiness означає дуже конкретну річ: база вже пройшла внутрішню ініціалізацію і готова приймати вхідні з’єднання, тобто клієнт може під’єднатися й отримати осмислену відповідь. У логах зазвичай з’являється фраза на кшталт database system is ready to accept connections, але читати логи й намагатися їх парсити — не той стиль, який ми хочемо закріпити як інженерний baseline.
Звідси випливає важлива практична думка: ми не повинні «вірити на слово», що PostgreSQL готова. Ми повинні перевіряти. І перевірка має бути такою, щоб Docker/Compose могли використовувати її як сигнал. У нас уже є ідеальний кандидат: утиліта pg_isready.
2. pg_isready: маленька команда, яка економить вам години життя
У світі PostgreSQL є спеціальна утиліта pg_isready. Її завдання звучить нудно, але корисно: швидко перевірити, чи відповідає PostgreSQL як сервер і чи готовий він приймати під’єднання. Якщо уявити базу як ресторан, то pg_isready — це не «ресторан існує за адресою», а «ресторан уже відчинився і готовий приймати замовлення», а не лише кухар надягнув фартух і шукає сіль.
В офіційному Docker image postgres ця утиліта є «з коробки», тож нам не потрібно ставити додаткові пакети або писати власні скрипти очікування. Це важливо: healthcheck має бути максимально простим, дешевим і відтворюваним, інакше він сам стане джерелом проблем.
Найцінніше для нас — те, що pg_isready працює як звичайна консольна команда з кодом повернення. Умовно: 0 — «усе добре», ненульове значення — «є проблема». Docker healthcheck саме так і мислить: команда виконалася успішно — контейнер healthy; неуспішно — unhealthy після певної кількості спроб.
Приклад ручної перевірки всередині контейнера, коли ви підняли сервіси через Compose:
# Виконуємо команду всередині контейнера postgres: перевіряємо, що база справді приймає під’єднання
docker compose exec postgres pg_isready -U catalog -d catalog
# postgres:5432 - приймає під’єднання
Якщо база ще прокидається, а PostgreSQL уміє прокидатися медитативно й з гідністю, ви побачите, що вона не готова:
# Та сама перевірка, але база ще не дійшла до стану "приймає під’єднання"
docker compose exec postgres pg_isready -U catalog -d catalog
# postgres:5432 - немає відповіді
І тут важливий момент для початківця: ви можете «випадково» вказати не того користувача або не ту базу. У такому разі ви отримаєте не «ready/not ready», а, по суті, перевірку іншої реальності. Тому в нашому проєкті ми спеціально тримаємо просту й синхронізовану конфігурацію: якщо в контейнері PostgreSQL створено користувача catalog і базу catalog, то й pg_isready має перевіряти те саме.
Якщо хочеться розуміти глибше, pg_isready повертає різні коди. Ідея проста, тож не потрібно заучувати її, як таблицю множення: успіх, відмова, немає відповіді, неправильні параметри. Для healthcheck нам важливіше не філософія, а те, що команда надійно сигналізує «готовий приймати з’єднання» й робить це однаково на всіх машинах.
3. Docker/Compose healthcheck
Тепер зберімо дві думки разом. У нас є перевірка готовності (pg_isready), але поки що вона живе у ваших руках: ви запускаєте команду самі. Compose, своєю чергою, має автоматично «зрозуміти» цей стан, щоб він відображався у статусі сервісу і міг використовуватися для контролю запуску залежних контейнерів.
Для цього Docker, а отже й Docker Compose, підтримує healthcheck: це команда, яку періодично виконують усередині контейнера. Якщо команда завершується успішно, контейнер позначається як healthy. Якщо ні — як unhealthy після кількох послідовних провалів. І саме тут часто відбувається приємне «клацання»: статус контейнера перестає бути бінарним «Up/Down» і стає трохи чеснішим.
Приклад того, що ви хочете побачити в підсумку:
postgres-1 Up (healthy)
У цей момент Up означає не просто «контейнер існує», а «контейнер живий, і його healthcheck підтверджує readiness». Тобто в нас з’являється спостережуваний, машиночитний сигнал, який можна використовувати далі.
Корисно прямо візуалізувати це як маленький конвеєр:
flowchart TD
%% Healthcheck — це "періодичний датчик", який дає Compose сигнал про readiness
A[Compose запускає контейнер postgres] --> B[PostgreSQL стартує всередині контейнера]
B --> C[Docker виконує healthcheck: pg_isready]
C -->|успіх| D[Контейнер отримує статус healthy]
C -->|провал| E[Контейнер залишається у стані starting/unhealthy]
Тут найважливіше: healthcheck живе на боці самої залежності — у postgres. Ми не змушуємо застосунок «вгадувати» готовність бази. Ми не додаємо в Spring Boot якийсь таймер на старті. Ми чесно описуємо готовність в інфраструктурному шарі, там, де їй і місце.
4. Параметри healthcheck
Коли початківець бачить healthcheck, перша реакція зазвичай така: «Окей, я зрозумів ідею, але чому тут п’ять параметрів? Docker що, не міг просто перевіряти?» Міг, але тоді в нас не було б контролю над тим, як часто перевіряти, скільки чекати і коли вважати сервіс справді хворим. А для бази даних ці нюанси дуже важливі.
Давайте розберемо параметри по-людськи. Я пропоную сприймати їх як налаштування будильника, який ви поставили на PostgreSQL: як часто будити, як довго кричати «прокинься», скільки разів розбудити, перш ніж вирішити «не прокидається», і скільки часу дати на ранкове розгойдування без штрафів.
Ось компактна таблиця з тим, що саме робить кожен параметр:
| Параметр | Що означає простими словами | Типове значення для PostgreSQL |
|---|---|---|
|
Команда, яка перевіряє стан сервісу | pg_isready та подібні перевірки |
|
Як часто повторювати перевірку | Кожні 5–10 секунд |
|
Скільки максимум чекати виконання однієї перевірки | 2–5 секунд, щоб не зависнути |
|
Скільки послідовних провалів потрібно, щоб визнати контейнер unhealthy | 5–10 спроб, залежно від середовища |
|
«Пільговий період» після старту, коли провали ще не вважаються провалами | 5–30 секунд: база має право «прокинутися» |
Чому саме start_period такий важливий? Тому що PostgreSQL на старті може робити корисні речі: ініціалізувати каталог даних, підняти внутрішні структури, створити користувача й базу за env-змінними, якщо це перший старт і volume порожній. У цей момент pg_isready цілком чесно може казати «не готова», і це нормально. Ми не хочемо, щоб контейнер одразу отримував unhealthy лише тому, що він ще стартує.
При цьому важливо не впасти в іншу крайність: поставити start_period: 5m, а retries: 100, а потім дивуватися, чому стенд «висить» і ви не розумієте, чи база справді не стартує. Healthcheck має допомагати діагностиці, а не ховати проблему під килим.
Для локального app + postgres стенда такого набору зазвичай достатньо: перевірка йде регулярно, база встигає прокинутися, а healthcheck не перетворюється ні на нервовий, ні на сонний.
5. pg_isready у compose.yaml
Тепер давайте зберемо той фрагмент compose.yaml, який справді відповідає за readiness PostgreSQL. Припустімо, що для сервісу postgres уже задано POSTGRES_DB, POSTGRES_USER і POSTGRES_PASSWORD. Для поточного завдання нам потрібен лише healthcheck:
services:
postgres:
healthcheck:
# test — це команда, яку Docker регулярно запускатиме всередині контейнера
test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]
# interval — як часто перевіряти readiness
interval: 5s
# timeout — скільки максимум чекати одну перевірку, щоб не зависати
timeout: 3s
# retries — скільки послідовних провалів потрібно для статусу unhealthy
retries: 10
# start_period — пільговий час на старт, коли провали ще не "штрафуються"
start_period: 10s
Значення catalog тут мають збігатися з тими POSTGRES_USER і POSTGRES_DB, які вже живуть у вашому сервісі postgres.
Зверніть увагу на кілька деталей. По-перше, ми використовуємо форму test: ["CMD", ...]. Це exec-форма: Docker запускає команду безпосередньо, без оболонки sh -c. Для нас це добре, бо менше магії й менше сюрпризів із лапками та пробілами.
По-друге, ми явно вказуємо користувача й базу. Так, це трохи жорсткіше, ніж використовувати змінні, але зате студенту простіше зрозуміти, що саме перевіряється. А головне — ця перевірка синхронізована з тим, що вже задано в environment.
Іноді вам трапиться більш «універсальний» варіант, де healthcheck бере параметри з env-змінних контейнера. Він виглядає подібно, але містить один підводний камінь: Compose любить підстановку змінних, тому всередині команди потрібно акуратно екранувати $, щоб змінна інтерпретувалася всередині контейнера, а не на хості.
Ось приклад «змінного» варіанта, який я показую як довідку, бо ви його побачите в інтернеті:
healthcheck:
# CMD-SHELL потрібен, щоб змінні розкривалися в shell усередині контейнера
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Тут CMD-SHELL означає, що команда виконуватиметься через shell, і змінні можуть розкриватися. А подвійний долар $${...} — це спосіб сказати Compose: «не підставляй це на хості, залиш один $ всередині контейнера». На перших кроках це легко сплутати, тому для навчального baseline мені подобається варіант із явними catalog/catalog. Але корисно знати, чому в чужих прикладах ви бачите ці подвійні долари: це не чорна магія, а просто конфлікт двох систем підстановки.
6. Спостерігаємо healthcheck: ps і inspect
Після того як ви додали healthcheck, хочеться побачити, що він справді працює. І тут на новачків чекає маленький сюрприз: healthcheck сам по собі зазвичай не пише в логи. Він тихо виконується «під капотом», а результат відображається у стані контейнера.
Найпростіший і найдружніший спосіб перевірити — подивитися статуси сервісів:
# Показує список контейнерів і їхній поточний статус, включно з (healthy)/(unhealthy)
docker compose ps
Вам потрібно побачити, що postgres став healthy. На старті це виглядає приблизно так: спочатку контейнер Up, потім через кілька успішних перевірок — Up (healthy). Якщо база не стартувала або healthcheck написано неправильно, ви побачите unhealthy.
Якщо потрібно зрозуміти, чому він unhealthy, можна зазирнути в подробиці через inspect. Для сервісів, піднятих Compose, найпростіше взяти container id із docker compose ps і далі:
# Виводимо подробиці healthcheck: історію спроб, exit code, stdout/stderr
docker inspect <container_id> --format '{{json .State.Health}}'
Ви отримаєте JSON з історією спроб, часом, кодами повернення і, іноді, stdout/stderr команди. Це корисно, коли в YAML усе виглядає «правильно», а статус уперто unhealthy. Наприклад, буває, що ви написали команду з опискою (pg_isredy — класика жанру), і тоді healthcheck завжди падає просто тому, що такої команди не існує.
І тут є ще один важливий момент, який варто прямо проговорити: healthcheck перевіряє readiness зсередини самого контейнера. Тобто команда запускається всередині postgres. Це означає, що їй не потрібен «хостовий порт» і не потрібен зовнішній доступ. Вона перевіряє готовність максимально чесно: «сам PostgreSQL усередині себе готовий приймати з’єднання?».
7. Декоративний healthcheck і місце readiness
Коли ви вперше дізнаєтеся про healthcheck, з’являється спокуса «поставити будь-який healthcheck, щоб було красиво». Наприклад, такий:
healthcheck:
# Цей варіант завжди успішний і нічого не перевіряє — приклад того, як робити не слід
test: ["CMD", "true"]
Так, контейнер стане healthy. І так, це виглядає заспокійливо. Але це як поставити датчик диму, який завжди показує «пожежі немає», навіть якщо у вас на кухні смажиться сам Dockerfile. У підсумку ви перестаєте довіряти сигналам середовища й повертаєтеся до хаосу «ніби працює, але іноді падає».
Хороший healthcheck має перевіряти те, що справді потрібно застосунку. У нашому випадку застосунку потрібно під’єднуватися до PostgreSQL. Отже, healthcheck має перевіряти, чи PostgreSQL приймає з’єднання. pg_isready саме це і робить.
Ще одна типова помилка мислення: намагатися вирішити readiness бази на боці застосунку. Починають з’являтися ідеї на кшталт «давайте в app напишемо скрипт очікування», «давайте зробимо sleep 10» — і це саме той випадок, коли фіксована затримка підміняє перевірку стану. Ретраї — корисна техніка, але вони не замінюють readiness-модель середовища. У вас усе одно залишиться питання: а чому середовище не може коректно описати, коли залежність готова?
Readiness — це контракт залежності. У Compose його логічно тримати в самій залежності (postgres) і використовувати далі як інфраструктурний сигнал. Так виходить чиста архітектура: застосунок залишається застосунком, а середовище залишається середовищем. І ніхто не намагається бути «і сервісом, і оркестратором» в одному флаконі.
8. Типові помилки: pg_isready і healthcheck
Помилка №1: використовувати healthcheck, який завжди успішний.
Іноді хочеться «просто увімкнути» healthcheck, і в хід ідуть true, echo ok або навіть exit 0. У підсумку Compose показує healthy, але до реальної готовності PostgreSQL це не має жодного стосунку. Така конфігурація небезпечніша, ніж відсутність healthcheck, бо вона створює хибне відчуття стабільності й ховає справжню проблему race condition.
Помилка №2: перевіряти не ту базу або не того користувача.
Якщо ви вказали в environment POSTGRES_DB: catalog, а в pg_isready перевіряєте -d postgres, ви не перевіряєте той самий readiness, який потрібен застосунку. У кращому разі отримаєте постійний unhealthy і будете «лагодити Docker», хоча потрібно просто синхронізувати параметри. У гіршому — отримаєте успішну перевірку в одному сценарії й падіння в іншому, якщо умови створення бази відрізняються.
Помилка №3: надто агресивні таймінги (timeout/start_period).
Початківці часто ставлять timeout: 1s і start_period: 0s, бо «хочеться швидше дізнаватися». PostgreSQL може стартувати довше за одну секунду, особливо на холодній машині, після очищення volume або при повільному диску. У результаті ви отримаєте unhealthy не тому, що база зламана, а тому, що ви не дали їй чесного часу на старт. Це призводить до дуже дратівливої flaky-поведінки: «іноді зелено, іноді червоно».
Помилка №4: намагатися використовувати env-змінні в test і забути про екранування $.
Коли ви пишете pg_isready -U ${POSTGRES_USER}, Compose може спробувати підставити змінну на боці хоста і замінити її порожнечею. У підсумку команда перетворюється на «перевір readiness без користувача», а ви ловите несподівані результати. Якщо вже використовуєте змінні, робіть це усвідомлено через CMD-SHELL і $${...}, або, у навчальному baseline, використовуйте явні значення catalog.
Помилка №5: думати, що healthcheck автоматично затримає старт app.
Healthcheck сам по собі лише позначає контейнер як healthy/unhealthy. Щоб від цього сигналу був практичний зиск, запуск app потрібно явно прив’язати до service_healthy. Якщо цього зв’язку немає, healthcheck буде лише спостереженням, а не правилом старту.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ