Docker 中級 #4 compose 深掘り — depends_on, healthcheck, profiles

読了 7分

#3 で web + db を一つのファイルに入れて回しました。この記事はその上に 運用感覚 を加える段階です。healthcheck、depends_on の condition、profiles、override ファイルまで。

Docker 中級 シリーズでこの記事の位置:

depends_on だけでは足りない場面 #

基礎の例で見た単純な形:

単純な depends_on
services:
  web:
    depends_on:
      - pg
  pg:
    image: postgres:16

この定義は pg コンテナが起動した後に web を起動 するという意味です。それ以上ではありません。pg が起動したからといって PostgreSQL デーモンが listen を始めたわけではありません。最初の起動時にはデータディレクトリ初期化に数秒かかるので、その間に web が先に立ち上がって接続を試みると — connection refused で死にます。

これを一行で整理すると: コンテナ開始 ≠ サービス準備完了。

このギャップを埋める道具が healthcheck です。

healthcheck — 本当に準備できたか #

healthcheck はコンテナの中で周期的にコマンドを回して、コンテナが healthy か unhealthy か を Docker が判断できるようにします。

postgres healthcheck
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 10s

各フィールドの意味:

フィールド意味
testどのコマンドで検査するか
interval検査周期
timeout検査コマンドのタイムアウト
retries何回連続失敗したら unhealthy と見るか
start_period起動グレース — この時間中の失敗は retries に数えない

test の形式は二つ:

test の形式
test: ["CMD", "pg_isready", "-U", "app"]      # exec form (直接実行)
test: ["CMD-SHELL", "pg_isready -U app"]      # shell 経由実行 (パイプ、$ 変数可)
test: ["NONE"]                                # healthcheck 無効化

CMD-SHELL の方が柔軟なのでよく使います。

よく使う healthcheck パターン #

様々なサービスの healthcheck
# Postgres
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"]

# MySQL
healthcheck:
  test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]

# Redis
healthcheck:
  test: ["CMD", "redis-cli", "ping"]

# 一般 HTTP サービス (curl があるとき)
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]

# wget がある alpine ベース
healthcheck:
  test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8000/health"]

Postgres healthcheck の $$ 表記に注意: compose.yaml の $VAR はホストの環境変数に置換されます。コンテナの中の 環境変数をそのまま渡すには $$ でエスケープしないといけません。上の例で $$POSTGRES_USER はコンテナの中の $POSTGRES_USER になります。

状態を見る #

healthcheck 状態確認
docker compose ps
# NAME       STATUS
# myapp-pg-1   Up 12 seconds (healthy)
# myapp-web-1  Up 5 seconds

docker inspect myapp-pg-1 --format '{{json .State.Health}}' | jq
# {
#   "Status": "healthy",
#   "FailingStreak": 0,
#   "Log": [...]
# }

Statusstartinghealthy または unhealthy に変わります。コンテナの中で healthcheck コマンドが終了コード 0 を返せば healthy、0 でなければそのカウントが retries まで積まれて unhealthy。

depends_on の condition — 意味のある依存 #

healthcheck があれば、depends_on もより厳密に定義できます。

condition 使用
services:
  web:
    depends_on:
      pg:
        condition: service_healthy
      redis:
        condition: service_started
      migrate:
        condition: service_completed_successfully

  pg:
    image: postgres:16
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app"]
      interval: 5s
      retries: 10

  migrate:
    image: myapp:latest
    command: python manage.py migrate
    depends_on:
      pg:
        condition: service_healthy
    restart: "no"

  redis:
    image: redis:7-alpine

三つの condition:

condition意味
service_startedコンテナが開始されれば OK (デフォルト)
service_healthyhealthcheck が healthy になれば OK
service_completed_successfullyコンテナが終了コード 0 で終われば OK

上の例で web は — pg が healthy で、migrate が成功で終わって、redis が開始されたときだけ立ち上がります。最初の起動時のマイグレーション → アプリ開始の流れが自然に整理されます。

service_completed_successfully パターン #

マイグレーション / シード / 静的ファイルビルドのような 一回限りのコンテナ を定義するパターンがよく使われます。

一回限りの作業コンテナ
services:
  collectstatic:
    image: myapp:latest
    command: python manage.py collectstatic --noinput
    volumes:
      - static:/app/static
    restart: "no"

  web:
    image: myapp:latest
    depends_on:
      collectstatic:
        condition: service_completed_successfully
    volumes:
      - static:/app/static:ro

collectstatic コンテナが静的ファイルを集めた後終了し、その結果を named volume で web が読みに行く流れです。運用 / ステージング環境でよく見るパターン。

restart — 死んだら再起動 #

運用用サービスには自動再起動方針を付けます。

意味
no (デフォルト)再起動しない
always常に — Docker 再起動時にも
on-failure[:N]終了コード 0 ではないときだけ、最大 N 回
unless-stoppedユーザが明示的に stop したのでなければ常に
restart 方針
services:
  web:
    image: myapp:latest
    restart: unless-stopped

  migrate:
    image: myapp:latest
    command: migrate
    restart: "no"      # 一回限りなので再起動されると困る

運用では普通 unless-stopped が安全なデフォルトです。always はユーザが意図的に止めても再び立ち上がってたまに邪魔で、on-failure は正常終了 (例: PID 1 が意図的に exit) までは生かせません。

profiles — 一つのファイルの中で環境分岐 #

同じ compose.yaml なのに dev では mailhog を立ち上げ、prod では立ち上げないとか、テストの時だけ追加サービスを起動したいとき — 昔はファイルを複数に分けて管理していました。最近は profiles がこれを綺麗に解いてくれます。

profiles 使用
services:
  web:
    image: myapp:latest
    # profile が付いていないものは常にオン

  pg:
    image: postgres:16
    # 常にオン

  mailhog:
    image: mailhog/mailhog
    ports:
      - "8025:8025"
    profiles:
      - dev

  pgadmin:
    image: dpage/pgadmin4
    profiles:
      - dev
      - debug

  load-test:
    image: locust
    profiles:
      - test
プロファイル有効化
docker compose up                        # デフォルトだけ (web、pg)
docker compose --profile dev up          # web、pg、mailhog、pgadmin
docker compose --profile test up         # web、pg、load-test
COMPOSE_PROFILES=dev,debug docker compose up

プロファイルが付いていないサービスは常に起動して、プロファイルが付いたサービスはそのプロファイルが明示されたときだけ起動します。一つのサービスに複数のプロファイルを書けば OR で動作します。

このパターンのおかげで:

  • dev プロファイル — メールの偽 SMTP、DB 管理 UI
  • test プロファイル — 負荷ツール
  • debug プロファイル — デバッグプロキシ、jaeger のような traces

同じ compose.yaml 一つのファイルに全て入って、普段は影響がありません。

Override ファイル — compose.dev.yaml #

profile だけでは解けない場面もあります。たとえば dev では volumes にコードを bind mount し、prod では mount しない場合。同じサービス定義を環境ごとに違う形で持っていきたいときです。

そんなときは override ファイル を置きます。

compose.yaml — 運用ベース
services:
  web:
    image: ghcr.io/curtis/myapp:1.0
    restart: unless-stopped
    environment:
      DATABASE_URL: ${DATABASE_URL}
compose.dev.yaml — dev オーバーライド
services:
  web:
    build: .
    image: myapp:dev
    volumes:
      - ./:/app
    environment:
      DEBUG: "1"
dev セットアップ
docker compose -f compose.yaml -f compose.dev.yaml up

後ろのファイルが前のファイルを deep merge する形でマージされます。同じキーは後ろが勝つ (スカラー / オブジェクト)、リストは普通マージされます。運用用ベースをそのまま置いて dev 時点の差分だけを乗せる流れです。

自動認識 — compose.override.yaml #

ファイル名を compose.override.yaml (または docker-compose.override.yml) にすると 明示なしに自動認識 されます。

自動でマージ
ls
# compose.yaml
# compose.override.yaml

docker compose up
# 二つのファイルが自動でマージされる

ローカル開発マシンごとに違う設定 (例: ホストポート衝突回避) を compose.override.yaml に置いて .gitignore に追加するパターンもあります。

extends — サービス定義の再利用 #

似たサービスが複数あるとき、一つの定義をベースに置いて他が継承するパターン。

extends パターン
services:
  worker-base:
    image: myapp:latest
    environment:
      QUEUE_URL: redis://redis:6379/0
    depends_on:
      redis:
        condition: service_started

  worker-default:
    extends: worker-base
    command: python -m worker --queue default

  worker-priority:
    extends: worker-base
    command: python -m worker --queue priority
    deploy:
      replicas: 3

複数の worker コンテナの共通設定を一度だけ書くことになります。ただし — 同じ効果が YAML anchor (& / *) でも出せて、ツールの可読性面では anchor の方がよく使われます。

YAML anchor — もっと軽い再利用 #

anchor / alias
x-worker-base: &worker-base
  image: myapp:latest
  environment:
    QUEUE_URL: redis://redis:6379/0
  depends_on:
    - redis

services:
  worker-default:
    <<: *worker-base
    command: python -m worker --queue default

  worker-priority:
    <<: *worker-base
    command: python -m worker --queue priority

x- で始まるキーは Compose が無視 (extension) するので一時定義に適しています。&worker-base で anchor を定めて、<<: *worker-base で展開して書けば同じ定義が複数のサービスに綺麗に入ります。

deploy: と Swarm — 一段落だけ #

deploy: というキーを見たことがあるはずです。これは Docker Swarm モードでだけ意味があり、普通の docker compose up ではほとんど無視されます (replicas: 3 のようなものは無視されて一つだけ立ち上がる)。

Kubernetes / クラウドネイティブ環境が一般化されて Swarm はあまり使われなくなりました。Compose は ローカル開発 + 小さな単一ホスト運用 までが sweet spot で、その先は別のツールへ移すのが自然です。

よく出会う落とし穴 #

  • healthcheck コマンドに外部ツールがない — slim/alpine イメージには curl がありません。wget --spider で代替するか、ベースに apk add curl / apt-get install curl
  • healthcheck が速すぎるinterval: 1s のような値はコンテナに負担。普通は 5s ~ 30s
  • depends_on の transitive が動かない感じcondition は直接依存だけ見ます。二段先の依存性は明示が必要。
  • override マージで list が意図と違ってマージされる — list は常に単純な concat ではないことがあります。結果が疑わしければ docker compose config でマージされた結果を出力してみる。
マージされた最終定義を見る
docker compose -f compose.yaml -f compose.dev.yaml config
# 二つのファイルがマージされた結果をそのまま出力 — デバッグにとても有用

docker compose config はよく使うデバッグ道具です。マージされた形が意図と同じかを確認するときに。

まとめ #

この記事で掴んだ絵:

  • healthcheck でコンテナの「準備の有無」を Docker が知れるようにする (pg_isreadyredis-cli ping、HTTP /health)
  • depends_on: condition で意味のある開始順序 — service_healthyservice_completed_successfully
  • restart: unless-stopped が運用用サービスの安全なデフォルト
  • profiles で一つのファイルの中で dev / test / debug 環境を分岐
  • override ファイル (compose.override.yamlcompose.prod.yaml) で環境ごとの差分だけを乗せる
  • docker compose config でマージされた最終定義を検証

次の記事 (#5 環境変数と secrets 管理) では秘密値 — DB パスワード、API キーのようなもの — をイメージに刻まずコンテナに注入する方法、そしてその値を .env と compose の secrets: / BuildKit シークレットで扱うパターンを整理します。

X