一、前言 #
前一篇 Docker Compose:多容器服務編排實戰 裡,我們已經學會用一份 compose.yaml 把 API、資料庫、Redis 串起來。
那很適合入門,也很適合 demo。
但當你真的每天用 Compose 開發,就會遇到更現實的問題:
depends_on只保證容器啟動,不保證資料庫已經 ready- 本機需要 hot reload,production 不該掛 source volume
- Mailpit、Adminer、Jaeger 這些工具不是每次都要開
- CI、dev、prod 混在同一份 YAML 裡,很快會變成毛線球 Docker Compose 進階使用的核心,不是背更多欄位。 真正的核心是:把環境差異變成可讀、可控、可重現的設定。 今天拍拍君會用一個常見 Web app 範例,整理三個實用技巧:
- 用
healthcheck讓服務真的健康後再被使用 - 用
profiles管理可選服務,不再註解 YAML - 用 override file 分離 dev/prod 設定
範例會包含
api、db、redis、worker、mailpit。 不用怕,不會變成 Kubernetes 論文,只是把 Compose 寫得更像可靠的工程設定。🐳
二、準備一個最小範例 #
假設我們有一個常見組合:FastAPI API、PostgreSQL、Redis、背景 worker。
API 至少提供一個 /health endpoint,讓 Compose 可以檢查它是否真的能回應:
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health():
return {"status": "ok"}
容器 image 可以先用簡單的 Python slim image 建起來,今天重點不在 Dockerfile,而在 Compose 如何描述服務關係。
三、depends_on 的陷阱
#
很多人第一次寫 Compose 會這樣:
services:
api:
build: ./api
depends_on:
- db
- redis
db:
image: postgres:16
redis:
image: redis:7
看起來合理,因為 api depends on db,Compose 會先啟動 db。
問題是:容器啟動不等於服務可用。
PostgreSQL 第一次啟動可能還在初始化資料目錄。
Redis 可能正在載入 snapshot。
API 一啟動就連線,然後得到:
connection refused
這時候很多人會在程式裡加 sleep(5)。
拍拍君不推薦,這是膠帶工程。
更好的做法是讓服務自己宣告健康狀態。
四、用 healthcheck 定義健康狀態
#
Docker 的 healthcheck 會在容器內定期執行一個命令。
命令成功,容器就是 healthy;連續失敗,容器會變成 unhealthy。
PostgreSQL 可以用 pg_isready:
services:
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: app
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d app"]
interval: 5s
timeout: 3s
retries: 10
start_period: 10s
Redis 可以用 redis-cli ping:
services:
redis:
image: redis:7
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 10
幾個欄位的意思:
test:實際檢查命令interval:每隔多久檢查一次timeout:單次檢查最多等多久retries:連續失敗幾次後標成 unhealthystart_period:剛啟動時的寬限期 啟動後可以看狀態:
docker compose up -d db redis
docker compose ps
看到 healthy,就比單純看到 Up 有意義多了。
五、等依賴健康後再啟動 API #
有了 healthcheck,就可以讓 api 等 db 和 redis 真的健康後再啟動:
services:
api:
build: ./api
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://app:app@db:5432/app
REDIS_URL: redis://redis:6379/0
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
這樣 API 不會在資料庫還沒 ready 時就衝出去撞牆。
不過要注意:condition: service_healthy 解決的是啟動順序,不是完整故障恢復。
如果資料庫之後突然掛掉,API 不會自動因為它 unhealthy 就重啟。
所以應用程式本身仍然要有 retry、timeout、graceful error handling。
API 自己也可以加 healthcheck:
services:
api:
build: ./api
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health').read()"]
interval: 10s
timeout: 3s
retries: 5
start_period: 10s
這裡故意不用 curl。
很多 slim image 沒有 curl,但 Python image 一定有 Python,用標準庫最省事。
六、用 profiles 管理可選服務
#
開發時你可能想要 Mailpit、Adminer、Jaeger。 但 production 不需要它們。 很多人會用註解當開關:
# adminer:
# image: adminer
# ports:
# - "8080:8080"
今天取消註解,明天註解回去,Git diff 很快就會很醜。
Compose 的 profiles 可以讓可選服務變成真正的開關:
services:
mailpit:
image: axllent/mailpit:latest
ports:
- "8025:8025"
- "1025:1025"
profiles: ["devtools"]
一般啟動:
docker compose up
mailpit 不會啟動。
需要開發工具時:
docker compose --profile devtools up
或用環境變數:
COMPOSE_PROFILES=devtools docker compose up
你也可以分類多個 profile:
services:
adminer:
image: adminer
ports: ["8080:8080"]
profiles: ["devtools"]
jaeger:
image: jaegertracing/all-in-one:latest
ports: ["16686:16686"]
profiles: ["observability"]
loadtest:
image: grafana/k6:latest
command: run /scripts/smoke.js
profiles: ["test"]
小提醒:profile 不是安全邊界。 如果你明確指定某個服務,Compose 還是會啟動它:
docker compose up mailpit
所以它是方便的啟動開關,不是權限系統。
七、用 override file 分離 dev/prod #
不要讓一份 compose.yaml 承擔所有環境。
本機開發常需要:
--reload- 掛 source volume
- expose database port
- debug mode
- Mailpit / Adminer Production 則常需要:
- 關掉 debug
- 使用 prebuilt image
- 不掛 source volume
- 不直接 expose database
- 設定 restart policy 比較乾淨的結構是:
compose.yaml # 共同基礎設定
compose.override.yaml # 本機開發預設載入
compose.prod.yaml # production 明確指定
Docker Compose 會自動載入 compose.override.yaml。
所以本機:
docker compose up
等同於:
docker compose -f compose.yaml -f compose.override.yaml up
Production 則明確指定:
docker compose -f compose.yaml -f compose.prod.yaml up -d
八、基礎檔案:只放共同設定 #
compose.yaml 應該放所有環境都需要的部分:
x-app-env: &app-env
DATABASE_URL: postgresql://app:app@db:5432/app
REDIS_URL: redis://redis:6379/0
services:
api:
build: ./api
environment:
<<: *app-env
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
worker:
build: ./api
command: python -m worker.run
environment:
<<: *app-env
db:
image: postgres:16
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
POSTGRES_DB: app
volumes:
- postgres_data:/var/lib/postgresql/data
redis:
image: redis:7
volumes:
- redis_data:/data
volumes:
postgres_data:
redis_data:
這裡故意不把 ports 放在 base 裡。
因為 port mapping 通常是環境差異,本機要開,production 不一定要開。
x-app-env 是 YAML anchor,能減少重複,但不要過度抽象。
YAML 太聰明的時候,讀起來會很痛苦。真的。
九、開發與 production override #
本機開發的 compose.override.yaml 可以放 hot reload、volume、開發工具:
services:
api:
ports: ["8000:8000"]
volumes:
- ./api:/app
command: uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
environment:
DEBUG: "true"
db:
ports: ["5432:5432"]
mailpit:
image: axllent/mailpit:latest
ports: ["8025:8025", "1025:1025"]
profiles: ["devtools"]
Production 的 compose.prod.yaml 則可以使用已建好的 image:
services:
api:
image: ghcr.io/example/compose-demo-api:${APP_VERSION:?set APP_VERSION}
build: null
restart: unless-stopped
ports:
- "127.0.0.1:8000:8000"
environment:
DEBUG: "false"
worker:
image: ghcr.io/example/compose-demo-api:${APP_VERSION:?set APP_VERSION}
build: null
restart: unless-stopped
db:
restart: unless-stopped
redis:
restart: unless-stopped
部署時:
APP_VERSION=2026.05.02 \
POSTGRES_PASSWORD='change-me' \
docker compose -f compose.yaml -f compose.prod.yaml up -d
幾個重點:
${APP_VERSION:?set APP_VERSION}可以避免忘記設定版本build: null避免 production 臨時 build127.0.0.1:8000:8000只綁 localhost,比直接暴露外網安全restart: unless-stopped適合單機服務,但不是完整 orchestrator
十、合併規則與環境變數 #
多個 -f 檔案不是單純文字合併。
Mapping 通常會 merge,但 ports、volumes 這類 list 容易追加。
所以拍拍君建議:不要把容易因環境改變的 list 放在 base 檔案。
想看真正生效的設定,用:
docker compose config
它會展開變數、合併檔案,讓你看到 Compose 實際理解到的 YAML。 環境變數也要分清楚:
.env:給 Compose CLI 做變數替換env_file:把變數注入容器environment:直接在 YAML 裡注入容器 例如.env:
APP_VERSION=2026.05.02
POSTGRES_PASSWORD=local-secret
可以拿來替換 image tag:
image: ghcr.io/example/api:${APP_VERSION}
但如果容器內也要讀到,就要寫進 environment 或 env_file。
真正的 secret 不要 commit,請 commit .env.example 就好。
結語 #
今天我們把 Docker Compose 從「可以跑」推進到「比較能維護」。 你學到了:
depends_on只管啟動順序,不等於服務 readyhealthcheck可以讓 Compose 理解服務健康狀態condition: service_healthy可以避免 API 太早啟動profiles可以乾淨管理 devtools、debug、test 服務compose.override.yaml很適合本機開發compose.prod.yaml可以讓 production 設定明確又安全docker compose config是排查設定合併問題的神器 Compose 寫得好,專案的開發體驗會差很多。 不是華麗的差異,而是新人少踩坑、CI 少抽風、部署少手抖的差異。 工程品質有時候就藏在這些不起眼的 YAML 裡。 拍拍君覺得,這種小地方最值得花時間整理。🐳