Docker Compose healthcheck 寫法與 depends_on 陷阱
了解 Docker Compose 如何判定 healthcheck,為何單用 depends_on 不會等待服務就緒,並掌握 Postgres 與應用程式的正確 readiness 檢查寫法。
Docker Compose healthcheck 實際執行的內容
Docker Compose healthcheck 是 Docker 依時間間隔在容器內執行的一個命令。Docker 不會讀取日誌、監控連接埠,也不會檢查程序清單。它會執行該命令、讀取結束代碼,並在容器上儲存單一狀態:starting、healthy 或 unhealthy。結束代碼 0 表示狀態正常。任何其他結束代碼都表示狀態異常,而結束代碼 2 已由 Docker 保留,因此切勿刻意返回該代碼。
這就是完整的運作機制。幾乎所有 healthcheck 問題都源自同一原因:您撰寫的命令回答了不同於預期的問題。本指南假設您已知道如何在 VPS 上撰寫 compose 檔案,並從堆疊以錯誤順序啟動的階段開始說明。
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: 30stest 值有兩種實用形式。以 CMD 開頭的清單會直接執行命令,不使用 shell,因此管線、&& 和變數展開都無法運作。以 CMD-SHELL 開頭的清單會將其餘內容作為一個字串,傳遞給容器內的 /bin/sh -c;只要檢查需要 shell 語法,就應使用此形式。純字串會視為 CMD-SHELL。只含 ["NONE"] 的清單會移除映像檔透過 Dockerfile 內建的 healthcheck。
檢查會在容器內執行,因此命令中指定的每個二進位檔都必須存在於該映像檔中。請先確認這一點,因為不含 curl 的精簡映像檔會讓容器永久處於異常狀態,而應用程式日誌中不會顯示原因。請手動測試:
docker compose exec api curl --version缺少二進位檔時會返回 OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown。以 Alpine 為基礎的映像檔通常會改用 BusyBox 的 wget,因此檢查會變成 ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"]。
interval、retries 和 start_period 如何配合
有 5 個設定會控制時間。這些設定的預設值來自 Docker Engine,而不是 Compose。
interval:容器經過啟動期間後,兩次檢查之間的時間。預設為 30s。timeout:一次檢查最多可執行的時間。超過此時間後,Docker 會終止該次檢查,並將其計為失敗。預設為 30s。retries:狀態變更為unhealthy前所需的連續失敗次數。預設為 3。start_period:容器啟動後的寬限期間。預設為 0s。start_interval:啟動期間內執行檢查的頻率。預設為 5s,且需要 Docker Engine 25.0 或更新版本。
關鍵規則如下:在啟動期間內,失敗的檢查不會計入 retries,容器會維持在 starting。檢查首次成功時,容器會變為 healthy,且啟動期間會立即結束,即使該期間尚未用完。如果啟動期間結束時檢查仍然失敗,便會開始正常倒數,容器必須連續失敗 retries 次,才會標記為 unhealthy。
因此,從容器啟動到 unhealthy 的最長時間為 start_period 加上 retries 乘以 interval,再加上 timeout。以上述檔案中的數值計算,即 30 加上 5 乘以 13,結果為 95 秒。設定部署逾時前,請先記下這個數值,因為若 rollout 在 60 秒後放棄,就永遠不會看到此容器進入最終狀態。
常見錯誤是提高 retries 來涵蓋緩慢的啟動過程。這只能解決一次問題,之後會永久造成影響:原本需要 8 次重試才能啟動的服務,現在會在 production 中容許連續失敗 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_successfullycondition 有 3 個值。service_started 與簡寫形式相同。service_healthy 會讓相依服務暫緩啟動,直到相依項回報健康。只有在該相依項定義了 healthcheck 時,這項設定才有意義;定義可以位於 compose 檔案或其映像中。service_completed_successfully 會等待一次性容器結束,且結束狀態必須為 0,例如資料庫遷移容器。
condition 旁邊還有 2 個額外欄位。restart: true 會指示 Compose 在更新相依服務後重新啟動此服務。required: false 會將缺少相依項從錯誤降級為警告。
接下來是容易造成誤解的限制。這些條件會在堆疊啟動時評估。它們只負責啟動順序,不是監督規則。如果資料庫在凌晨 3 點重新啟動,系統不會重新評估 service_healthy,也不會為了再次符合該條件而重新啟動您的應用程式。您的應用程式程式碼仍必須自行重新連線。docker compose up --no-deps api 會刻意略過整個機制,直接使用 docker start 啟動容器時也一樣。
撰寫測試就緒狀態的檢查,而不是測試程序是否存在
像 pgrep nginx 這類檢查只能證明程序表中存在項目。它無法證明服務是否能夠回應要求。Web 應用程式可能在資料庫連線池失效很久後仍保持監聽 socket 開啟,而程序檢查會在整段中斷期間持續顯示正常。
請讓容器執行它本來就要執行的工作:
- 對於 HTTP 服務,請要求實際的端點。
curl -fsS會因為-f而在任何 400 或更高的狀態碼下以非零狀態結束,因此損壞應用程式回傳 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 port 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 與 app 堆疊
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 psSTATUS 欄會在方括號中顯示健康狀態。健康的兩個容器都會在該欄顯示 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 狀態達 1 週,而 Compose 不會處理它。Swarm mode 會取代不健康的工作,但在單一伺服器上執行的普通 Compose 堆疊不會。
這會留下兩個可行的選項。讓程序在確認自身故障時結束,使重新啟動原則有可執行的對象。或者從外部監控狀態,並針對該狀態發出警示。將 Uptime Kuma 監控器指向 healthcheck 呼叫的相同端點,表示故障的相依性會同時顯示在兩處;如此一來,您會先從監控器得知問題,而不是由使用者告知。如果流量是透過 Traefik 反向代理到達應用程式,請記住,代理程式對後端的判定與 Docker 健康狀態彼此獨立,因此其中一者無法取代另一者。
永遠無法變為 healthy 的檢查除錯
在相同的 container 中親自執行完全相同的 command,並查看 exit code:
docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"container 仍回報 unhealthy 時,exit=0 表示 compose test 與您剛才輸入的內容不同,通常是因為在需要 shell 語法的位置使用了 CMD。
其餘問題大多可歸納為兩項錯誤。第一項是使用錯誤的 port。healthcheck 會在 container 內執行,因此必須使用 container port,不能使用 published host port。使用 ports: - "8080:3000" 時,應用程式會在 3000 上監聽;如果檢查 http://localhost:8080,檢查就會永遠失敗,但網站在瀏覽器中仍能正常運作。第二項是使用錯誤的 host。在檢查中,localhost 指的是同一個 container;檢查自身時這是正確的,但檢查鄰近服務時則不正確。此時需要使用 service name,例如 db。
還有一種情況值得特別指出:healthcheck 通過,但使用者仍看到錯誤。這是因為 endpoint 在未實際檢查任何元件的情況下,直接回傳靜態的 200。從未查詢資料庫的 readiness endpoint,無法判斷資料庫是否已中斷。請讓它執行一個成本低且實際的查詢。
FAQ
為什麼 depends_on 顯示資料庫狀態正常,我的應用程式仍然無法連線?
因為 condition: service_healthy 只會在啟動堆疊時評估一次。之後不會再監控任何項目。如果資料庫容器稍後重新啟動,Compose 不會重新啟動應用程式以再次滿足該條件,因此應用程式程式碼必須自行實作重新連線和重試邏輯。使用 docker start 或 docker compose up --no-deps 啟動單一容器時,該條件也不會發揮作用。
如果映像已定義 healthcheck,還需要自行設定嗎?
通常不需要。覆寫它往往反而降低效果,因為映像維護者了解該軟體的就緒條件。只有在映像中的檢查不適用於你的環境時,才應加入自訂檢查,例如檢查的連接埠已被你移動。若要停用映像的 healthcheck,請在服務上設定 test: ["NONE"] 或 disable: true。
healthcheck 應使用 curl 還是 wget?
使用映像中現有的工具,並在依賴它之前透過 docker compose exec <service> curl --version 確認。許多以 Debian 為基礎的映像兩者都沒有。以 Alpine 為基礎的映像包含 BusyBox wget。如果軟體本身提供用戶端,例如 pg_isready 或 redis-cli,不要只為了執行 healthcheck 而在映像中安裝套件。
狀態不正常的容器會自動重新啟動嗎?
在單一主機上,不會由 Docker Engine 自動重新啟動。重新啟動原則會回應程序結束,而不是健康狀態,因此狀態不正常的容器會持續執行並維持故障,直到其他元件採取動作。你可以讓程序在偵測到故障時結束,或執行外部監控器,針對該狀態發出警示。
start_period 應設定多久?
應足以涵蓋你測量到的最慢正常首次啟動時間,並再加上緩衝時間。請使用 docker compose up 在空白 volume 上測量,因為資料庫首次啟動的速度遠慢於後續每次啟動。start period 過長,只會延遲第一次 unhealthy 判定。重試次數過高,會在容器的整個生命週期中削弱檢查效果,這是更嚴重的故障。