2-1. Docker Compose 基礎 - 複数コンテナのオーケストレーション
所要時間: 40-50分 ゴール: docker compose がなぜ存在するか説明でき、最小構成の compose.yml を読み書きできる コミット内容:
~/learn/infra/day08/に nginx + 静的サイトの compose.yml と起動ログ
このレッスンのゴール
- Compose がなぜ必要かを「
docker run数珠つなぎ地獄」で説明できる - 最小構成の compose.yml を読み書きできる
-
up/down/start/stop/restartの違いを使い分けられる - サービス名による DNS 解決の仕組みを腹落ちしている
- v1 (
docker-compose) と v2 (docker compose) の違いを把握している
なぜ学ぶか(実務悩みベース)
- 「ローカル開発環境を git clone 直後に立ち上げたい」を実現する
- CI のテスト DB 起動を1コマンドで済ませたい
- チームメンバー全員に同じ開発環境を配りたい
- 本番が k8s でも、開発が Compose で済むパターンは多い
前章とのつながり
1-7 までで docker run の主要オプションを叩けるようになった。実務に出ると 複数コンテナ が前提になる。今回はその数珠つなぎを YAML で宣言的に書く世界に進む。Level 1 で身につけた docker run の全オプションが、ほぼそのまま YAML のキーに対応する。
大前提: なぜ Compose が必要になるのか
レベル1までで「コンテナを1つ起動する」はできるようになった。しかし実務のシステムは コンテナ1つで完結することがほぼ無い。最小構成でも:
- Web アプリ(Go / Node / Rails)
- データベース(MySQL / PostgreSQL)
- キャッシュ(Redis)
- リバースプロキシ(nginx)
の 3〜4個のコンテナが連動して動く。これを docker run だけで管理しようとすると地獄になる。
# こういう作業を毎回やる羽目になる
docker network create myapp-net
docker run -d --name db --network myapp-net -e MYSQL_ROOT_PASSWORD=secret -v db_data:/var/lib/mysql mysql:8
docker run -d --name redis --network myapp-net redis:7
docker run -d --name api --network myapp-net -e DATABASE_URL=mysql://... -p 8080:8080 myapp:latest
docker run -d --name web --network myapp-net -p 80:80 -v ./nginx.conf:/etc/nginx/nginx.conf:ro nginx:alpineこれを 起動順を考慮しながら、毎回手で打つ のは非現実的。チームで共有もできない。Compose はこれを「YAML 1ファイル」に固めて、docker compose up 1コマンドで起動できるようにする。
Compose が解く本質的な問題
Compose は単なる「コマンド短縮ツール」ではない。次の3つの問題を同時に解いている。
- 複数コンテナの宣言的管理: 「こういう状態にしたい」を YAML で書き、Compose が現状との差分を埋める(
kubectl applyの思想の先祖)- コンテナ間ネットワークの自動構築: サービス名で DNS 解決できるネットワークを暗黙に作る
- 起動順とライフサイクル: 依存関係を宣言すれば、依存先から順に起動する
ユースケース
- 開発環境: 「git clone してきて
docker compose upで全部立ち上がる」開発体験- CI 環境: テスト用 DB を起動 → テスト実行 → 後片付け、を1ファイルで
- 小規模本番: VPS 1台で動く程度のシステムなら本番 Compose 運用もあり(後述)
セッション①: Compose とは何か、最小構成を動かす(25-30分)
1. v1 docker-compose と v2 docker compose
# 古い書き方(v1、Python 製。EOL)
docker-compose up
# 新しい書き方(v2、Go 製。docker CLI のサブコマンド)
docker compose upv1 と v2 の違いと「どっちを使うか」
Compose には大きく2世代ある。新規プロジェクトでは v2 (
docker compose) 一択。
項目 v1 docker-composev2 docker compose言語 Python Go 配布 別バイナリ Docker CLI のプラグイン 状態 EOL(2023年7月でサポート終了) 現役 コマンド ハイフン区切り スペース区切り プロファイル 限定的 フル対応 歴史的経緯: Compose はもともと Fig という別プロジェクト(Orchard Labs 製)が Docker 社に買収されてできた。Python で書かれていたため動作が遅く、Docker Desktop に同梱されている v2 で Go に書き直された。
ユースケース
- 新規プロジェクト: v2 一択
- 既存プロジェクトの保守: v1 で書かれていたら v2 に移行(多くの場合互換)
- CI: v2 を使う(GitHub Actions の ubuntu-latest には既に入っている)
v1 のコマンドが体に染み付いていると損
docker-compose upと打つクセが付いていると、v2 オンリーな環境(Docker Desktop の新しめのバージョン)で「コマンドが見つかりません」エラーが出る。今日から 必ずdocker compose(スペース区切り) で打つ習慣を。
2. compose.yml の最小構成
# compose.yml (ファイル名は compose.yaml / docker-compose.yml も可)
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"# 起動
docker compose up
# バックグラウンド起動
docker compose up -d
# 確認
docker compose ps
# 停止&削除
docker compose down
servicesがトップレベルのキーCompose の YAML はトップレベルが固定の予約語。主要なものは:
キー 役割 servicesコンテナ群の定義。各サービスがコンテナ1つに相当 networksカスタムネットワーク定義(省略可、デフォルトのネットワークが作られる) volumes名前付きボリュームの宣言 secretsSecret の宣言(Swarm/Compose v2 で利用可) configs設定ファイルの宣言(Swarm 主体) 設計思想: 「コンテナ」ではなく「サービス」と呼ぶのがミソ。同じ image から複数レプリカを動かす想定(
deploy.replicas)があり、サービスは「論理的な役割」を指す。
サービス内で書ける主要なキー
キー 役割 image使用するイメージ buildDockerfile からビルドする場合の指定 portsホストにポート公開( "ホスト:コンテナ")environment環境変数 env_file環境変数を別ファイルから volumesボリュームマウント networks所属させるネットワーク depends_on起動順の依存 healthcheckヘルスチェック定義 restart再起動ポリシー commandコンテナ起動時のコマンド上書き
静的サイトを nginx で配信する最小構成
services: web: image: nginx:1.27-alpine ports: - "8080:80" volumes: - ./public:/usr/share/nginx/html:ro restart: unless-stopped
./public/に置いた HTML/CSS がhttp://localhost:8080/で見える。:roで read-only マウントするのがセキュリティのお作法。
アンチパターン: latest タグでイメージ固定
services: web: image: nginx:latest # NG # または image: nginx (タグ省略は latest と同じ)なぜNGか:
nginx:latestは 常に最新版を指す可動の参照。今日の latest と来週の latest は違うイメージ- 「昨日まで動いてたのに今日ビルドし直したら壊れた」が起きる典型パターン
- 本番で
docker compose pull && up -dした瞬間にメジャーバージョンアップが入って事故った事例は枚挙にいとまがない正解:
nginx:1.27-alpineのように メジャー.マイナー or 完全な digest で固定する。nginx:1.27.0-alpine@sha256:abcdef...まで指定するとイメージ改ざんも防げる(supply chain attack 対策)。
3. up / down / ps / logs / exec / build / pull
# 起動(フォアグラウンド、ログがそのまま流れる)
docker compose up
# バックグラウンド起動
docker compose up -d
# 起動中サービス一覧
docker compose ps
# 全ログをリアルタイムで追跡
docker compose logs -f
# 特定サービスだけ
docker compose logs -f web
# コンテナ内でコマンド実行(シェル入りたい時の鉄板)
docker compose exec web sh
# 1回限りの使い捨てコマンド実行(コンテナを起動して終わったら消す)
docker compose run --rm web nginx -t
# ビルドだけ
docker compose build
# ビルドキャッシュを使わない
docker compose build --no-cache
# レジストリから最新イメージを取得
docker compose pull
# 停止&コンテナ削除(ボリュームは残る)
docker compose down
# 停止&コンテナ&ネットワーク&named volume も削除
docker compose down -v
# イメージも削除
docker compose down --rmi local
up/start/restartの違い似たコマンドが多いので整理。
コマンド 動作 up必要なら イメージpull → ネットワーク作成 → ボリューム作成 → コンテナ作成 → 起動 まで全部。差分があれば再作成 start既に作られているコンテナを 起動するだけ。差分検出なし restart停止 → 起動。設定変更は反映 されない(コンテナ再作成じゃないので) down停止+削除。 stopよりも片付けが徹底的stop停止のみ(コンテナは残る) 実務での使い分け
- 開発中に compose.yml を編集 →
docker compose up -d(差分が再作成される)- PC再起動後に環境を起こす →
docker compose start- 設定ファイル(マウント先)だけ変えてアプリ再読込 →
docker compose restart web
execとrunの違いどちらもコンテナ内でコマンドを叩くが用途が違う。
exec: 既に起動中の コンテナにアタッチして実行。デバッグ・状態確認の鉄板run: 新しいコンテナを起こして 実行。マイグレーション、1回きりの管理コマンド向け。--rmで実行後に自動削除使い分け例
# 動いてる Web コンテナに入ってログ調査 docker compose exec web sh # マイグレーションを別コンテナで1回だけ実行 docker compose run --rm api ./migrate up
down -vは named volume も消す
-vフラグを付けると named volume も削除される。MySQL のデータボリュームを named volume にしていた場合、down -vで データが全消滅 する。開発中のリセット用には便利だが、本番で間違えて打つと最悪。
4. profiles で環境切替
services:
app:
image: myapp:1.0
ports:
- "8080:8080"
# 開発時だけ起動したいサービス
debug-proxy:
image: mitmproxy/mitmproxy
profiles: ["debug"]
# E2E テストの時だけ立てる
selenium:
image: selenium/standalone-chrome
profiles: ["test"]# 通常起動(debug-proxy と selenium は起動しない)
docker compose up -d
# debug プロファイル含めて起動
docker compose --profile debug up -d
# 複数プロファイル
docker compose --profile debug --profile test up -dprofiles の使い所
Compose では「全サービスをいつも起動する」がデフォルト。だが、デバッグツールやテスト専用サービスは普段は起動したくない。
profilesを付けたサービスは そのプロファイルを明示しないと起動されない。ユースケース
- 開発時のみ立てる管理画面(pgAdmin、redis-commander、mitmproxy)
- E2E テストでのみ必要なヘッドレスブラウザ
- 重い ML サービスを通常開発では起動しない
プロファイルが付いていないサービスは 常に起動対象 になる点に注意。明示しない限りオフ、ではない。
セッション②: ネットワーク・ボリューム・実例(25-30分)
5. デフォルトネットワーク
services:
api:
image: myapi:1.0
db:
image: mysql:8このとき Compose は 暗黙的にネットワークを1つ作る。プロジェクト名(デフォルトはディレクトリ名)が prefix される。
docker compose up -d
docker network ls
# myapp_default bridge localサービス名 = ホスト名
同じネットワークにいるサービスは、サービス名で名前解決できる。
# api コンテナの中から ping db # OK("db" がサービス名) mysql -h db -u root -p # MySQL コンテナへ接続これは Docker の内蔵 DNS(127.0.0.11)が解決している。
localhostではない点に注意。コンテナ内のlocalhostは自分自身。アンチパターン: 接続文字列に
localhostを書く → 別コンテナの DB に届かない。サービス名を使う。
6. ボリュームの宣言
services:
db:
image: mysql:8
environment:
MYSQL_ROOT_PASSWORD: secret
volumes:
- db_data:/var/lib/mysql # named volume
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro # bind mount
volumes:
db_data: # ここで named volume を宣言named volume と bind mount の使い分け
種類 書き方 実体 用途 named volume db_data:/var/lib/mysqlDocker が管理する領域 永続データ(DB、ファイルストア) bind mount ./code:/appホストのファイルシステム ソースコード、設定ファイル anonymous volume /var/lib/mysqlDocker 管理だが名前無し 基本使うな(後述) named volume の利点
- ホスト OS の挙動に依存しない(macOS の bind mount は遅い、Linux なら速い、など)
- バックアップ・移行は
docker volumeコマンドで完結- ボリュームの命名で意図が明確(
db_data、uploadsなど)
アンチパターン: 無名ボリュームでデータ消失
services: db: image: postgres:16 # volumes 指定なしDockerfile(postgres 公式イメージ)が
VOLUME /var/lib/postgresql/dataを宣言しているため、コンテナ起動時に 無名のボリューム が勝手に作られる。このとき:
docker compose downだけならボリュームは残るdocker compose down -vで データが消える- 無名なので別の compose 環境からは参照できず、コンテナ削除=復元困難
正解: 必ず named volume を宣言する。
services: db: image: postgres:16 volumes: - pg_data:/var/lib/postgresql/data volumes: pg_data:
7. ネットワークを複数定義
services:
web:
image: nginx:alpine
networks: [frontend]
api:
image: myapi:1.0
networks: [frontend, backend] # 両方に所属
db:
image: mysql:8
networks: [backend] # web からは直接見えない
networks:
frontend:
backend:ネットワーク分離の意義(セキュリティ)
本番で重要なのは 「全サービスが全サービスに見える」状態を作らないこと。攻撃者が web コンテナを乗っ取った時、DB に直接届かない構成にする。
上の例では:
webからdbは 名前解決できない(別ネットワークなので)apiだけが両方に居て橋渡しこれは Kubernetes の NetworkPolicy、AWS の Security Group と同じ思想。defense in depth(多層防御) の最初の一歩。
8. 実例: nginx + 静的サイト
mkdir -p ~/learn/infra/day08/{public,nginx}
cd ~/learn/infra/day08public/index.html:
<!DOCTYPE html>
<html><head><title>compose hello</title></head>
<body><h1>Hello from nginx + compose</h1></body></html>nginx/default.conf:
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# セキュリティヘッダの基本
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
# ヘルスチェック用
location = /healthz {
access_log off;
return 200 "ok\n";
}
}compose.yml:
services:
web:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./public:/usr/share/nginx/html:ro
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/healthz"]
interval: 10s
timeout: 3s
retries: 3# 起動
docker compose up -d
# 確認
curl http://localhost:8080/
curl http://localhost:8080/healthz
# ログ
docker compose logs -f web
# 後片付け
docker compose down
:ro(read-only) を付ける意味静的ファイルや設定ファイルを bind mount する時、コンテナ側から書き換えられないようにする。
- セキュリティ: コンテナが侵害されてもホストの HTML を書き換えられない
- 安全性: 想定外の書き込みでホストファイルが壊れる事故を防ぐ
- 意図の明示: 「読み取り専用です」とコードで宣言
設定ファイルは原則
:roで。
アンチパターン: 本番で
upだけ叩く運用# 本番サーバーで毎回これ docker compose upなぜNGか:
upはフォアグラウンドで動く。SSH を切ると コンテナも止まる- 標準出力のログがコンソールに流れて、保存されない
- 異常終了時に自動再起動しない
正解:
docker compose up -dで detach。さらに本番ではrestart: unless-stoppedかrestart: alwaysを必ず宣言。systemd unit から起動する運用も一般的(OS再起動時に自動起動)。
練習課題
mkdir -p ~/learn/infra/day08/practice
cd ~/learn/infra/day08/practice
script ~/log/infra_day08.log- 上記の nginx + 静的サイト構成を
docker compose up -dで起動し、curlで疎通確認 public/index.htmlを書き換えて再アクセス → 即座に反映されるか確認(bind mount の効果)compose.ymlのimage:をnginx:1.26-alpineに変えてdocker compose up -d→ コンテナが再作成されることをdocker compose psで確認(CONTAINER ID が変わる)profiles: ["debug"]を付けたtraefik/whoamiイメージのサービスを追加し、--profile debugを付けたときだけ起動することを確認docker compose downとdocker compose down -vの挙動の違いを、named volume を1つ作って体感するexitで script 終了
考察課題
docker compose upを2回連続で叩いたとき、2回目は何をしている?(コンテナは再作成される?されない?)- チームで
compose.ymlを共有する時、リポジトリに commit すべきか?.envは?
締め: git で証跡を残す
cd ~/learn/infra/day08
git init -q 2>/dev/null || true
git add compose.yml public/ nginx/
git commit -m "feat(infra): compose 基礎 - nginx 静的サイト最小構成"チェックリスト
- v1 と v2 の違い、v2 を使うべき理由を口頭で説明できる
-
servicesnetworksvolumesの役割を理解した -
up/down/ps/logs/exec/runの使い分けができる -
latestタグを使ってはいけない理由を説明できる - named volume と bind mount の違いと使い分けが分かる
- profiles を使って環境別にサービスを切り替えた
- nginx + 静的サイト構成を自力で組み立てた
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| 起動(フォアグラウンド) | docker compose up |
| 起動(バックグラウンド) | docker compose up -d |
| 起動中サービス一覧 | docker compose ps |
| ログ追跡 | docker compose logs -f [サービス] |
| シェル入る | docker compose exec <サービス> sh |
| 使い捨て実行 | docker compose run --rm <サービス> <cmd> |
| イメージビルド | docker compose build |
| キャッシュ無視 | docker compose build --no-cache |
| イメージ取得 | docker compose pull |
| 停止+削除 | docker compose down |
| ボリュームも削除 | docker compose down -v |
| profile 指定 | docker compose --profile <name> up -d |
| 設定の検証 | docker compose config |
やらかし事例: Compose の落とし穴
事例1: v1 と v2 の混在
古いチュートリアルを真似て
docker-compose up(ハイフン)を打ったら動かない。今はdocker compose up(スペース)が v2 正式。v1 は Python 製で EOL。
事例2: サービス名と DNS の関係を勘違い
webサービスからdbに繋ぐとき、ホストとしてlocalhostを指定してconnection refused。Compose のサービス名 = DNS 名。db:3306のように書く。
事例3:
docker compose down -vでデータ消失
down単独はコンテナとネットワークだけ削除(volume は残る)。-v付きで named volume も消える。本番運用で間違えると一発で DB 消滅。
事例4: バインドマウントで本番に開発ソース漏出
開発用 compose.yml をそのまま本番にコピー、
volumes: - .:/appでホスト直結。本番では override / profile で確実に分離。
事例5:
:latestのままチームに配って「私の環境では動く」compose.yml で
image: postgresのように省略すると:latest。日付が変わると別バージョンが落ちてくる。
対比表: docker run と compose の対応
| docker run | compose.yml キー |
|---|---|
-d | デフォルト(up -d) |
-p HOST:CONTAINER | ports: ["HOST:CONTAINER"] |
-v vol:/path | volumes: [vol:/path] |
-e KEY=VAL | environment: { KEY: VAL } |
--name X | サービス名 X: がそのまま |
--network N | networks: [N] |
--restart unless-stopped | restart: unless-stopped |
image:tag | image: image:tag または build: . |
「実務OK」基準
- compose.yml を読んでアーキテクチャ図が頭に浮かぶ: サービス・ネットワーク・ボリュームの関係を即座に理解
latestタグの危険性を周りに説明できる: タグ運用の合意形成ができる- named volume vs bind mount を使い分けられる: データ永続化の設計判断ができる
upとstartとrestartの違いで詰まらない: ライフサイクル操作を意識して使う
自己評価チェックリスト
知識レベル
- Compose の3つの本質(宣言的管理 / 暗黙DNS / 起動順)を語れる
- v1 と v2 の違いを区別している
-
up/down/start/stop/restartの使い分け基準を持っている
実行レベル
- 最小構成の compose.yml を1から書ける
- サービス名 DNS で別コンテナにアクセスできた
-
down -vの破壊力を意識して使えている
メタ認知
- Level 1 で
docker runで苦しんだ点が解消されたかを振り返った - 「Compose で解決できない問題」(依存関係 ready 待ち等)を1つ挙げた
さらに深掘るなら
- Compose Specification - 公式仕様。v3 と v2 を統一した「Compose Spec」が今の正式仕様
- Docker docs: Compose file reference - 全キーのリファレンス
- 書籍: 「Docker/Kubernetes 実践コンテナ開発入門」(山田明憲)- Compose の章は今読んでも価値あり
- OSS: traefik/whoami - Compose の動作確認に便利な軽量イメージ
- 思想: 「Twelve-Factor App」(12factor.net)- 後の env_secrets 章で深堀る
次のレッスン
2-2_複数コンテナ構成.md: Go API + MySQL の実用構成、depends_on の本質、ヘルスチェック付き起動、マイグレーション戦略へ進む。