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つの問題を同時に解いている。

  1. 複数コンテナの宣言的管理: 「こういう状態にしたい」を YAML で書き、Compose が現状との差分を埋める(kubectl apply の思想の先祖)
  2. コンテナ間ネットワークの自動構築: サービス名で DNS 解決できるネットワークを暗黙に作る
  3. 起動順とライフサイクル: 依存関係を宣言すれば、依存先から順に起動する

ユースケース

  • 開発環境: 「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 up

v1 と v2 の違いと「どっちを使うか」

Compose には大きく2世代ある。新規プロジェクトでは v2 (docker compose) 一択

項目v1 docker-composev2 docker compose
言語PythonGo
配布別バイナリ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

execrun の違い

どちらもコンテナ内でコマンドを叩くが用途が違う。

  • 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 -d

profiles の使い所

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 volumedb_data:/var/lib/mysqlDocker が管理する領域永続データ(DB、ファイルストア)
bind mount./code:/appホストのファイルシステムソースコード、設定ファイル
anonymous volume/var/lib/mysqlDocker 管理だが名前無し基本使うな(後述)

named volume の利点

  • ホスト OS の挙動に依存しない(macOS の bind mount は遅い、Linux なら速い、など)
  • バックアップ・移行は docker volume コマンドで完結
  • ボリュームの命名で意図が明確(db_datauploads など)

アンチパターン: 無名ボリュームでデータ消失

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/day08

public/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-stoppedrestart: always を必ず宣言。systemd unit から起動する運用も一般的(OS再起動時に自動起動)。


練習課題

mkdir -p ~/learn/infra/day08/practice
cd ~/learn/infra/day08/practice
script ~/log/infra_day08.log
  1. 上記の nginx + 静的サイト構成を docker compose up -d で起動し、curl で疎通確認
  2. public/index.html を書き換えて再アクセス → 即座に反映されるか確認(bind mount の効果)
  3. compose.ymlimage:nginx:1.26-alpine に変えて docker compose up -d → コンテナが再作成されることを docker compose ps で確認(CONTAINER ID が変わる)
  4. profiles: ["debug"] を付けた traefik/whoami イメージのサービスを追加し、--profile debug を付けたときだけ起動することを確認
  5. docker compose downdocker compose down -v の挙動の違いを、named volume を1つ作って体感する
  6. 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 を使うべき理由を口頭で説明できる
  • services networks volumes の役割を理解した
  • 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 runcompose.yml キー
-dデフォルト(up -d
-p HOST:CONTAINERports: ["HOST:CONTAINER"]
-v vol:/pathvolumes: [vol:/path]
-e KEY=VALenvironment: { KEY: VAL }
--name Xサービス名 X: がそのまま
--network Nnetworks: [N]
--restart unless-stoppedrestart: unless-stopped
image:tagimage: image:tag または build: .

「実務OK」基準

  • compose.yml を読んでアーキテクチャ図が頭に浮かぶ: サービス・ネットワーク・ボリュームの関係を即座に理解
  • latest タグの危険性を周りに説明できる: タグ運用の合意形成ができる
  • named volume vs bind mount を使い分けられる: データ永続化の設計判断ができる
  • upstartrestart の違いで詰まらない: ライフサイクル操作を意識して使う

自己評価チェックリスト

知識レベル

  • 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 の本質、ヘルスチェック付き起動、マイグレーション戦略へ進む。