1. Фінальний readiness-baseline
До цього місця проблему вже названо: короткий depends_on упорядковує старт контейнерів, але не чекає, поки PostgreSQL справді почне приймати з’єднання. Тепер зберемо один акуратний, читабельний і відтворюваний фрагмент compose.yaml, який перетворює app + postgres із «інколи запускається» на «запускається передбачувано». Це не про ускладнення заради ускладнення. Це про те, щоб один раз чітко сформулювати правило запуску так, щоб його зрозуміли Docker Compose, ваша майбутня команда і ви самі через два тижні.
Readiness-baseline в контексті сьогоднішнього матеріалу — це дві зчеплені частини. Перша живе у PostgreSQL і каже: «ось як перевірити, що я справді готова». Друга живе у застосунку і каже: «я стартую лише після того, як залежність пройшла перевірку здоров’я». Docker Compose прямо описує проблему: на старті він не чекає, доки сервіс стане “ready”, він чекає лише, доки контейнер буде “running”. Тому для БД потрібні healthcheck + depends_on.condition.
Щоб було зовсім приземлено, ось коротка схема «хто за що відповідає»:
| Рівень | Де це описано | Що гарантує |
|---|---|---|
| Readiness-сигнал БД | postgres.healthcheck | «PostgreSQL справді приймає з’єднання» |
| Очікування readiness | app.depends_on.postgres.condition: service_healthy | «Застосунок не стартує, доки БД не healthy» |
| Підключення до БД | SPRING_DATASOURCE_URL | «Застосунок іде в БД за ім’ям сервісу та портом контейнера» |
2. Фінальний compose.yaml по частинах
Зараз ми зробимо те, що зазвичай називають baseline: мінімальний, але професійний шаблон. Ідея не в тому, щоб написати найкоротший YAML на планеті, а в тому, щоб він читався як сценарій. Добрий compose.yaml схожий на зрозумілу угоду: хто запускається, чого чекає і як зрозуміти, що все справді працює.
Почнемо з частини застосунку. Нам потрібні дві речі: розгорнутий depends_on із service_healthy і конфігурація Spring Boot для postgres-режиму. Важливо, що ми не змінюємо образ і не змінюємо бізнес-код: змінюється лише середовище запуску, як і належить у контейнерному світі.
Фрагмент 1 — app: чекаємо БД і підключаємося
services:
app: # сервіс застосунку
build: .
depends_on:
postgres:
condition: service_healthy # стартуємо лише тоді, коли postgres стане healthy
environment:
SPRING_PROFILES_ACTIVE: postgres # вмикаємо профіль із налаштуваннями під Postgres
# Важливо: `postgres` — це імʼя сервісу в мережі Compose, а `5432` — порт контейнера
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog
Тут ключова думка — у рядку condition: service_healthy: Compose справді вміє чекати проходження healthcheck на залежності перед стартом залежного сервісу. Друга думка — URL підключення: postgres:5432, тобто ім’я сервісу і container port. Якщо ви спробуєте підставити localhost, ви знову повернетеся у світ «чому воно впало». Контейнер усередині себе вважає localhost самим собою, а не сусідньою БД.
У реальному проєкті майже напевно будуть ще SPRING_DATASOURCE_USERNAME і SPRING_DATASOURCE_PASSWORD. Я навмисно не розписую їх у цьому першому фрагменті, щоб око новачка не потонуло в деталях, але логіка проста: ці значення мають збігатися з POSTGRES_USER / POSTGRES_PASSWORD у сервісі бази.
Фрагмент 2 — postgres: healthcheck на pg_isready
Тепер додамо те, що робить “ready” вимірюваним: healthcheck для PostgreSQL. У документації Docker щодо порядку запуску прямо показано підхід: readiness залежності виражається через healthcheck, а залежний сервіс чекає service_healthy.
services:
postgres: # сервіс бази даних
image: postgres:17 # образ Postgres для поточного стенду
environment:
POSTGRES_DB: catalog # імʼя бази, яку створить контейнер під час першого старту
POSTGRES_USER: catalog # користувач БД
POSTGRES_PASSWORD: catalog # пароль користувача БД
healthcheck:
# Команда перевірки готовності: "чи вже можна приймати з’єднання?"
test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]
pg_isready — це маленький «лікар», який запитує у PostgreSQL: «ти вже готова приймати з’єднання, чи ще не випила каву?». У Docker Java guide саме так і радять перевіряти БД: healthcheck на pg_isready і очікування service_healthy з боку застосунку.
Тепер додамо параметри таймінгу. Вони впливають не на правильність ідеї, а на те, наскільки добре вона працюватиме на різних машинах.
services:
postgres:
healthcheck:
interval: 5s # як часто перевіряємо готовність
timeout: 3s # скільки чекаємо відповіді від однієї перевірки
retries: 10 # скільки невдалих перевірок поспіль допустимо
start_period: 10s # "пільговий" час на прогрів перед суворими перевірками
Ці чотири числа — не магічні, але вони мають виражати одну думку: «Дайте базі трохи часу на старт, а потім регулярно перевіряйте, доки не стане добре».
Фрагмент 3 — повний baseline
Коли все разом, виходить приблизно так. Так, це вже більше ніж 10 рядків, але це той випадок, коли компактність гірша за читабельність:
services:
app: # застосунок (Spring Boot)
build: .
depends_on:
postgres:
condition: service_healthy # чекаємо, доки БД стане healthy
environment:
SPRING_PROFILES_ACTIVE: postgres
# Підключаємося за імʼям сервісу в мережі Compose, не за localhost
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/catalog
# Ці облікові дані мають збігатися з POSTGRES_USER/POSTGRES_PASSWORD у postgres
SPRING_DATASOURCE_USERNAME: catalog
SPRING_DATASOURCE_PASSWORD: catalog
ports:
- "8080:8080" # переадресація порту застосунку на хост
postgres: # база даних
image: postgres:17
environment:
POSTGRES_DB: catalog
POSTGRES_USER: catalog
POSTGRES_PASSWORD: catalog
healthcheck:
# Readiness БД: перевіряємо, чи приймає Postgres з’єднання
test: ["CMD", "pg_isready", "-U", "catalog", "-d", "catalog"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Якщо ви зараз дивитеся на YAML і думаєте «чому це все не могло бути однією кнопкою?», то вітаю: ви щойно відчули, чому DevOps-інженери інколи виглядають стомленими. Але добра новина в тому, що це справді невеликий, зрозумілий і типовий baseline.
І так: compose.yaml — канонічна назва файлу, її вважають основним варіантом у моделі Compose.
3. Реальний старт: події та ознаки
Ми часто намагаємося судити про систему за однією ознакою: «контейнер запустився». У випадку бази даних це особливо підступно: контейнер може бути Up, процес живий, але сама СУБД ще не готова до з’єднань. Тому в readiness-моделі ми свідомо дивимося на послідовність: “postgres started → postgres healthy → app started”. Це не філософія, а спосіб перестати залежати від удачі та швидкості ноутбука.
Ось як це можна уявити як сценарій:
sequenceDiagram
participant C as docker compose
participant P as postgres
participant A as "app (Spring Boot)"
C->>P: запуск контейнера
P-->>P: "ініціалізація (старт)"
C->>P: "healthcheck: pg_isready (цикл)"
P-->>C: "healthy (exit code 0)"
C->>A: "запуск контейнера (depends_on: service_healthy)"
A-->>P: підключення jdbc:postgresql://postgres:5432/catalog
A-->>C: "запущено успішно"
Якщо схема здається занадто «розумною», можна звести все до одного простого спостереження: застосунок не має падати на старті з Connection refused лише тому, що БД стартує на кілька секунд довше.
Добра логіка старту майже завжди читається в логах як людська історія. Приблизно так:
postgres-1 | database system is ready to accept connections
app-1 | Started CatalogApplication in 4.123 seconds
Це не декоративна краса, а головний результат дня: стек стартує стабільно, тому що очікування виражене через перевірюваний стан, а не через «давайте просто почекаємо 20 секунд і сподіватимемося».
4. Діагностика readiness-baseline
Сильна сторона readiness-baseline в тому, що він не лише працює, а й добре спостерігається. Ми не запускаємо стек навмання і не ворожимо на кавовій гущі. Ми дивимося на статуси та логи. І для цього нам не потрібен цілий стек спостережуваності, достатньо двох команд, які ви вже бачили: docker compose ps і docker compose logs.
Ось канонічний мінімальний набір. Це справді варто тримати в голові як «відкрити капот»:
docker compose ps # дивимося статуси контейнерів і healthcheck (healthy/unhealthy)
docker compose logs -f app postgres # читаємо логи застосунку та БД одночасно
У docker compose ps вам цікавий не лише стовпчик “Up”, а саме здоров’я. Коли у сервісу є healthcheck, статус змінюється і починає нести зміст. У Docker-документації цей зміст описано буквально: service_healthy означає, що залежність має бути “healthy”, а “healthy” визначається healthcheck.
Важливий психологічний момент: якщо ви читаєте лише логи застосунку, ви майже гарантовано почнете лікувати не те. Наприклад, зміните SPRING_DATASOURCE_URL, хоча проблема була не в URL, а в часі старту БД. Коли ви дивитеся логи app і postgres одночасно, причинно-наслідковий зв’язок стає нудно очевидним. А «нудно очевидно» — це, взагалі-то, комплімент інфраструктурі.
Можна навіть тримати в себе маленьку таблицю інтерпретації сигналів:
| Що ви бачите | Що це означає | Що робити мозком |
|---|---|---|
| postgres ... Up (healthy) | БД перевірку пройшла | Застосунок має право стартувати |
| postgres ... Up без (healthy) | healthcheck не задано або не працює | Не можна використовувати service_healthy як гарантію |
| app ... Exited (1) | Застосунок упав під час старту | Одразу дивимося логи app і postgres разом |
| postgres ... (unhealthy) | healthcheck падає | Проблема або в параметрах перевірки, або в самій БД |
5. Параметри healthcheck
Числа в healthcheck не магічні. Для локального стенду app + postgres цей набір робить саме те, що нам потрібно: перевірка йде досить часто, одна повільна спроба не оголошує базу зламаною, а start_period дає їй спокійно пережити холодний старт.
Якщо на вашій машині PostgreSQL справді прокидається довше, коригувати варто саме ці параметри, а не додавати fixed-delay у запуск застосунку.
Критерій якості baseline
Добра ознака — стек можна підняти кілька разів підряд, і app більше не падає випадковим Connection refused на старті. Після цього проблема зміщується з таймінгу на життєвий цикл даних: уже важливо не «вгадати readiness», а нормально переживати міграції, скидання і відновлення локальної БД.
6. Типові помилки під час налаштування readiness-baseline
Помилка № 1: healthcheck написано «для галочки».
Іноді трапляється test: ["CMD", "true"] або щось подібне, що завжди успішне. Це гірше, ніж відсутність healthcheck, тому що створює ілюзію надійності: Compose миттєво почне вважати сервіс “healthy”, а застосунок стартує і впаде так само, як і раніше. Readiness має вимірювати саме готовність PostgreSQL приймати з’єднання, і pg_isready — якраз такий практичний індикатор.
Помилка № 2: depends_on залишився в короткій формі.
Коротка форма depends_on: - postgres задає порядок, але не готовність. У результаті ви ніби «зробили залежність», але race condition лишається. Потрібен саме довгий запис із condition: service_healthy, тому що Compose чекає проходження healthcheck лише для залежностей, позначених як service_healthy.
Помилка № 3: застосунок підключається до localhost або до host-порту.
Це класична звичка після локального запуску без Docker: «БД же на localhost». У Compose-мережі localhost усередині контейнера — це сам контейнер застосунку, а не база. Тому JDBC URL усередині Compose має посилатися на ім’я сервісу postgres і container port 5432. Якщо ви підставляєте host port, ви фактично намагаєтеся обійти мережу Compose і повертаєте в систему зайву крихкість.
Помилка № 4: YAML-відступи зʼїли ваш condition, і ви цього не помітили.
Це тиха біль: Compose не завжди кричить так, як вам хочеться, а людина очима легко пропускає один пробіл. Коли condition лежить не під тим ключем, Compose просто сприймає depends_on як звичайний список і не чекає healthcheck. Якщо раптом знову зʼявився Connection refused на старті — першою справою дивіться не в Java-код, а в структуру YAML.
Помилка № 5: занадто агресивні timeout і start_period.
Якщо поставити start_period: 0s і timeout: 1s, ви отримаєте “unhealthy” навіть на нормальній базі, просто тому, що вона не встигла стартувати, а перевірка занадто нервова. У документації Docker щодо порядку запуску прямо показано, що start_period нерідко роблять помітним, щоб healthcheck був чесним щодо стартової фази БД.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ