Contents
導入と目的:なぜComposeでテスト環境を作るのか
Docker Composeでテスト環境を定義すると、ローカルとCIで同一の構成を使えます。
これにより、環境差分によるテスト誤差を減らし、トラブルシュートが容易になります。
この記事のゴールと対象読者
この記事は中級〜上級の開発者、QAエンジニア、CI担当者向けに、即コピー&実行できるテンプレートと運用上の注意点を提供します。
基本テンプレート(実務向け docker-compose.yml の実例)
ここでは即コピーして試せるテンプレートを示します。コードの直前に目的と想定シナリオを記載しますので、そのままCIに組み込める構成です。
.env.example と最小 .env サンプル
このブロックの目的: リポジトリに置く .env.example の具体例を示します。想定される利用シナリオ(ローカル/CI): リポジトリをクローンしたら .env を作成してすぐ動かせるようにするため。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# .env.example # 簡単な説明と推奨値を付けています。リポジトリに実際の秘密は置かないでください。 # Postgres (CI/ローカル共通) POSTGRES_USER=test POSTGRES_PASSWORD=test POSTGRES_DB=test_db POSTGRES_PORT=5432 # DB 接続(DATABASE_URL がある場合はこれを優先できます) # フォーマット: postgresql://user:pass@host:port/dbname DATABASE_URL=postgresql://test:test@postgres:5432/test_db # Redis REDIS_HOST=redis REDIS_PORT=6379 REDIS_PASSWORD= # SMTP (MailHog など) SMTP_HOST=mail SMTP_PORT=1025 # CI 用プロジェクト名の接頭辞(ジョブで上書き推奨) COMPOSE_PROJECT_PREFIX=ci # オプション: テストランナーでのタイムアウト等 MAX_RETRIES=60 |
このブロックの目的: 最小限の .env サンプル(試用向け)を示します。想定される利用シナリオ(ローカル): クローン後すぐローカルで動かすための簡易値。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# .env (ローカル向け最小サンプル) POSTGRES_USER=test POSTGRES_PASSWORD=test POSTGRES_DB=test_db DATABASE_URL=postgresql://test:test@postgres:5432/test_db REDIS_HOST=redis REDIS_PORT=6379 SMTP_HOST=mail SMTP_PORT=1025 COMPOSE_PROJECT_PREFIX=local MAX_RETRIES=60 |
docker-compose.yml(ベース)
このブロックの目的: ベースの docker-compose.yml(サービス定義)を示します。想定される利用シナリオ(ローカル/CI): ローカルは override を使いソースマウントやポート公開、CIは --profile ci でテスト専用サービスを起動します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 |
# docker-compose.yml (ベース) version: "3.9" services: app: build: context: . dockerfile: Dockerfile target: app env_file: - .env environment: DATABASE_URL: ${DATABASE_URL} REDIS_URL: redis://${REDIS_HOST:-redis}:${REDIS_PORT:-6379}/0 SMTP_HOST: ${SMTP_HOST:-mail} SMTP_PORT: ${SMTP_PORT:-1025} depends_on: - postgres - redis - mail healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 10s networks: - backend postgres: image: postgres:15 env_file: - .env environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - pgdata:/var/lib/postgresql/data - ./docker/postgres/init:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -h ${POSTGRES_HOST:-postgres} -p ${POSTGRES_PORT:-5432} -U ${POSTGRES_USER} -d ${POSTGRES_DB} || exit 1"] interval: 5s timeout: 5s retries: 10 start_period: 10s networks: - backend redis: image: redis:7-alpine healthcheck: test: ["CMD-SHELL", "redis-cli -h ${REDIS_HOST:-redis} -p ${REDIS_PORT:-6379} ${REDIS_PASSWORD:+-a ${REDIS_PASSWORD}} ping || exit 1"] interval: 5s timeout: 2s retries: 5 networks: - backend mail: image: mailhog/mailhog healthcheck: test: ["CMD-SHELL", "curl -fsS http://localhost:8025/ >/dev/null || exit 1"] interval: 5s timeout: 3s retries: 5 networks: - backend test: build: context: . dockerfile: Dockerfile target: test profiles: - ci env_file: - .env # CI ではソースをイメージに含める/readonlyマウントを避けることを推奨します。 # ローカル開発時にのみソースマウントでホットリロードする場合は override で設定してください。 # volumes: # - ./:/app:ro depends_on: - postgres - redis - mail command: ["/bin/sh", "-c", "/usr/local/bin/wait-for-services.sh && alembic upgrade head && pytest -q"] networks: - backend networks: backend: volumes: pgdata: |
補足(docker-compose.yml に関する重要ポイント):
- healthcheck のコマンドで環境変数展開を使う場合は CMD-SHELL を使ってください。JSON 形式(["CMD", ...])だとシェル拡張が行われません。
- depends_on は単に起動順のヒントであり、readiness は保証しません。readiness は healthcheck と下記の待ち合わせスクリプトで扱います。
- test サービスのソースを read-only マウントする場合、テストやマイグレーションで書き込みが必要だと失敗する可能性があります。CIでは可能ならソースをイメージに含める(マウントしない)か、rw マウントを使ってください。
Dockerfile(multi-stage、ビルド依存分離)
このブロックの目的: ビルド依存を分離して最終イメージを小さく保つ Dockerfile の例を示します。想定される利用シナリオ(ローカル/CI): ローカルは app ターゲットを使い起動、CI は test ターゲットでテスト実行。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
# Dockerfile (最適化された multi-stage の例) # ランタイム用の最小ベース FROM python:3.11-slim AS base ENV PYTHONDONTWRITEBYTECODE=1 ENV PYTHONUNBUFFERED=1 WORKDIR /app # ランタイムに必要なパッケージだけを入れる RUN apt-get update \ && apt-get install -y --no-install-recommends ca-certificates curl libpq5 \ && rm -rf /var/lib/apt/lists/* # ビルド専用ステージ: build deps をここに集約 FROM python:3.11-slim AS builder RUN apt-get update \ && apt-get install -y --no-install-recommends build-essential libpq-dev gcc \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY requirements.txt . # wheel 化してキャッシュする例 RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt # 実行用イメージ FROM base AS app COPY --from=builder /wheels /wheels RUN pip install --no-cache-dir --no-index --find-links /wheels -r requirements.txt COPY . /app CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] # テスト用イメージ(CI用) FROM base AS test COPY --from=builder /wheels /wheels RUN pip install --no-cache-dir --no-index --find-links /wheels -r requirements-dev.txt COPY ci/wait-for-services.sh /usr/local/bin/wait-for-services.sh RUN chmod +x /usr/local/bin/wait-for-services.sh COPY . /app CMD ["/usr/local/bin/wait-for-services.sh", "pytest", "-q"] |
補足(Dockerfile 最適化):
- build-essential などのビルド依存は builder ステージに限定して最終イメージには含めません。
- pip の wheel キャッシュや --no-index を使うと CI の再現性とビルド時間が改善します。
- サイズが問題になる場合は alpine ベースや distroless を検討してください。ただしビルドやライブラリの互換性に注意が必要です。
待ち合わせスクリプト(ci/wait-for-services.sh)
このブロックの目的: DB/Redis 等の到達性を確認し、認証や DATABASE_URL の形式に対応する待ち合わせスクリプトの例です。想定される利用シナリオ(CI): test コンテナのエントリポイントで実行してマイグレーション→テストを行います。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
#!/bin/sh set -eu MAX_RETRIES=${MAX_RETRIES:-60} SLEEP_POSTGRES=${SLEEP_POSTGRES:-2} SLEEP_REDIS=${SLEEP_REDIS:-1} # DATABASE_URL があれば解析して DB_* 環境変数にセットする # (例: postgresql://user:pass@host:port/dbname) if [ -n "${DATABASE_URL:-}" ]; then eval "$(python - <<'PY' import os,sys,urllib.parse u=os.environ.get('DATABASE_URL','') if not u: sys.exit(0) p=urllib.parse.urlparse(u) print("export DB_HOST='{}'".format(p.hostname or "")) print("export DB_PORT='{}'".format(p.port or "")) print("export DB_USER='{}'".format(p.username or "")) print("export DB_PASSWORD='{}'".format(p.password or "")) print("export DB_NAME='{}'".format((p.path[1:] if p.path else ""))) PY )" fi # Postgres 到達確認 i=0 until pg_isready -h "${DB_HOST:-postgres}" -p "${DB_PORT:-5432}" -U "${DB_USER:-${POSTGRES_USER:-test}}" -d "${DB_NAME:-${POSTGRES_DB:-test_db}}"; do i=$((i+1)) if [ "$i" -ge "$MAX_RETRIES" ]; then echo "Postgres did not become ready" exit 1 fi echo "Waiting for Postgres..." sleep "${SLEEP_POSTGRES}" done # Redis 到達確認(認証がある場合は REDIS_PASSWORD を使う) i=0 REDIS_AUTH_ARGS= if [ -n "${REDIS_PASSWORD:-}" ]; then REDIS_AUTH_ARGS="-a ${REDIS_PASSWORD}" fi until redis-cli -h "${REDIS_HOST:-redis}" -p "${REDIS_PORT:-6379}" $REDIS_AUTH_ARGS ping >/dev/null 2>&1; do i=$((i+1)) if [ "$i" -ge "$MAX_RETRIES" ]; then echo "Redis did not become ready" exit 1 fi sleep "${SLEEP_REDIS}" done echo "All services ready" exec "$@" |
補足(healthcheck と wait スクリプトの役割分担):
- healthcheck はコンテナ内部プロセスやポートが「生きている」かの簡易判定に使います(TCP/HTTP レベル)。
- 待ち合わせスクリプトは「マイグレーション実行可否」「認証を伴う接続チェック」「アプリケーションレベルの準備完了」などより厳密な readiness を担います。
- pg_isready はサーバが接続を受け付けているかを確認しますが、必ずしも「指定ユーザーで認証できるか」を保証するものではありません。認証を検証したい場合は psql での接続試行を行ってください。
開発・テスト・CIでの設定切り替えと待ち合わせ戦略
ここではローカル/CIでの設定切り替えパターンと安全な待ち合わせ方法を示します。各環境で同一ファイルを使いながら挙動を分ける手法を紹介します。
override/profiles/環境変数での切り替え
ローカルは override、CI は profiles を使って振る舞いを分けるのが実務的です。環境変数は .env / CI シークレットで注入します。
- ローカル: docker-compose.override.yml でソースマウントとポート公開を定義します(ホスト競合回避が必須)。
- CI: docker-compose.ci.yml を用意し --profile ci で test サービスを起動します。
- 実行例(ローカル):
- docker compose -f docker-compose.yml -f docker-compose.override.yml up --build
- 実行例(CI):
- docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile ci up --build --abort-on-container-exit --exit-code-from test
ローカルではホストポートの公開、CIでは非公開を徹底するとポート競合を減らせます。
ヘルスチェックと待機(マイグレーション実行)のパターン
ヘルスチェックは簡易判定に留め、マイグレーションとテスト開始は待ち合わせスクリプトに任せるのが堅牢です。
- 推奨パターン:
- 各サービスに簡易 healthcheck(ポート/HTTP)を設定。
- test コンテナのエントリで wait→migrate→test の順で実行。
- あるいは CI の最初のステップで migrate サービスを単体実行して終了コードで判定する方法も有効です。
- 注意点:
- healthcheck のコマンドに環境変数を使う場合は CMD-SHELL を使う必要があります。
- Redis が Unix ソケットで動作している場合は redis-cli の接続先を調整してください。
test サービスの read-only マウントに関するリスクと代替案
read-only マウントはイミュータブル性を保てますが、テストやマイグレーションで書き込みが必要だと失敗します。
- 代替案:
- CI ではソースをイメージに組み込んでマウントしない(推奨)。これによりファイルシステムの互換性問題を回避できます。
- または rw マウント(./:/app:rw)にするか、一時ディレクトリだけを名付けボリュームで書き込み可能にする(例: - /tmp/testdata)。
- 目的に応じて readonly を採用するか決めてください(セキュリティと互換性のトレードオフがあります)。
データ永続化・クリーンアップ、ネットワーク設計と並列テスト、ロギング/シークレット
ここではボリューム設計、並列実行時の名前衝突回避、ログ管理とシークレット運用の実務的な注意点を説明します。
ボリューム設計と初期化の注意点
docker-entrypoint-initdb.d に置いた初期化スクリプトは、Postgres コンテナのデータディレクトリが空のときのみ実行されます。既存ボリュームがあると初期データは読み込まれません。
- 再初期化が必要な場合:
- テスト用なら docker compose down -v でボリュームを削除してから再作成します。
- データを保持する必要がある場合は pg_dump/restore でスナップショット運用を行うか、初期データをマイグレーションで整備してください。
- CI 高速化の工夫:
- ベースイメージに初期 DB を bake しておき起動を早める(pg_dump → イメージに組み込む)。
- 最小限のマイグレーション+シードのみ実行する。
COMPOSE_PROJECT_NAME とクリーンアップ運用(集約)
CI でのリソース衝突を避けるためにプロジェクト名/ボリューム名や DB 名を一意化する運用を標準化します。またクリーンアップ手順は1箇所にまとめて確実に実行してください。
- 一意化の方法例:
- export COMPOSE_PROJECT_NAME=${COMPOSE_PROJECT_PREFIX}-${CI_JOB_ID:-local}-${GITHUB_RUN_ID:-}
- あるいは環境ごとに POSTGRES_DB=test_${CI_JOB_ID}
- クリーンアップ(CI の最後に必ず実行)
- docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans
- 名前付きボリュームを手動で削除する場合: docker volume rm
- 注意:
- down -v は重要ですが多数の箇所で繰り返すと冗長になります。CI ワークフローの一箇所(after_script / teardown)に集約してください。
ロギングとアーティファクトサイズ管理
CIでコンテナログをそのままアーティファクトにする場合、巨大化に注意してください。対策を必ず入れます。
- ログの切り出し・圧縮例:
- docker compose logs --no-color | tail -n 2000 | gzip -9 > compose.log.gz
- docker compose logs --no-color --since 10m > compose-recent.log
- ポイント:
- 長時間のジョブや大量ログ出力のテストではログをtailしてアップロードするか、圧縮してアップロードしてください。
- Artifact ストレージ上限を超えないように事前に制限を設けます。
シークレットの扱いと docker stack deploy / docker compose の違い
Compose の secrets 機能は環境によって実装差があります。Swarm の secret と docker compose plugin の扱いは異なるため注意してください。
- 違いの概略:
- docker stack deploy(Swarm): Swarm のシークレットストアに登録し、サービスに安全に注入する。Swarm の機能に依存する。
- docker compose(プラグイン): Compose Spec による secrets サポートがあるが、実装や扱いが Swarm と完全一致しない場合がある。古い docker-compose ではサポートが弱い。
- 実務的な推奨:
- ポータビリティの高い運用としては CI のシークレット機能で環境変数注入か、ジョブ内で一時ファイルを作成してマウントする運用が無難です(ジョブ終了時に削除)。
- 機密性が高い場合は自己管理 runner + Swarm/KMS などの仕組みを検討してください。
CI統合、代替技術との使い分け、トラブルシューティングと実務テンプレートの実行手順
この記事のテンプレートを CI に組み込む際の具体例と、Testcontainers や Kubernetes といった代替技術の使い分けを示します。CI ランナー固有の制約に注意が必要です。
GitHub Actions/GitLab CI の実例とランナー設定注意
このブロックの目的: GitHub Actions と GitLab CI での代表的な実装例と、DinD(Docker-in-Docker)やソケット共有の注意点を示します。想定される利用シナリオ(CI): hosted runner と self-hosted runner の違いに応じた設定。
- GitHub Actions(ホストされている ubuntu-latest 上で docker compose を直接使う例)
- hosted runner は既に Docker が入っているため、多くの場合 docker compose がそのまま動きます。
- buildx を使う場合は docker/setup-buildx-action を導入すると安定します。
このブロックの目的: GitHub Actions の例ワークフロー。想定される利用シナリオ(CI): 普通の統合テスト(self-hosted を必要としないケース)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
# .github/workflows/ci-compose.yml (抜粋) name: CI - Compose integration on: [push, pull_request] jobs: integration: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build and run Compose tests env: COMPOSE_PROJECT_NAME: ci-${{ github.run_id }} run: | docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile ci up --build --abort-on-container-exit --exit-code-from test - name: Collect logs (tail + compress) run: docker compose -f docker-compose.yml -f docker-compose.ci.yml logs --no-color | tail -n 2000 | gzip -9 > compose.log.gz || true - name: Upload logs uses: actions/upload-artifact@v4 with: name: compose-logs path: compose.log.gz - name: Tear down run: docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans || true |
- GitLab CI(docker:24 + docker:24-dind を使う例)
- dind をサービスとして使う場合は runner 側で privileged: true を許可する必要があることが多いです。共有ランナーでは制限されている場合があるため事前に runner 設定を確認してください。
- 代替として self-hosted runner やソケット共有(/var/run/docker.sock)を検討します。ソケット共有はセキュリティリスクが大きい点に注意。
このブロックの目的: GitLab CI の例(DinD 使用)。想定される利用シナリオ(CI): 自己管理 runner または privileged が許可された環境。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
# .gitlab-ci.yml (抜粋) stages: - test variables: DOCKER_HOST: tcp://docker:2375/ DOCKER_TLS_CERTDIR: "" integration_test: image: docker:24 services: - name: docker:24-dind stage: test script: - export COMPOSE_PROJECT_NAME=ci-$CI_JOB_ID - docker info - docker compose -f docker-compose.yml -f docker-compose.ci.yml --profile ci up --build --abort-on-container-exit --exit-code-from test after_script: - docker compose -f docker-compose.yml -f docker-compose.ci.yml down -v --remove-orphans || true # 注意: 多くの共有ランナーでは privileged を許可していません。 # self-hosted runner を検討してください。 |
代替技術の比較と使い分け(簡易表)
このブロックの目的: Compose / Testcontainers / Kubernetes の実務的な選択基準を示します。想定される利用シナリオ(選定時の意思決定): プロジェクト要件に合わせて技術を選ぶ参考にしてください。
| 技術 | 長所 | 短所 | 選ぶべきケース |
|---|---|---|---|
| Docker Compose | 導入が簡単でローカル開発と親和性が高い | 大規模なクラスター本番挙動と差異が出る | 軽量な統合テスト、ローカルとCIの共通構成 |
| Testcontainers | テストコード内でライフサイクル管理可能、分離が容易 | テスト毎にコンテナ起動で遅くなることがある | 単体テストで外部依存を隔離したい場合 |
| Kubernetes (Kind/k3s) | 本番クラスターに近い検証が可能 | セットアップとリソースコストが高い | 本番に近いE2Eや複雑なオーケストレーション検証 |
よくある失敗と即効対処(Q&A)
- 問題: サービスが接続できない(Connection refused)。
- 対処: docker compose ps / docker compose logs を確認し、healthcheck と待ち合わせスクリプトのログを見直す。
- 問題: depends_on で readiness が保証されていると思っていた。
- 対処: depends_on は起動順のみ。readiness は healthcheck と待機ロジックで保証してください。
- 問題: CI 上で DinD が使えない / 権限エラー。
- 対処: runner 設定を確認し、privileged が許可されないなら self-hosted runner やソケット共有、あるいは buildx を使ったリモートビルドを検討してください。
- 問題: 古いボリュームが残ってテストに影響する。
- 対処: CI の teardown で docker compose down -v を実行するか、手動でボリュームを削除します。永続化が必要ならスナップショット戦略に切り替えます。
実務向けテンプレートのローカル実行とCI組み込み(実行手順)
ここではテンプレートを手元で動かし、問題を切り分けるための段階的手順を示します。手順通りに実行してログを確認してください。
- ローカルでの素早い検証手順(推奨)
- リポジトリを clone して .env.example をコピーし .env を作成します。
- cp .env.example .env
- ローカル専用の override を確認(docker-compose.override.yml)。
- ローカル検証実行:
- docker compose -f docker-compose.yml -f docker-compose.override.yml up --build --abort-on-container-exit --exit-code-from test
-
結果のログを確認・切り出し:
- docker compose logs --no-color | tail -n 2000 > local-compose.log
-
CI への組み込み(概略)
- CI 環境に必要なシークレットを登録します(POSTGRES_PASSWORD 等)。
- CI のワークフローに docker compose 起動ステップを追加します(上記 GitHub Actions 例を参照)。
- CI では必ずユニークな COMPOSE_PROJECT_NAME を使う、または DB 名を差し替えて衝突を防ぎます。
- テスト後は CI の teardown で docker compose down -v --remove-orphans を実行します。
- 失敗時はログを tail して圧縮したアーティファクトを保存してください。
まとめ
Docker Compose テスト環境をローカルとCIで共通化すると、再現性とデバッグ効率が向上します。ここでは即利用できる .env.example、待ち合わせスクリプト、ビルド最適化、CI 統合の注意点を実務的に示しました。
- .env.example を用意してすぐ試せる状態にすること
- healthcheck は簡易判定、待ち合わせスクリプトでマイグレーション等を確実に行うこと
- test コンテナの read-only マウントはリスクがあるためCIではイメージ内実行や rw を検討すること
- COMPOSE_PROJECT_NAME / DB 名は CI で一意化し、クリーンアップを teardown に集約すること
- Dockerfile はマルチステージでビルド依存を分離しイメージサイズを抑えること
これらを踏まえてテンプレートを手元で検証し、CI 環境に順次組み込んでください。