SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 已更新 2026-08-07

Docker Compose healthcheck 正確寫法與 depends_on

了解 Docker Compose 如何判定 healthcheck,為何單用 depends_on 不會等待服務就緒,並掌握 Postgres 與應用程式的實用 readiness 檢查寫法。

Docker Compose healthcheck 的實際作用

Docker Compose healthcheck 是一個由 Docker 在容器內定期執行的命令。Docker 不會讀取日誌、監控連接埠或檢查程序清單。它會執行該命令、讀取結束代碼,並在容器上儲存單一狀態:startinghealthyunhealthy。結束代碼 0 代表健康。任何其他結束代碼都代表不健康;結束代碼 2 由 Docker 保留,因此切勿刻意回傳該代碼。

這就是完整機制。幾乎所有 healthcheck 問題都源自同一原因:你撰寫的命令回答了不同於預期的問題。本指南假設你已經知道如何在 VPS 上撰寫 compose 檔案,並從 stack 以錯誤順序啟動的階段開始說明。

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

test 值有兩種實用形式。以 CMD 開頭的清單會直接執行命令,不使用 shell,因此 pipe、&& 和變數展開都不會生效。以 CMD-SHELL 開頭的清單會將其餘內容作為單一字串,傳給容器內的 /bin/sh -c;只要檢查需要 shell 語法,就應使用此形式。純字串會被視為 CMD-SHELL。只有 ["NONE"] 的清單會移除 image 透過 Dockerfile 內建的 healthcheck。

檢查會在容器內執行,因此命令中指定的每個 binary 都必須存在於該 image 中。請先確認這一點,因為不含 curl 的精簡 image 會讓容器永久處於不健康狀態,而應用程式日誌中不會顯示原因。請手動測試:

docker compose exec api curl --version

找不到 binary 時會回傳 OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown。以 Alpine 為基礎的 image 通常會改用 BusyBox wget,因此檢查命令會變成 ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"]

interval、retries 與 start_period 如何搭配

5 個設定會控制時序。這些預設值來自 Docker Engine,不是 Compose。

  • interval:容器經過 start period 後,兩次檢查之間的間隔。預設為 30s。
  • timeout:單次檢查最多可執行多久;超過後 Docker 會終止該次檢查,並將其計為失敗。預設為 30s。
  • retries:狀態變更為 unhealthy 前所需的連續失敗次數。預設為 3。
  • start_period:容器啟動後的寬限期間。預設為 0s。
  • start_interval:start period 期間執行檢查的頻率。預設為 5s,且需要 Docker Engine 25.0 或更新版本。

關鍵規則是:在 start period 期間,失敗的檢查不會計入 retries,容器會維持在 starting。檢查首次成功時,容器會變為 healthy,並立即結束 start period,即使該期間尚未用完。如果 start period 結束時檢查仍持續失敗,便會開始正常倒數;容器必須連續失敗 retries 次,才會標記為 unhealthy

因此,從容器啟動到 unhealthy 的最長時間為 start_period 加上 retries 乘以 interval,再加上 timeout。以上方檔案中的值計算,就是 30 加上 5 乘以 13,結果為 95 秒。設定部署逾時前,請先記下這個數字,因為若 rollout 在 60 秒後放棄,就永遠等不到此容器進入最終狀態。

常見錯誤是提高 retries 來因應緩慢的啟動程序。這只能解決一次問題,之後卻會持續造成影響:原本啟動需要 8 次重試的服務,現在會在正式環境中容許連續失敗 8 次,其他機制才會察覺。請改用 start_period,因為它只會在首次成功前生效。

為什麼單獨使用 depends_on 什麼也無法保證

depends_on 的簡寫形式是大多數混淆的來源。

  api:
    depends_on:
      - db

這只代表一件事:先啟動 db 容器,再啟動 api 容器。Compose 會等待容器建立並啟動,但不會等待 PostgreSQL 完成首次初始化,也不會等待連接埠 5432 開始接受連線。您的應用程式約在 1 秒後啟動,連線到尚未有服務監聽的連接埠,然後結束。您會在日誌中看到 Connection refused;如果伺服器已啟動但仍在復原,則會看到 FATAL: the database system is starting up

長格式才是多數人真正需要的形式:

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

condition 有 3 個值。service_started 與簡寫形式相同。service_healthy 會讓相依服務保持等待,直到相依服務回報健康;但這只有在相依服務定義了 healthcheck 時才有意義。該定義可以位於 compose 檔案或其映像中。service_completed_successfully 會等待一次性容器,例如資料庫 migration,並確認其以狀態碼 0 結束。

condition 旁邊還有 2 個欄位。restart: true 會在相依服務更新後,告知 Compose 重新啟動此服務。required: false 會將缺少相依服務的情況從錯誤降級為警告。

接下來是容易被忽略的限制。這些條件只會在 stack 啟動時評估。它們負責啟動順序,不是監督規則。如果資料庫在凌晨 3 點重新啟動,系統不會重新評估 service_healthy,也不會為了再次滿足該條件而重新啟動您的應用程式。應用程式程式碼仍必須自行重新建立連線。docker compose up --no-deps api 會依設計略過整套機制,使用 docker start 直接啟動容器時也是如此。

撰寫檢查以測試就緒狀態,而不是測試程序是否存在

pgrep nginx 這類檢查只能證明程序表中存在項目,無法證明服務能否回應請求。Web 應用程式的資料庫連線集區停止運作後,仍可能長時間保持監聽 socket 開啟,而程序檢查在整段中斷期間都會顯示正常。

請讓容器執行它所提供的實際工作:

  • 對 HTTP 服務,請求實際的端點。curl -fsS 遇到 400 或以上的任何狀態碼時,會因 -f 而以非零狀態結束,因此應用程式故障時回傳 500 也會使檢查失敗。
  • 對 PostgreSQL,使用 pg_isready。伺服器接受連線時會以 0 結束,拒絕連線時以 1 結束,完全沒有回應時以 2 結束,而傳入參數錯誤時以 3 結束。
  • 對 Redis,使用 redis-cli ping。此命令會輸出 PONG,並以 0 結束。
  • 對 MariaDB,官方映像檔包含 healthcheck.sh 指令碼,而 healthcheck.sh --connect --innodb_initialized 是其維護者文件中記載的用法。

pg_isready 有一個值得注意的陷阱。官方 postgres 映像檔第一次以空白資料目錄啟動時,會對暫存伺服器執行初始化,而該伺服器只監聽 Unix socket。未指定主機引數的 pg_isready 會使用該 socket,因此即使 TCP 埠 5432 對應用程式仍關閉,它仍可能回報「接受連線」。請明確將檢查指向 TCP,即可排除這個問題,因為暫存伺服器不會在該處回應。

    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

兩個連續的 dollar sign 並非筆誤。Compose 讀取檔案時會自行展開 $VAR,這會將主機環境中的值直接寫入檢查設定。$$ 會將其轉義為單一 $,讓容器內的 shell 依容器自身的環境進行展開。

依正確順序啟動的 postgres 與應用程式堆疊

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:

啟動堆疊並觀察狀態變化:

docker compose up -d
docker compose ps

STATUS 欄會在方括號中顯示健康狀態。健康的兩個服務列都會顯示 Up 41 seconds (healthy)。資料庫仍在初始化時,db 會顯示 Up 4 seconds (health: starting),而清單中不會有 api,因為 Compose 尚未建立該服務。

若要查看檢查通過或失敗的原因,請讀取健康狀態日誌:

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

Docker 會保留最近幾次結果。每筆結果包含開始時間、結束時間、ExitCode,以及命令的 Output。儲存的輸出長度有限,因此會大量輸出頁面內容的檢查只會產生無用的日誌項目。請讓檢查保持簡短。

Docker 在容器變成不健康時的行為

什麼都不會發生。這是最讓人意外的答案。

單一主機上的 Docker Engine 不會重新啟動不健康的容器。restart: unless-stopped 原則會在主要程序結束時做出反應,而不健康的容器並未結束。它可能在 unhealthy 狀態停留一週,Compose 卻不會處理它。Swarm mode 會替換不健康的工作,但單一伺服器上的一般 Compose stack 不會。

這留下兩個實際可行的選項。讓程序在確認自身故障時結束,讓重新啟動原則有機會作用。或者從外部監看狀態,並針對該狀態發出警示。將 Uptime Kuma 監看項目指向 healthcheck 呼叫的相同端點,可讓相依服務故障同時出現在兩處;如此一來,你會先從監看系統得知問題,而不是由使用者告知。如果流量是透過 Traefik 反向代理到達應用程式,請記住,代理對後端的判斷與 Docker health 狀態彼此獨立,因此其中一者無法取代另一者。

偵錯永遠無法變成 healthy 的檢查

在相同的 container 中自行執行完全相同的命令,並查看結束代碼:

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

container 仍回報 unhealthy 時,exit=0 表示 compose test 與剛才輸入的內容不同,通常是因為使用了 CMD,而不是需要的 shell 語法。

其餘大多數問題源自兩個錯誤。第一個是連接埠錯誤。healthcheck 在 container 內執行,因此必須使用 container port,不能使用 published host port。使用 ports: - "8080:3000" 時,應用程式會監聽 3000;若檢查指向 http://localhost:8080,即使網站在瀏覽器中正常運作,檢查也會永遠失敗。第二個是主機錯誤。在檢查內,localhost 指的是同一個 container;檢查自身時這是正確的,但檢查其他 container 時則不正確。此時應使用 service name,例如 db

最後還有一種情況值得特別指出:healthcheck 通過,但使用者仍看到錯誤。這通常是因為 endpoint 未執行任何實際檢查,只回傳靜態的 200。若 readiness endpoint 從未查詢資料庫,就無法反映資料庫是否已中斷。請讓它執行一個成本低且實際的查詢。

FAQ

為什麼 depends_on 顯示資料庫狀態正常時,應用程式仍無法連線?

因為 condition: service_healthy 只會在 stack 啟動時評估一次。之後不會持續監控。如果資料庫容器稍後重新啟動,Compose 不會為了再次滿足該條件而重新啟動應用程式,因此應用程式程式碼必須自行實作重新連線與重試邏輯。使用 docker startdocker compose up --no-deps 啟動單一容器時,該條件也不會生效。

如果 image 已經定義 healthcheck,還需要自行設定嗎?

通常不需要。覆寫它通常反而會退步,因為 image 維護者最清楚該軟體的就緒條件。只有在 image 的檢查不符合你的環境時,才應自行新增,例如檢查的連接埠已變更。若要停用 image 的 healthcheck,請在服務上設定 test: ["NONE"]disable: true

healthcheck 應使用 curl 還是 wget?

使用 image 中已存在的工具,並在依賴它之前透過 docker compose exec <service> curl --version 確認。許多以 Debian 為基礎的 image 兩者都沒有。以 Alpine 為基礎的 image 具備 BusyBox wget。如果軟體本身提供 client,例如 pg_isreadyredis-cli,不要只為了執行 healthcheck 而在 image 中安裝套件。

狀態不正常的容器會自動重新啟動嗎?

在單一主機上,Docker Engine 不會自動執行此操作。重新啟動政策會回應程序結束,而不是健康狀態,因此狀態不正常的容器會持續執行且維持故障,直到其他元件採取行動。你可以讓程序偵測到故障時結束,或執行外部監控器,在狀態變更時發出警示。

start_period 應設定多久?

應長到足以涵蓋你實測的最慢正常首次啟動時間,並額外保留緩衝。使用 docker compose up 在空白 volume 上測量,因為資料庫的首次啟動速度遠慢於後續每次啟動。start period 過長只會延後首次 unhealthy 判定。重試次數過高則會削弱整個容器生命週期中的檢查,這是更嚴重的故障。

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