SSD Nodes Learn Hosting plans →
Przewodniki Matt ConnorAutor: Matt Connor · Zaktualizowano 2026-08-07

Docker Compose healthcheck: jak poprawnie skonfigurować

Dowiedz się, dlaczego parametr depends_on nie gwarantuje gotowości usług. Poznaj mechanizm działania healthcheck i napisz skuteczne testy readiness dla Postgres oraz aplikacji.

Czym w rzeczywistości jest healthcheck w Docker Compose

Healthcheck w Docker Compose to pojedyncze polecenie, które Docker uruchamia wewnątrz kontenera zgodnie z harmonogramem. Docker nie analizuje logów, nie monitoruje portów ani nie sprawdza listy procesów. Wykonuje polecenie, odczytuje kod wyjścia i zapisuje jeden stan kontenera: starting, healthy lub unhealthy. Kod wyjścia 0 oznacza stan poprawny (healthy). Każdy inny kod oznacza stan niepoprawny (unhealthy), przy czym kod 2 jest zarezerwowany przez Dockera, więc nie należy go zwracać celowo.

To cały mechanizm. Niemal każdy problem z healthcheckiem wynika z tego samego: polecenie odpowiada na inne pytanie, niż zamierzano. Ten przewodnik zakłada znajomość tworzenia plików compose na VPS i skupia się na sytuacjach, w których stos usług uruchamia się w niewłaściwej kolejności.

services:
  api:
    image: ghcr.io/example/api:1.4.0
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s

Wartość test przyjmuje dwie użyteczne formy. Lista zaczynająca się od CMD uruchamia polecenie bezpośrednio, bez powłoki, więc potoki, && oraz rozwijanie zmiennych nie działają. Lista zaczynająca się od CMD-SHELL przekazuje resztę jako jeden ciąg znaków do /bin/sh -c wewnątrz kontenera, co jest wymagane, gdy sprawdzanie wymaga składni powłoki. Zwykły ciąg znaków jest traktowany jako CMD-SHELL. Lista o wartości dokładnie ["NONE"] usuwa healthcheck zdefiniowany w obrazie przez Dockerfile.

Sprawdzanie odbywa się wewnątrz kontenera, więc każdy plik binarny musi istnieć w danym obrazie. Należy to zweryfikować w pierwszej kolejności, ponieważ odchudzony obraz bez curl spowoduje, że kontener będzie trwale w stanie niepoprawnym z powodu, który nigdy nie pojawi się w logach aplikacji. Należy to przetestować ręcznie:

docker compose exec api curl --version

Brakujący plik binarny zwraca OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. Obrazy oparte na Alpine zazwyczaj zawierają BusyBox wget, więc sprawdzanie powinno przyjąć postać ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].

Współdziałanie parametrów interval, retries oraz start_period

Pięć ustawień kontroluje czas wykonywania testów. Ich wartości domyślne pochodzą z Docker Engine, a nie z Compose.

  • interval: czas między dwoma testami po zakończeniu okresu startowego kontenera. Wartość domyślna to 30s.
  • timeout: czas, po którym Docker przerywa test i uznaje go za nieudany, jeśli nie zakończył się powodzeniem. Wartość domyślna to 30s.
  • retries: liczba kolejnych nieudanych testów wymagana do zmiany stanu na unhealthy. Wartość domyślna to 3.
  • start_period: okres karencji po uruchomieniu kontenera. Wartość domyślna to 0s.
  • start_interval: częstotliwość wykonywania testów w trakcie okresu startowego. Wartość domyślna to 5s; wymaga Docker Engine w wersji 25.0 lub nowszej.

Kluczowa zasada: w trakcie okresu startowego nieudany test nie wlicza się do retries, a kontener pozostaje w stanie starting. Gdy test zakończy się powodzeniem po raz pierwszy, kontener przechodzi w stan healthy, a okres startowy kończy się natychmiast, nawet jeśli nie upłynął w całości. Jeśli okres startowy upłynie, a testy nadal kończą się niepowodzeniem, rozpoczyna się standardowe odliczanie i kontener wymaga retries kolejnych nieudanych prób, zanim zostanie oznaczony jako unhealthy.

Najgorszy scenariusz czasu od startu kontenera do unhealthy to start_period plus retries pomnożone przez interval plus timeout. Przy wartościach z powyższego pliku jest to 30 plus 5 razy 13, co daje 95 sekund. Należy zapisać tę wartość przed ustawieniem limitu czasu wdrożenia (deploy timeout), ponieważ proces wdrażania, który przerywa działanie po 60 sekundach, nigdy nie pozwoli kontenerowi osiągnąć stanu końcowego.

Częstym błędem jest zwiększanie retries w celu wydłużenia czasu na powolny start. Działa to jednorazowo, ale powoduje trwałe obniżenie odporności: usługa, która potrzebowała 8 prób do uruchomienia, będzie teraz tolerować 8 kolejnych awarii w środowisku produkcyjnym, zanim system zareaguje. Należy używać start_period, ponieważ parametr ten ma zastosowanie wyłącznie przed pierwszym udanym testem.

Dlaczego depends_on samo w sobie niczego nie gwarantuje

Krótka forma depends_on jest źródłem większości nieporozumień.

  api:
    depends_on:
      - db

Oznacza to tylko jedno: uruchom kontener db przed kontenerem api. Compose czeka na utworzenie i uruchomienie kontenera. Nie czeka na zakończenie pierwszej inicjalizacji PostgreSQL ani na moment, w którym port 5432 zacznie akceptować połączenia. Aplikacja uruchamia się sekundę później, próbuje połączyć się z portem, na którym jeszcze nikt nie nasłuchuje, i kończy działanie. W logach widać Connection refused lub FATAL: the database system is starting up, gdy serwer działa, ale wciąż trwa proces odzyskiwania.

Długa forma jest tym, czego użytkownicy faktycznie potrzebują:

  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrate:
        condition: service_completed_successfully

condition przyjmuje trzy wartości. service_started działa tak samo jak forma krótka. service_healthy wstrzymuje usługę zależną do momentu, aż zależność zgłosi stan healthy, co ma znaczenie tylko wtedy, gdy zależność definiuje healthcheck w pliku compose lub w swoim obrazie. service_completed_successfully czeka, aż kontener typu one-shot, na przykład migracja bazy danych, zakończy działanie ze statusem 0.

Obok condition znajdują się dwa dodatkowe pola. restart: true nakazuje Compose zrestartowanie usługi po aktualizacji zależności. required: false zmienia brak zależności z błędu na ostrzeżenie.

Oto ograniczenie, które często sprawia problemy. Warunki te są sprawdzane podczas uruchamiania stosu. Określają one kolejność startu, a nie zasady nadzoru. Jeśli baza danych zrestartuje się o trzeciej nad ranem, nic nie sprawdza ponownie service_healthy i nic nie restartuje aplikacji w celu ponownego spełnienia warunku. Kod aplikacji musi samodzielnie obsługiwać ponowne łączenie. docker compose up --no-deps api z założenia pomija cały ten mechanizm, podobnie jak uruchamianie kontenera bezpośrednio za pomocą docker start.

Wdrożenie testu gotowości zamiast sprawdzania obecności procesu

Test typu pgrep nginx jedynie potwierdza obecność wpisu w tablicy procesów. Nie dowodzi on, czy usługa jest w stanie odpowiedzieć na żądanie. Aplikacja internetowa może utrzymywać otwarte gniazdo nasłuchujące długo po awarii puli połączeń z bazą danych, przez co test procesu pozostaje poprawny mimo trwającej awarii.

Należy wymusić na kontenerze wykonanie zadania, do którego jest przeznaczony:

  • W przypadku usługi HTTP należy odpytać rzeczywisty endpoint. curl -fsS kończy się kodem innym niż zero dla każdego statusu 400 lub wyższego dzięki -f, więc błąd 500 z uszkodzonej aplikacji powoduje niepowodzenie testu.
  • Dla PostgreSQL należy użyć pg_isready, który zwraca 0, gdy serwer akceptuje połączenia, 1, gdy je odrzuca, 2, gdy w ogóle nie odpowiada, oraz 3, gdy przekazane parametry są błędne.
  • Dla Redis należy użyć redis-cli ping, który wypisuje PONG i kończy się kodem 0.
  • Dla MariaDB oficjalny obraz zawiera skrypt healthcheck.sh, a healthcheck.sh --connect --innodb_initialized jest formą zalecaną przez opiekunów obrazu.

pg_isready posiada jedną pułapkę, o której warto wiedzieć. Przy pierwszym uruchomieniu z pustym katalogiem danych, oficjalny obraz postgres przeprowadza inicjalizację na tymczasowym serwerze nasłuchującym wyłącznie na gnieździe Unix. pg_isready bez argumentu hosta korzysta z tego gniazda, więc może odpowiedzieć „akceptuję połączenia”, podczas gdy port TCP 5432 pozostaje zamknięty dla aplikacji. Wskazanie testowi konkretnego portu TCP rozwiązuje problem, ponieważ tymczasowy serwer nie odpowiada na tym porcie.

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

Podwójne znaki dolara nie są błędem. Compose samodzielnie rozwija $VAR podczas odczytu pliku, co spowodowałoby wstrzyknięcie wartości ze środowiska hosta do testu. $$ eskapuje ten zapis do pojedynczego $, dzięki czemu powłoka wewnątrz kontenera rozwija go w oparciu o środowisko samego kontenera.

Stos postgres i aplikacji uruchamiany w odpowiedniej kolejności

services:
  db:
    image: postgres:17.5
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped

  api:
    image: ghcr.io/example/api:1.4.0
    environment:
      DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

volumes:
  pgdata:

Uruchom stos i obserwuj zmiany stanów:

docker compose up -d
docker compose ps

Kolumna STATUS zawiera stan kondycji w nawiasach. Prawidłowo działająca para wskazuje Up 41 seconds (healthy) w obu wierszach. Podczas inicjalizacji bazy danych db wskazuje Up 4 seconds (health: starting), a api nie znajduje się na liście, ponieważ Compose jeszcze go nie utworzył.

Aby sprawdzić przyczynę powodzenia lub niepowodzenia testu, należy odczytać dziennik kondycji:

docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

Docker przechowuje kilka ostatnich wyników, z których każdy zawiera czas rozpoczęcia, czas zakończenia, ExitCode oraz Output polecenia. Zapisane dane wyjściowe są ucinane, więc test wypisujący dużą treść strony wygeneruje bezużyteczny wpis w dzienniku. Testy powinny działać w trybie cichym.

Co robi Docker, gdy kontener przechodzi w stan unhealthy

Nic. To odpowiedź, która najbardziej zaskakuje użytkowników.

Docker Engine na pojedynczym hoście nie restartuje kontenera w stanie unhealthy. Polityka restart: unless-stopped reaguje na zakończenie głównego procesu, a kontener w stanie unhealthy nie zakończył działania. Może on pozostawać w stanie unhealthy przez tydzień, podczas gdy Compose nie podejmie żadnych działań. Tryb Swarm zastępuje zadania w stanie unhealthy, ale zwykły stos Compose na jednym serwerze tego nie robi.

Pozostają dwie uczciwe opcje. Należy sprawić, aby proces kończył działanie, gdy wykryje awarię, dzięki czemu polityka restartu będzie miała podstawę do działania. Alternatywnie można monitorować stan z zewnątrz i otrzymywać powiadomienia. Skierowanie monitora Uptime Kuma na ten sam punkt końcowy, który wywołuje healthcheck, sprawia, że uszkodzona zależność jest widoczna w obu miejscach, a administrator dowiaduje się o niej z monitora, a nie od użytkownika. Jeśli ruch dociera do aplikacji przez reverse proxy Traefik, należy pamiętać, że własny widok proxy na backend jest niezależny od stanu zdrowia w Docker, więc jeden nie zastępuje drugiego.

Diagnostyka sprawdzania, które nigdy nie uzyskuje statusu healthy

Uruchom polecenie samodzielnie wewnątrz tego samego kontenera i sprawdź kod wyjścia:

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

exit=0 w tym miejscu, podczas gdy kontener nadal zgłasza stan unhealthy, oznacza, że plik test różni się od wpisanej komendy. Zazwyczaj wynika to z użycia CMD w miejscu, gdzie wymagana była składnia powłoki.

Większość pozostałych problemów wynika z dwóch błędów. Pierwszym jest błędny port. Healthcheck działa wewnątrz kontenera, więc musi korzystać z portu kontenera, a nie z opublikowanego portu hosta. Przy ports: - "8080:3000" aplikacja nasłuchuje na porcie 3000, a sprawdzanie pod adresem http://localhost:8080 będzie zawsze kończyć się niepowodzeniem, mimo że strona działa poprawnie w przeglądarce. Drugim błędem jest błędny host. Wewnątrz sprawdzania localhost odnosi się do tego samego kontenera, co jest poprawne dla autodiagnostyki, ale błędne przy sprawdzaniu sąsiedniej usługi, gdzie należy użyć nazwy usługi, na przykład db.

Ostatni przypadek zasługuje na uwagę: healthcheck przechodzi pomyślnie, podczas gdy użytkownicy widzą błędy. Dzieje się tak, gdy endpoint zwraca statyczne 200 bez wykonywania żadnych rzeczywistych operacji. Endpoint typu readiness, który nigdy nie odpytuje bazy danych, nie wykryje jej awarii. Należy skonfigurować go tak, aby wykonywał jedno proste, rzeczywiste zapytanie.

FAQ

Why does my app still fail to connect when depends_on says the database is healthy?

Because condition: service_healthy is evaluated once, when the stack starts. It does not supervise anything afterwards. If the database container restarts later, Compose does not restart your application to satisfy the condition again, so your application code needs its own reconnect and retry logic. The condition also does nothing when you start a single container with docker start or with docker compose up --no-deps.

Do I need a healthcheck if the image already defines one?

Usually not, and overriding it is often a step backwards, because the image maintainer knows what readiness means for that software. Add your own only when the image check is wrong for your setup, for example when it probes a port you moved. To turn an image healthcheck off, set test: ["NONE"] or disable: true on the service.

Should the healthcheck use curl or wget?

Use whichever one already exists in the image, and confirm it with docker compose exec <service> curl --version before you rely on it. Many Debian based images have neither. Alpine based images have BusyBox wget. Do not add a package to an image only to run a healthcheck when the software ships its own client, such as pg_isready or redis-cli.

Does an unhealthy container get restarted automatically?

Not by Docker Engine on a single host. Restart policies react to the process exiting, not to the health state, so an unhealthy container stays up and stays broken until something else acts on it. Either make the process exit when it detects the failure, or run an external monitor that alerts on the state.

How long should start_period be?

Long enough for the slowest legitimate first start you have measured, plus a margin. Time it with docker compose up against an empty volume, since the first start of a database is far slower than every start after it. A start period that is too long only delays the first unhealthy verdict. Retries that are too high weaken the check for the whole life of the container, which is the worse failure.

#docker-compose#healthcheck#depends-on#docker#reliability