Docker Compose:実践で学ぶマルチコンテナアプリケーション管理

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

5分でできる:すぐに動くフルスタックの構築

理論の説明より、まず一番実践的なことから始めよう——すぐ動くアプリ+データベースのスタックを作る。docker-compose.ymlファイルを作成する:

version: '3.8'

services:
  db:
    image: mysql:8.0
    restart: unless-stopped
    environment:
      MYSQL_ROOT_PASSWORD: rootpass
      MYSQL_DATABASE: myapp
      MYSQL_USER: app_user
      MYSQL_PASSWORD: app_pass
    volumes:
      - db_data:/var/lib/mysql

  app:
    image: wordpress:latest
    restart: unless-stopped
    ports:
      - "8080:80"
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: myapp
      WORDPRESS_DB_USER: app_user
      WORDPRESS_DB_PASSWORD: app_pass
    depends_on:
      - db

volumes:
  db_data:

スタックを起動:

docker compose up -d

状態を確認:

docker compose ps
docker compose logs -f app

http://localhost:8080を開くと、MySQLと連携したWordPressが動いている。一連の作業は30秒もかからない。手動でやる場合と比べてみよう:MySQLに5〜6個のフラグを付けたdocker runコマンド、WordPressに8〜10個のフラグを付けた別のコマンド、そして2つのコンテナが互いを認識できるようにネットワークを手動で作成……Composeはこれらすべてを1つのファイル、1つのコマンドにまとめてくれる。

詳細解説:罠にはまらないために理解しておくこと

depends_onは「サービスが準備完了するまで待つ」ではない

初めて本番環境をセットアップしたとき、自分もこの罠にはまった。depends_onが保証するのは、コンテナが順番に起動することだけだ——内部のサービスが接続を受け付ける準備ができているかどうかは保証しない。MySQLが初期化を完了して接続を受け付けるまで通常15〜30秒かかるが、appコンテナはその前にすでに起動を終えてしまっている。

healthcheckで正しく修正する:

services:
  db:
    image: mysql:8.0
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s

  app:
    depends_on:
      db:
        condition: service_healthy

Networks:コンテナはどのようにお互いを認識するのか

Docker Composeはプロジェクトごとに専用のbridgeネットワークを自動作成する。コンテナ同士はIPアドレスではなくサービス名で通信する。WORDPRESS_DB_HOST: dbにIPアドレスをハードコードせずサービス名を使う理由はこれだ:IPアドレスは再起動のたびに変わる可能性があるが、サービス名は変わらない。

スタックが大きくなると、トラフィックを分離したくなる。例えば、nginxはフロントエンドとバックエンドの両方と通信する必要があるが、データベースはインターネットに出てはいけない:

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # インターネットアクセスなし

services:
  nginx:
    networks:
      - frontend
      - backend

  api:
    networks:
      - backend

  db:
    networks:
      - backend

internal: trueはデータベースを隔離するための便利なテクニックだ——dbコンテナは自らインターネットへの外部通信ができなくなり、攻撃対象領域を大幅に削減できる。

Volumes:Named VolumesとBind Mountの違い

services:
  app:
    volumes:
      # Named volume — Dockerが管理、コンテナを削除しても保持される
      - app_data:/var/www/html/uploads

      # Bind mount — ホストからディレクトリをマウント(開発時に便利)
      - ./config/nginx.conf:/etc/nginx/nginx.conf:ro

      # tmpfs — RAMのみ、コンテナ停止で消去(キャッシュに使用)
      - type: tmpfs
        target: /tmp/cache

volumes:
  app_data:

シンプルなルール:開発時の設定ファイルにはbind mountを使う——ホスト上でnginx.confを編集すれば即座に反映され、イメージの再ビルドは不要。データベースやアップロードファイルなど永続化が必要なデータにはNamed volumeを使う——Dockerが管理し、コンテナを削除して再作成しても消えない。

応用編:本番環境で毎日使うパターン

.envファイルで環境を分離する

docker-compose.ymlに認証情報をハードコードしてはいけない。.envを使い、すぐに.gitignoreに追加しよう:

# .env
MYSQL_ROOT_PASSWORD=super_secret_pass
MYSQL_DATABASE=production_db
APP_PORT=8080
APP_ENV=production
# docker-compose.yml
services:
  db:
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
      MYSQL_DATABASE: ${MYSQL_DATABASE}

  app:
    ports:
      - "${APP_PORT}:80"
    environment:
      APP_ENV: ${APP_ENV}

開発環境と本番環境のoverride files

同じコードベースから開発と本番デプロイを行うチームにとって非常に便利なパターンだ。共通の設定はdocker-compose.ymlに保持し、環境ごとに個別のoverride fileを作成する:

# docker-compose.override.yml ('docker compose up'実行時に自動的に読み込まれる)
services:
  app:
    volumes:
      - .:/var/www/html  # 開発時にソースコードをマウント
    environment:
      APP_DEBUG: "true"
# docker-compose.prod.yml
services:
  app:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    restart: always
# 本番環境へのデプロイ
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Resource limits — クラスター全体を守る

30本以上のコンテナが動くクラスターでよく遭遇するのがこの状況だ:あるサービスがメモリリークを起こしてRAMを静かに食いつぶし、ノード全体を道連れにする。誰も気づかないのは、何も防ぐ仕組みがないからだ。resource limitsを適用してからは二度と起きなくなり——各サービスが自分の割り当て分だけ使うようになったことで、総resource使用量が約40%削減された。

services:
  api:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 256M
        reservations:
          cpus: '0.25'
          memory: 128M

Profiles:必要なときだけサービスを起動する

services:
  app:  # profileなし = 常に起動
  db:   # profileなし = 常に起動

  adminer:
    image: adminer
    profiles: ["tools"]  # --profile toolsで呼ばれたときだけ起動
    ports:
      - "8081:8080"
# app + db を通常起動
docker compose up -d

# データベースのデバッグが必要なときにadminerも起動
docker compose --profile tools up -d

実践的なTips:意外と知られていない便利なコマンド

日常業務で役立つコマンド

# 複数サービスのログを同時に表示、キーワードでフィルタリング
docker compose logs -f --tail=100 app db | grep ERROR

# 実行中のコンテナに接続
docker compose exec app bash

# 他のサービスをダウンさせずにスケール
docker compose up -d --scale worker=3

# 1つのサービスだけを再ビルドして再デプロイ
docker compose up -d --no-deps --build app

# リアルタイムのリソース使用量を確認
docker compose stats

# 最新のイメージをPullして再起動
docker compose pull && docker compose up -d

プロジェクト名の設定——3ヶ月後のコンフリクトを防ぐ

Docker Composeはコンテナ名を{project}_{service}_{replica}というパターンで命名する。デフォルトのプロジェクト名はディレクトリ名だ。問題は:異なる2つのプロジェクトが同じappというディレクトリ名を使っていると、ネットワークを共有してコンテナ名が衝突する——午前2時にこれが起きると非常にデバッグが難しいエラーになる。ファイルの先頭にnameを宣言すればそれで解決だ:

name: myapp-production

services:
  ...

定期的なクリーンアップでディスク容量を確保

# プロジェクトのコンテナとネットワークを削除(volumeは保持)
docker compose down

# volumeも一緒に削除——注意、データが消える!
docker compose down -v

# Docker全体をクリーンアップ:停止済みコンテナ、未使用イメージ、volume
docker system prune -a --volumes

サーバーでdocker system prune -fを毎週cronで実行している——手動で介入することなく、毎月数十GBのディスクスペースを節約できている。

Share: