一句話結論
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 開著」當唯一判準 |
interval | 30s | 兩次檢查間隔 | 開發 3–5s、生產 10–30s |
timeout | 30s | 單次檢查最長執行時間,逾時視為失敗 | 設 3–5s,避免檢查本身卡住 |
retries | 3 | 連續失敗幾次才標記 unhealthy | 3 次是合理值,資料庫可拉到 5 |
start_period | 0s | 開機寬限期,期間的失敗不計入 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_successfully | migrate 容器 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 處理真正的崩潰。
檢查清單
- 每個有狀態服務(資料庫、快取、訊息佇列)都應該有 healthcheck。
- 有 healthcheck 的服務,依賴它的一方一律用長語法
condition: service_healthy。 start_period至少設成「冷啟動時間 × 2」。- 檢查指令必須存在於映像內——先
docker run --rm <image> sh -c "command -v curl"確認。 - 一次性任務(migration、seed)用
service_completed_successfully,不要用service_started。 - CI 一律用
docker compose up -d --wait,並檢查離開碼。
站內延伸閱讀
(本文所有數字為 2026-11-14 在 Docker 29.2.1/Compose v5.1.0 上實測;參數預設值以 Docker 官方文件為準。)
