Docker 中級 #3 docker compose 基礎 — web + db を一つのファイルで

読了 8分

ここまでのコマンドは全てコンテナ一つを扱いました。でも実際のアプリは web + db、または web + db + cache + worker のように複数コンテナの束です。これを一つのファイルに定義して一つのコマンドで回す道具が Docker Compose です。

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

docker-composedocker compose #

最初に見ると混乱しやすい点から。二つの表記があります。

  • docker-compose (ハイフン) — 旧 v1、Python で作られた別バイナリ。deprecated
  • docker compose (スペース) — 現在 v2、Go で作られて Docker に統合。これが標準

この記事は docker compose (v2) で統一します。Docker Desktop には標準で含まれ、Linux では docker-compose-plugin パッケージがあります。

インストール確認
docker compose version
# Docker Compose version v2.30.x

なぜ Compose が必要か #

docker run でやっていたことをそのまま移すとすぐ限界が見えます。

run で web + db を立ち上げる
docker network create myapp-net

docker volume create pgdata

docker run -d --name pg \
  --network myapp-net \
  -v pgdata:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=secret \
  postgres:16

docker run -d --name web \
  --network myapp-net \
  -p 8000:8000 \
  -e DB_HOST=pg \
  -e DB_PASSWORD=secret \
  myapp:latest

これくらいが「最小」セットアップです。環境がもっと増えるとシェルスクリプトに移しますが、それでもどこかに変更が出ると最初から立ち上げ直すことになり、同僚にセットアップを教えるのに README が長くなります。

同じことを Compose で書くと:

compose.yaml
services:
  pg:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: secret
    volumes:
      - pgdata:/var/lib/postgresql/data

  web:
    image: myapp:latest
    ports:
      - "8000:8000"
    environment:
      DB_HOST: pg
      DB_PASSWORD: secret
    depends_on:
      - pg

volumes:
  pgdata:

docker compose up 一つのコマンドで全体起動。変更があれば同じコマンドで変更された部分だけ作り直し。同僚には「リポジトリをクローンして docker compose up」一行で終わります。

ファイル名と位置 #

標準ファイル名は compose.yaml (または compose.yml) です。昔よく見た docker-compose.yml も今でも動きますが、新規プロジェクトなら compose.yaml 推奨。

デフォルト認識順
compose.yaml          # 1 番目
compose.yml           # 2
docker-compose.yaml   # 3
docker-compose.yml    # 4 (古い慣習)

コマンドはファイルがあるディレクトリで単に docker compose up — パス指定がなければ上の順序で探します。ファイルが別の場所にあるなら -f で:

ファイル明示
docker compose -f infra/compose.yaml up
docker compose -f compose.yaml -f compose.dev.yaml up   # 複数 (override)

compose.yaml の大枠 #

トップレベルキー一覧。

トップレベルキー意味
servicesコンテナ群 (一番よく書くところ)
volumes名前付きボリューム
networks名前付きネットワーク
secretssecret (#5 で)
configsコンテナに注入する設定ファイル

古いファイルでよく見る version: "3.8" のような行は もう使いません。 Compose v2 が無視します。新しいファイルでは外す方が綺麗。

実践例 — Django + Postgres + Redis #

もっと現実的な例を組みます。Django 実戦シリーズ のようなセットアップ。

compose.yaml
services:
  web:
    build: .
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - ./:/app
    ports:
      - "8000:8000"
    environment:
      DATABASE_URL: postgres://app:secret@pg:5432/app
      REDIS_URL: redis://redis:6379/0
      DEBUG: "1"
    depends_on:
      - pg
      - redis

  pg:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: app
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "127.0.0.1:5432:5432"   # ホストの DB クライアントからアクセスする場合

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

各項目を解きほぐすと:

  • build: . — カレントディレクトリの Dockerfile でビルド。イメージを直接作るとき
  • image: postgres:16 — イメージを取って使うとき
  • command: — イメージのデフォルト CMD をオーバーライド
  • volumes: — bind mount (./:/app) と named volume (pgdata:/var/...) が一箇所に混じっています。: 前が絶対パス / 相対パスなら bind、ただの名前なら named。
  • ports:docker run -p と同じ形。"127.0.0.1:5432:5432" のようにホスト IP も可能。
  • environment:-e KEY=val に相当。マッピングまたはリストで書ける
  • depends_on: — 開始順序 (#4 で深く)

Compose が自動でやってくれること #

docker run で一つずつやっていたことを Compose が自動でやります。

自動ネットワーク #

compose up の最初の実行時に プロジェクト名のデフォルトネットワーク を作って全サービスをそのネットワークにぶら下げます。

ネットワーク自動生成
docker network ls
# NAME                  DRIVER    SCOPE
# myapp_default         bridge    local

同じネットワークの中のサービスは サービス名でお互いを呼べます。 上の例で web コンテナの環境変数 DATABASE_URL: postgres://app:secret@pg:5432/app — ホストが pg なんですが、これがそのまま動きます。(基礎 #4 のユーザー定義 bridge ネットワーク + DNS と同じ原理。)

プロジェクト名前空間 #

Compose は全リソースに プロジェクト名 を prefix として付けます。

コンテナ名
docker ps
# NAMES
# myapp-web-1
# myapp-pg-1
# myapp-redis-1

デフォルトプロジェクト名はディレクトリ名。変更するなら -p:

プロジェクト名指定
docker compose -p myproj up
# myproj-web-1, myproj-pg-1 ...

同じマシンで同じ compose ファイルを別の名前で二つ立ち上げられます。テスト用 / 開発用環境を同時に回すときに便利です。

日常コマンド群 #

up — 立ち上げる
docker compose up                # フォアグラウンド + 全ログを一箇所で
docker compose up -d             # バックグラウンド (detached)
docker compose up --build        # ビルドからやり直し (コード変更後)
docker compose up web            # 特定サービスだけ
docker compose up --remove-orphans   # compose.yaml から外れた古いサービスも片付ける
down — 落とす
docker compose down              # コンテナ + ネットワーク削除
docker compose down -v           # 上 + ボリュームまで (データが消える)
docker compose down --rmi local  # 上 + ビルドされたイメージまで
ps — 状態
docker compose ps                # このプロジェクトのコンテナだけ
docker compose ps -a             # 終了したものまで
logs — ログ
docker compose logs              # 全サービスのログを一箇所で
docker compose logs -f           # follow
docker compose logs -f web       # 特定サービスだけ
docker compose logs --since 10m  # 直近 10 分
exec / run — 中に入る
docker compose exec web bash     # 立ち上がっている web の中で bash
docker compose run --rm web python manage.py migrate   # 新しいコンテナで一回限りのコマンド

execrun の違いは 基礎 #3docker exec vs docker run の違いと同じです。マイグレーション / シードのような一回限りの作業は run --rm、立ち上がっているコンテナのデバッグは exec

restart / stop / start
docker compose restart web       # 一サービス再起動
docker compose stop              # 止めるがコンテナは保存 (down と違う)
docker compose start             # 止まっているのを再び起動

up vs start vs restart #

三つのコマンドが似て見えますが意味が違います。

  • up — コンテナを作る (なければ)、変更を反映して作り直す (あっても定義が変わっていれば)
  • start — 止まった (既存) コンテナを起動。定義変更は反映しない
  • restartstopstart。定義変更反映なし

compose.yaml を編集した後は常に up です。

build: 深掘り #

サービスでイメージを直接ビルドするとき:

build オプション
services:
  web:
    build:
      context: .              # ビルドコンテキスト (Dockerfile がある場所)
      dockerfile: Dockerfile.dev   # デフォルト以外の別ファイルを使うとき
      args:
        APP_VERSION: "1.0.0"
      target: dev             # マルチステージの特定ステージ
      cache_from:
        - myapp:cache
    image: myapp:dev          # ビルド結果にこの名前を付ける

image:build: を一緒に書くと、ビルドした成果物にその名前を付けます。すると docker compose push でそのイメージをレジストリにアップロードもできます。

volumes: の整理 #

サービスの volumes: は三つの形式がよく見えます。

volumes 形式
services:
  web:
    volumes:
      - ./:/app                          # bind (相対パス)
      - /Users/me/data:/app/data         # bind (絶対パス)
      - pgdata:/var/lib/postgresql/data  # named (トップレベル volumes で定義)
      - logs:/app/logs:ro                # named, read-only
      - type: bind
        source: ./config
        target: /etc/myapp
        read_only: true                  # 長い形式

volumes:
  pgdata:
  logs:

短い形式が日常では十分で、読み取り専用 / 権限 / オプションがもっと必要なとき 長い形式に展開して書きます。

networks: — ユーザー定義 #

デフォルトネットワーク以外にユーザー定義ネットワークも作れます。一つのプロジェクトの中で一部サービスだけ同じネットワークに置きたいとき。

複数ネットワーク
services:
  web:
    networks:
      - frontend
      - backend
  pg:
    networks:
      - backend
  nginx:
    networks:
      - frontend

networks:
  frontend:
  backend:

pgbackend だけにあるので nginx が直接呼べません。セキュリティ隔離に有用なパターンです。ただし一般のアプリではデフォルトネットワーク一つで十分です。

よく出会う落とし穴 #

  • ログが見えない — アプリが 127.0.0.1 にバインドした、またはファイルにログを書いています。0.0.0.0 バインド + stdout 出力に。
  • DB は全部立ち上がったのに web で connection refuseddepends_on はコンテナの開始順序だけを見る。DB が 準備 できたかは見ません。(#4 の healthcheck で解く。)
  • bind mount したコードをコンテナが見られない — Docker Desktop のファイル共有設定で該当ディレクトリが外れている可能性。Settings → Resources → File Sharing を確認。
  • compose down で DB データが消えた-v を一緒に使うと named volume まで消える。普段は -v なしで。
  • build: したのに変更が反映されないup --build または up -d --build で。

一サイクル #

新しいプロジェクトの日常:

開発フロー
# 最初
docker compose up -d --build

# コード編集 (bind mount なら自動反映、dev サーバなら自動再起動)
docker compose logs -f web

# マイグレーション
docker compose run --rm web python manage.py migrate

# DB シェル進入
docker compose exec pg psql -U app

# 一日の終わり
docker compose down

# 最初からやり直し (DB データも飛ばして)
docker compose down -v
docker compose up -d --build

これくらいが手に馴染めば、一つのプロジェクトのインフラセットアップが一つのファイル + 数行のコマンドにまとまります。

まとめ #

この記事で掴んだ絵:

  • docker compose (v2) が標準。docker-compose (v1) は deprecated
  • compose.yaml 一つのファイルに services / volumes / networks を定義 — 一つのコマンドで立ち上げ
  • サービス同士は サービス名が DNS のように動作 (自動ネットワーク + ユーザー定義 bridge)
  • build: + image: でビルドとタグを一箇所で
  • 日常コマンド: up -ddownlogs -fexecrun --rm
  • up は変更反映、start は単純起動 — compose.yaml を変えたら常に up

次の記事 (#4 compose 深掘り — depends_on, healthcheck, profiles) ではこの骨格に運用道具を重ねます。healthcheck で「DB が本当に準備できたか」を見て、depends_on の condition で意味のある開始順序を立て、profiles で dev / test / prod を一つのファイルの中で分岐する方法です。

X