Skip to main content
CI/CD8 min read

CI/CDにおけるDocker Compose接続エラーの修正

Jenkinsで「接続が拒否されました」エラーのデバッグに4時間費やしました。CIパイプラインにおけるDockerネットワーキングについて学んだことを紹介します。

Part ofCloud & Infrastructure->
By Jason TeixeiraJanuary 5, 2024
DockerJenkinsCI/CDTroubleshooting
Share:
On this page

こんな状況を想像してみてください。ローカルマシンでは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内で実行される場合:

code panelTXT
DATABASE_URL=postgres://user:pass@postgres:5432/app

テストがCIホスト上で実行され、Composeがポートを公開している場合:

code panelTXT
DATABASE_URL=postgres://user:pass@127.0.0.1:5432/app

テストが別のCIコンテナで実行される場合、CIプラットフォームのサービスネットワーキングが設定されるまでは、どちらも機能しない可能性があります。

CIネットワーキングの判断compose -> tests
Compose serviceNetworkTest runnerDatabase

適切なホスト名は、テストランナーがどこに存在するかに依存します。サービス名はComposeネットワーク内で機能します。公開されたlocalhostポートはホストから機能します。

depends_onを準備完了として信用しない

depends_onは起動順序を制御できます。しかし、Postgres、Redis、またはアプリケーションが接続を受け入れる準備ができていることを保証するものではありません。

よくある悪い例:

code panelYAML
services:
  api:
    depends_on:
      - postgres

これは単にpostgresコンテナがapiより先に起動することを意味するだけです。マイグレーションが実行されたことを意味しません。TCPの準備ができていることを意味しません。データベースが認証を受け入れたことを意味しません。

ヘルスチェックか明示的な待機スクリプトを使用してください。

code panelYAML
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チェックは地味ですが有用です:

code panelBASH
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) })"

これが失敗する場合、アプリケーションのテストスイートをデバッグする段階ではありません。

異なる境界に異なる接続文字列を使用する

一つのクリーンなパターンは、境界を明示的にすることです:

code panelENV
DATABASE_URL_INTERNAL=postgres://app:app@postgres:5432/app
DATABASE_URL_HOST=postgres://app:app@127.0.0.1:5432/app

そして、CIジョブはコマンドが実行される場所に基づいて適切な方を選択します。

これは、一つのURLをどこでも機能させようとするよりも、はるかに魔法の少ない方法です。

マイグレーションと準備完了を分離する

データベースは、スキーマの準備ができる前に正常になる可能性があります。

アプリケーションにマイグレーションが必要な場合、それを明示的なパイプラインステップにします:

code panelBASH
docker compose up -d postgres
docker compose run --rm migrate
docker compose run --rm test

または、以下を待機するサービス内でテストを実行します:

  • データベースの正常性
  • マイグレーションの完了
  • シードデータのロード

そうしないと、より悪い種類の障害が発生します。アプリのバグのように見えるが、実際にはセットアップの競合状態である断続的なテストエラーです。

すべてのCI障害で欲しいデバッグ出力

シークレットをダンプしないでください。環境の形状を出力してください。

有用な出力:

  • Docker Composeのサービスとステータス
  • 依存関係のコンテナログ
  • パスワードを伏せ字にした、解決済みのホストとポート
  • ネットワーク名
  • ヘルスチェックのステータス
  • マイグレーションのステータス

例:

code panelBASH
docker compose ps
docker compose logs --tail=80 postgres
docker network ls

目標は、次の障害を一度のパスで診断可能にすることです。

本番環境からの教訓

CIネットワーキングの苦労は、本番環境での統合の苦労の予告編です。

テストが「うまくいくことを願う」に依存しているなら、デプロイメントもおそらくそうでしょう。サービス境界を明示的にし、ヘルスチェックを追加し、準備完了とマイグレーションを分離し、適切な情報をログに記録してください。

そうすることで、「私のマシンでは動く」をパイプラインが証明できるものに変えられるのです。

Reader route

article -> proof -> offer

ReadClusterProofScope

cluster

Cloud & Infrastructure

intent

CI/CD

route

next step

What to do with this

Turn the note into a build path.

If this topic maps to a real business problem, keep reading the cluster, study the academy path, or route the work into a scoped engagement.

Jason Teixeira
Written by
Jason Teixeira
Founder, Sage Ideas Studio · Principal Engineer
livebuild 5d6c8652026-08-05 06:00Z
// solo studio// no analytics resold// every commit human-reviewed