Docker Compose healthcheck 實戰:等到健康才啟動

容器 Up 不等於服務能用:資料庫還在初始化,API 先啟動再 crash loop。本文實測四種 depends_on 情境,整理四參數行為、三個必踩的坑(alpine 沒 curl、pg_isready 不驗帳密、unhealthy 不重啟),與 CI 用 up --wait 判讀成敗。

  • Dennis
  • 6 分鐘閱讀
Docker Compose healthcheck 實戰:等到健康才啟動

一句話結論

depends_on 只保證「容器啟動順序」,depends_on 加上 condition: service_healthy 才保證「服務可用」;差別在實測裡是 6 秒的等待,與一個永遠起不來的 API。

痛點:Up 不等於能用

docker compose ps 顯示 Up 的時候,容器可能只是「程序跑起來了」。PostgreSQL 第一次啟動要建目錄、跑 initdb、開 listen socket,這段時間內 process 存在、port 卻還沒接。API 若在這時連線,典型結果就是啟動失敗 → 容器退出 → 被 restart policy 拉起來 → 再失敗,形成 crash loop,而日誌裡只看得到 connection refused。

真正要問的問題不是「db 容器起來了嗎」,而是「db 可以接受查詢了嗎」。healthcheck 就是把這個問題變成 Docker 自己會回答的狀態機。

一表看懂四個參數

參數預設作用實務建議
test無檢查指令,離開碼 0 視為健康、1 視為不健康、2 視為保留(Docker 視為不健康)用服務自己的 readiness 指令,不要用「port 開著」當唯一判準
interval30s兩次檢查間隔開發 3–5s、生產 10–30s
timeout30s單次檢查最長執行時間,逾時視為失敗設 3–5s,避免檢查本身卡住
retries3連續失敗幾次才標記 unhealthy3 次是合理值,資料庫可拉到 5
start_period0s開機寬限期,期間的失敗不計入 retries資料庫、JVM 服務一定要設,抓它初始化時間的 2 倍

start_period 是最常被忽略的一個。沒有它,一個正常但要初始化 25 秒的資料庫,會在 interval 3s + retries 3 的設定下於第 12 秒就被標記 unhealthy;有了 start_period: 30s,前 30 秒的失敗不會累積,第 31 秒之後才開始算。

test 的三種寫法

# 1) exec 形式(不經過 shell,訊號與離開碼最乾淨)
healthcheck:
  test: ["CMD", "pg_isready", "-U", "postgres", "-d", "demo"]

# 2) shell 形式(需要變數、管線、`||` 時使用)
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U postgres -d demo || exit 1"]

# 3) 舊式字串(等於 CMD-SHELL,只建議相容舊版時使用)
healthcheck:
  test: pg_isready -U postgres -d demo

CMD 形式不啟動 shell,因此 $PGPASSWORD、&&、重導向全部無效——這是最常見的「明明手動跑得動、放進 healthcheck 就失敗」的原因。

完整可貼的 compose 範例

services:
  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: demo
      POSTGRES_DB: demo
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres -d demo"]
      interval: 3s
      timeout: 3s
      retries: 3
      start_period: 10s

  api:
    image: postgres:17-alpine
    entrypoint: ["sh", "-c"]
    command: ["echo API_STARTED_AT $(date -u +%H:%M:%S); sleep 600"]
    depends_on:
      db:
        condition: service_healthy

關鍵只有兩行:depends_on 用長語法、condition: service_healthy。短語法 depends_on: [db] 完全不看健康狀態,只保證啟動順序。

實測:四種情境的真實差異

以下數字是在 Linux 主機、Docker 29.2.1 + Compose v5.1.0、postgres:17-alpine 上實跑得到的,不是文件推論。

情境設定實測結果
A. 等健康depends_on: {db: {condition: service_healthy}}db 於 20:02:44 啟動,api 於 20:02:50 啟動(6 秒後),docker inspect 顯示 db Health=healthy、健康日誌 accepting connections
B. 只等啟動短語法 depends_on: [db]兩個容器同秒啟動;api 日誌第一行就是 db:5432 - no response,接著印出 DB_NOT_READY_YET
C. 健康檢查失敗test: ["CMD", "curl", "-f", "http://localhost:5432"]db 停在 unhealthy(FailingStreak=10),api 始終停在 created 狀態、從未啟動
D. 等一次性任務condition: service_completed_successfullymigrate 容器 20:05:38 執行、退出碼 0;api 於 20:05:42 啟動(任務結束後 4 秒)

情境 C 的 health log 長這樣,值得記住這個訊息,因為它幾乎一定代表「映像裡沒有這個指令」:

OCI runtime exec failed: exec failed: unable to start container process:
exec: "curl": executable file not found in $PATH

三個一定會踩的坑

坑 1:alpine 映像沒有 curl,也沒有 bash

postgres:17-alpine、redis:alpine、nginx:alpine 這類映像基於 Alpine + busybox,實測 command -v curl 回空,只有 wget 與服務自帶的檢查工具。把 curl -f http://localhost 寫進 healthcheck,結果不是「服務不健康」,而是檢查指令本身不存在——兩者在 UI 上都顯示 unhealthy,但排查方向完全不同。

替代寫法:

# busybox wget
test: ["CMD-SHELL", "wget -q --spider http://localhost:8080/healthz || exit 1"]
# 或用服務自帶工具
test: ["CMD", "pg_isready", "-U", "postgres", "-d", "demo"]

坑 2:pg_isready 回 0 不代表帳密正確

實測在健康的資料庫容器裡跑 pg_isready -U wronguser -d demo,離開碼是 0,輸出仍是 accepting connections;就算指定不存在的資料庫也一樣。原因是 pg_isready 只確認「伺服器願意接受連線」,並不做認證。

也就是說,用 pg_isready 當 healthcheck 只能保證「資料庫進程活著、socket 可連」,不能保證「應用的帳號密碼與資料庫存在」。要驗到這一層,得用實際查詢:

test: ["CMD-SHELL", "PGPASSWORD=$POSTGRES_PASSWORD psql -U postgres -d demo -c 'select 1' > /dev/null || exit 1"]

代價是每次檢查都開一條連線,interval 就不要設得太激進。

坑 3:unhealthy 不會自動重啟,也不會 self-heal

實測讓 db 一直 unhealthy 超過 30 秒,docker inspect 的 RestartCount 仍是 0:Docker Engine 只「標記」狀態,不會因為 unhealthy 而重啟容器。restart: unless-stopped 管的是「進程退出」,不是「健康狀態」。

需要自動替換 unhealthy 容器的場景有三條路:

做法適用注意
Docker Swarm service內建:unhealthy 任務會被替換要接受 Swarm 的部署模型
外部 watchdog(如 autoheal 類工具)單機 compose等於給工具刪容器的權限,要限縮範圍
應用層自己重試+指數退避最常見也最穩讓 API 在 DB 未就緒時等待,而不是直接退出

實務上第三條最值得做:healthcheck 負責「觀測與順序」,重試邏輯負責「容錯」,兩者不是替代關係。

CI 裡怎麼用:--wait 與離開碼

docker compose up -d 在容器 unhealthy 時仍然回 0,CI 會誤判成功。加上 --wait 就會等所有服務達到 healthy(或 running,視有無 healthcheck)才返回,失敗則非 0:

docker compose up -d --wait --wait-timeout 60

實測在情境 C(curl 不存在、db unhealthy)下,指令回傳 exit code 1:

Container hcfail-db-1 Error dependency db failed to start
dependency failed to start: container hcfail-db-1 is unhealthy

這行訊息同時指出兩件事:哪個服務不健康、以及依賴它的服務因此沒有啟動。把它放進 CI 的啟動步驟,就能在部署階段而不是使用者回報時發現問題。

healthcheck、restart policy、入口腳本怎麼分工

機制回答的問題觸發時機不會處理的事
healthcheck服務現在可用嗎?週期性檢查不會重啟、不會修復
restart: unless-stopped進程死了要不要拉起來?進程退出進程活著但功能壞掉
depends_on + condition啟動順序怎麼排?只在 up 當下執行期間服務掉線
入口腳本重試依賴還沒好時怎麼等?應用啟動時執行期間的依賴失效

四者疊起來才是完整的啟動契約:depends_on 排順序、healthcheck 定義「可用」、入口腳本處理殘餘競態、restart 處理真正的崩潰。

檢查清單

  1. 每個有狀態服務(資料庫、快取、訊息佇列)都應該有 healthcheck。
  2. 有 healthcheck 的服務,依賴它的一方一律用長語法 condition: service_healthy。
  3. start_period 至少設成「冷啟動時間 × 2」。
  4. 檢查指令必須存在於映像內——先 docker run --rm <image> sh -c "command -v curl" 確認。
  5. 一次性任務(migration、seed)用 service_completed_successfully,不要用 service_started。
  6. CI 一律用 docker compose up -d --wait,並檢查離開碼。

站內延伸閱讀

(本文所有數字為 2026-11-14 在 Docker 29.2.1/Compose v5.1.0 上實測;參數預設值以 Docker 官方文件為準。)

📬 訂閱 most.tw 電子報

每週精選 AI 工具教學與技術乾貨,直接送到你的信箱。免費、隨時可退訂。

訂閱即表示同意收到 most.tw 電子報,隨時可一鍵退訂。

💬 有問題想討論?加 LINE 聯絡我

歡迎透過 LINE 官方帳號直接留言,我會盡快回覆你的問題。

加入 LINE 好友