pg_isready і healthcheck PostgreSQL

Docker for Spring
Рівень 17 , Лекція 1
Відкрита

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
test
Команда, яка перевіряє стан сервісу pg_isready та подібні перевірки
interval
Як часто повторювати перевірку Кожні 5–10 секунд
timeout
Скільки максимум чекати виконання однієї перевірки 2–5 секунд, щоб не зависнути
retries
Скільки послідовних провалів потрібно, щоб визнати контейнер unhealthy 5–10 спроб, залежно від середовища
start_period
«Пільговий період» після старту, коли провали ще не вважаються провалами 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 буде лише спостереженням, а не правилом старту.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ