Docker 中級 #4 compose 深掘り — depends_on, healthcheck, profiles
#3 で web + db を一つのファイルに入れて回しました。この記事はその上に 運用感覚 を加える段階です。healthcheck、depends_on の condition、profiles、override ファイルまで。
Docker 中級 シリーズでこの記事の位置:
- #1 マルチステージビルドとイメージスリミング
- #2 ビルドキャッシュ — レイヤー順序の最適化
- #3 docker compose 基礎 — web + db
- #4 compose 深掘り — depends_on, healthcheck, profiles ← この記事
- #5 環境変数と secrets 管理
- #6 ロギングとデバッグ
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 が判断できるようにします。
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: ["CMD", "pg_isready", "-U", "app"] # exec form (直接実行)
test: ["CMD-SHELL", "pg_isready -U app"] # shell 経由実行 (パイプ、$ 変数可)
test: ["NONE"] # healthcheck 無効化CMD-SHELL の方が柔軟なのでよく使います。
よく使う 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になります。
状態を見る #
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": [...]
# }Status は starting → healthy または unhealthy に変わります。コンテナの中で healthcheck コマンドが終了コード 0 を返せば healthy、0 でなければそのカウントが retries まで積まれて unhealthy。
depends_on の condition — 意味のある依存
#
healthcheck があれば、depends_on もより厳密に定義できます。
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_healthy | healthcheck が 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:rocollectstatic コンテナが静的ファイルを集めた後終了し、その結果を named volume で web が読みに行く流れです。運用 / ステージング環境でよく見るパターン。
restart — 死んだら再起動
#
運用用サービスには自動再起動方針を付けます。
| 値 | 意味 |
|---|---|
no (デフォルト) | 再起動しない |
always | 常に — Docker 再起動時にも |
on-failure[:N] | 終了コード 0 ではないときだけ、最大 N 回 |
unless-stopped | ユーザが明示的に stop したのでなければ常に |
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 がこれを綺麗に解いてくれます。
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:
- testdocker 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 管理 UItestプロファイル — 負荷ツールdebugプロファイル — デバッグプロキシ、jaeger のような traces
同じ compose.yaml 一つのファイルに全て入って、普段は影響がありません。
Override ファイル — compose.dev.yaml
#
profile だけでは解けない場面もあります。たとえば dev では volumes にコードを bind mount し、prod では mount しない場合。同じサービス定義を環境ごとに違う形で持っていきたいときです。
そんなときは override ファイル を置きます。
services:
web:
image: ghcr.io/curtis/myapp:1.0
restart: unless-stopped
environment:
DATABASE_URL: ${DATABASE_URL}services:
web:
build: .
image: myapp:dev
volumes:
- ./:/app
environment:
DEBUG: "1"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 — サービス定義の再利用
#
似たサービスが複数あるとき、一つの定義をベースに置いて他が継承するパターン。
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 — もっと軽い再利用 #
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 priorityx- で始まるキーは 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_isready、redis-cli ping、HTTP/health) depends_on: conditionで意味のある開始順序 —service_healthy、service_completed_successfullyrestart: unless-stoppedが運用用サービスの安全なデフォルトprofilesで一つのファイルの中で dev / test / debug 環境を分岐- override ファイル (
compose.override.yaml、compose.prod.yaml) で環境ごとの差分だけを乗せる docker compose configでマージされた最終定義を検証
次の記事 (#5 環境変数と secrets 管理) では秘密値 — DB パスワード、API キーのようなもの — をイメージに刻まずコンテナに注入する方法、そしてその値を .env と compose の secrets: / BuildKit シークレットで扱うパターンを整理します。