快轉到主要內容
  1. 教學文章/

Docker Compose 進階:healthcheck、profiles 與 dev/prod 設定分離

·6 分鐘· loading · loading · ·
Docker Docker-Compose Devops Container Healthcheck Profiles
每日拍拍
作者
每日拍拍
科學家 X 科技宅宅
目錄
DevOps - 本文屬於一個選集。
§ 4: 本文

featured

一、前言
#

前一篇 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 範例,整理三個實用技巧:
  1. healthcheck 讓服務真的健康後再被使用
  2. profiles 管理可選服務,不再註解 YAML
  3. 用 override file 分離 dev/prod 設定 範例會包含 apidbredisworkermailpit。 不用怕,不會變成 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:連續失敗幾次後標成 unhealthy
  • start_period:剛啟動時的寬限期 啟動後可以看狀態:
docker compose up -d db redis
docker compose ps

看到 healthy,就比單純看到 Up 有意義多了。

五、等依賴健康後再啟動 API
#

有了 healthcheck,就可以讓 apidbredis 真的健康後再啟動:

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 臨時 build
  • 127.0.0.1:8000:8000 只綁 localhost,比直接暴露外網安全
  • restart: unless-stopped 適合單機服務,但不是完整 orchestrator

十、合併規則與環境變數
#

多個 -f 檔案不是單純文字合併。 Mapping 通常會 merge,但 portsvolumes 這類 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}

但如果容器內也要讀到,就要寫進 environmentenv_file。 真正的 secret 不要 commit,請 commit .env.example 就好。

結語
#

今天我們把 Docker Compose 從「可以跑」推進到「比較能維護」。 你學到了:

  • depends_on 只管啟動順序,不等於服務 ready
  • healthcheck 可以讓 Compose 理解服務健康狀態
  • condition: service_healthy 可以避免 API 太早啟動
  • profiles 可以乾淨管理 devtools、debug、test 服務
  • compose.override.yaml 很適合本機開發
  • compose.prod.yaml 可以讓 production 設定明確又安全
  • docker compose config 是排查設定合併問題的神器 Compose 寫得好,專案的開發體驗會差很多。 不是華麗的差異,而是新人少踩坑、CI 少抽風、部署少手抖的差異。 工程品質有時候就藏在這些不起眼的 YAML 裡。 拍拍君覺得,這種小地方最值得花時間整理。🐳

延伸閱讀
#

DevOps - 本文屬於一個選集。
§ 4: 本文

相關文章

Docker Compose:多容器服務編排實戰
·5 分鐘· loading · loading
Docker Docker-Compose Devops Container 部署
Kubernetes 入門:Pod, Service, Deployment 一次搞懂
·10 分鐘· loading · loading
Kubernetes K8s Devops Container Docker Pod Deployment
Docker for Python:讓你的程式在任何地方都能跑
·6 分鐘· loading · loading
Python Docker Container Devops 部署
Helm 入門:Kubernetes 套件管理與 Chart 實戰
·8 分鐘· loading · loading
Helm Kubernetes K8s Devops Chart Yaml
Python argparse 實戰:CLI 參數解析、旗標設計與 subcommands 完全攻略
·9 分鐘· loading · loading
Python Argparse Cli Command-Line Automation Developer-Tools
Rust 多執行緒入門:threads、Mutex、Arc 與共享狀態完全攻略
·10 分鐘· loading · loading
Rust Thread Mutex Arc Concurrency Python