こんな状況を想像してみてください。ローカルマシンではDocker Composeの設定が完璧に動作している。ところがCIにプッシュした途端、すべての統合テストがConnection refusedで失敗する。
データベースコンテナは「実行中」、APIコンテナは「正常」、テストプロセスが起動する。そして必要なサービスに接続できない。
この失敗はランダムに見えますが、一つだけ覚えておくべきことがあります。ローカルのDockerネットワークとCIのDockerネットワークは同じ環境ではないのです。
ローカル環境はあなたを欺く
自分のマシンでは、localhost:5432でPostgresに接続するかもしれません。
Composeネットワーク内では、別のコンテナは通常postgres:5432に接続すべきです。ここでpostgresはサービス名です。
CIでは、テストランナーは以下のいずれかの場所に存在します:
- Composeネットワークの内部
- Composeネットワークの外部(ホスト上)
- CIサービスのコンテナ内部
- ネストされたDocker executorの内部
これら4つのケースでは、異なるホスト名を使用します。
そのため、接続文字列がローカルでは「正しく」、パイプラインでは間違っているということが起こるのです。
まず、テストプロセスがどこで実行されているかを特定する
ポートを変更する前に、一つの質問をしてください:
テストコマンドはComposeサービス内で実行されていますか、それともCIホスト上で実行されていますか?
テストがCompose内で実行される場合:
DATABASE_URL=postgres://user:pass@postgres:5432/appテストがCIホスト上で実行され、Composeがポートを公開している場合:
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/appテストが別のCIコンテナで実行される場合、CIプラットフォームのサービスネットワーキングが設定されるまでは、どちらも機能しない可能性があります。
適切なホスト名は、テストランナーがどこに存在するかに依存します。サービス名はComposeネットワーク内で機能します。公開されたlocalhostポートはホストから機能します。
depends_onを準備完了として信用しない
depends_onは起動順序を制御できます。しかし、Postgres、Redis、またはアプリケーションが接続を受け入れる準備ができていることを保証するものではありません。
よくある悪い例:
services:
api:
depends_on:
- postgresこれは単にpostgresコンテナがapiより先に起動することを意味するだけです。マイグレーションが実行されたことを意味しません。TCPの準備ができていることを意味しません。データベースが認証を受け入れたことを意味しません。
ヘルスチェックか明示的な待機スクリプトを使用してください。
services:
postgres:
image: postgres:16
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app"]
interval: 5s
timeout: 5s
retries: 12
api:
depends_on:
postgres:
condition: service_healthyこれでもすべてのCIプラットフォームで解決するわけではありませんが、最も一般的な競合状態は除去できます。
4つの障害クラスを確認する
Connection refusedを見たときは、この順序で確認します。
TCPチェックは地味ですが有用です:
node -e "require('net').connect(5432, process.env.DB_HOST).on('connect', () => { console.log('ok'); process.exit(0) }).on('error', e => { console.error(e.message); process.exit(1) })"これが失敗する場合、アプリケーションのテストスイートをデバッグする段階ではありません。
異なる境界に異なる接続文字列を使用する
一つのクリーンなパターンは、境界を明示的にすることです:
DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/appそして、CIジョブはコマンドが実行される場所に基づいて適切な方を選択します。
これは、一つのURLをどこでも機能させようとするよりも、はるかに魔法の少ない方法です。
マイグレーションと準備完了を分離する
データベースは、スキーマの準備ができる前に正常になる可能性があります。
アプリケーションにマイグレーションが必要な場合、それを明示的なパイプラインステップにします:
docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm testまたは、以下を待機するサービス内でテストを実行します:
- データベースの正常性
- マイグレーションの完了
- シードデータのロード
そうしないと、より悪い種類の障害が発生します。アプリのバグのように見えるが、実際にはセットアップの競合状態である断続的なテストエラーです。
すべてのCI障害で欲しいデバッグ出力
シークレットをダンプしないでください。環境の形状を出力してください。
有用な出力:
- Docker Composeのサービスとステータス
- 依存関係のコンテナログ
- パスワードを伏せ字にした、解決済みのホストとポート
- ネットワーク名
- ヘルスチェックのステータス
- マイグレーションのステータス
例:
docker compose ps
docker compose logs --tail=80 postgres
docker network ls目標は、次の障害を一度のパスで診断可能にすることです。
本番環境からの教訓
CIネットワーキングの苦労は、本番環境での統合の苦労の予告編です。
テストが「うまくいくことを願う」に依存しているなら、デプロイメントもおそらくそうでしょう。サービス境界を明示的にし、ヘルスチェックを追加し、準備完了とマイグレーションを分離し、適切な情報をログに記録してください。
そうすることで、「私のマシンでは動く」をパイプラインが証明できるものに変えられるのです。
