カスタムDockerネットワークとコンテナ通信の実践ガイド

このガイドでは、カスタムDockerブリッジネットワークとコンテナ通信におけるその役割を実践的に探求します。Docker CLIとDocker Composeを使用してネットワークを作成、管理、コンテナを接続する方法を学びます。カスタムネットワークが自動DNS解決を可能にし、分離性を向上させ、サービス間通信を簡素化し、より堅牢でスケーラブルなコンテナ化アプリケーションを実現する方法を発見します。

カスタムDockerネットワークとコンテナ通信の実践ガイド

カスタムDockerネットワークは、複数のコンテナを実行するまではオプションに感じられる機能の1つです。デフォルトのブリッジは簡単なテストには使えますが、ユーザー定義のブリッジを使用すると、予測可能なサービス名、よりクリーンな分離、そして簡単なデバッグが可能になります。Webコンテナ、APIコンテナ、データベースを持つ小さなアプリでは、その違いはすぐにわかります。APIは、Dockerが今日割り当てたIPを追いかける代わりに、db:5432に接続できます。

このガイドでは、単一のDockerホスト上のユーザー定義ブリッジネットワークに焦点を当てます。オーバーレイネットワーク、Kubernetesネットワーキング、Swarmサービスディスカバリーは、マルチホスト環境で関連する問題を解決しますが、ブリッジネットワークは、ローカル開発、小規模デプロイ、Docker Composeプロジェクトにおいて依然として日常的なツールです。

デフォルトのブリッジが扱いにくい理由

Dockerは自動的にbridgeという名前のネットワークを作成します。ネットワークを指定せずにコンテナを実行すると、通常はそこに配置されます。単純なケースでは機能しますが、マルチコンテナアプリケーションには適していません。

ユーザー定義のブリッジネットワークでは、Dockerはコンテナ名とComposeサービス名に対して組み込みのDNSを提供します。デフォルトのブリッジでは、名前ベースのディスカバリーは制限されており、レガシーなリンクは構築すべきパターンではありません。実際的な結果として、カスタムネットワークを使用すると、安定したホスト名でアプリケーションを設定できます。

DATABASE_HOST=db
REDIS_HOST=redis
API_BASE_URL=http://api:3000

これは読みやすく、マシン間で移動しやすく、コンテナIPアドレスよりも脆弱ではありません。

カスタムネットワークは、より明確な境界も作成します。同じネットワークに接続されたコンテナは互いに通信できます。異なるネットワーク上のコンテナは、一方のコンテナを両方に接続するか、ホスト経由でポートを公開しない限り通信できません。これは完全なセキュリティモデルではありませんが、有用な分離レイヤーです。

Docker CLIでネットワークを作成する

ユーザー定義のブリッジを作成します。

docker network create app-net

その上で2つのコンテナを起動します。

docker run -d --name db --network app-net -e POSTGRES_PASSWORD=devpass postgres:16
docker run -d --name adminer --network app-net -p 8080:8080 adminer

adminerコンテナから、データベースのホスト名はdbです。IPを知る必要はありません。

ネットワークを検査します。

docker network inspect app-net

ドライバー、サブネット、ゲートウェイ、接続されたコンテナが表示されます。デバッグ時に、このコマンドは基本的な質問に答えます。2つのコンテナが実際に同じネットワーク上にあるかどうかです。

既存のコンテナを接続できます。

docker network connect app-net some-container

そして切断します。

docker network disconnect app-net some-container

Dockerは、コンテナが接続されている間はネットワークを削除しません。最初にコンテナを切断するか削除します。

docker network rm app-net

公開ポートはコンテナ間ポートとは異なる

よくある混乱:同じDockerネットワーク上のコンテナは、互いに通信するためにホストポートを公開する必要はありません。公開ポートは、ホストまたはホスト外部からのトラフィック用です。

APIコンテナがポート3000でリッスンし、Webコンテナが同じネットワーク上にある場合、Webコンテナは次のように呼び出すことができます。

http://api:3000

ラップトップのブラウザや別のホストからDockerホスト経由でAPIにアクセスしたい場合にのみ、-p 3000:3000が必要です。

つまり、本番環境のようなセットアップでは、Docker外部の何かが直接アクセスする必要がない限り、データベースは通常ホストポートを公開すべきではありません。代わりに、APIがプライベートDockerネットワークを介してdb:5432に到達できるようにします。

通常のマルチサービスアプリにはComposeを使用する

Docker Composeは、定義しなくてもプロジェクトのデフォルトネットワークを作成します。サービスはサービス名で互いに到達できます。

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    depends_on:
      - api

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass

このファイルでは、apiはホスト名dbを使用してdbに到達できます。webは、アプリケーションレベルの設定がそこを指していると仮定すると、ホスト名apiを使用してapiに到達できます。

また、より明確な意図や分離が必要な場合に、名前付きネットワークを定義することもできます。

services:
  web:
    image: nginx:latest
    ports:
      - "8080:80"
    networks:
      - frontend

  api:
    image: my-api:latest
    environment:
      DATABASE_HOST: db
    networks:
      - frontend
      - backend

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: devpass
    networks:
      - backend

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge

ここでは、webはネットワークを共有していないため、dbと直接通信できません。apiが2つのアプリケーションレイヤー間のブリッジです。これは実際のサービスにとって有用な形状です。エッジサービスのみをホストに公開し、データベースをプライベートに保ち、各サービスを通信が必要な場所にのみ接続します。

depends_onは準備完了ではない

Composeのdepends_onは、一般的なComposeの使用法では起動順序を制御しますが、データベースが接続を受け入れる準備ができていることを保証するものではありません。APIはdbコンテナプロセスが起動した後に開始されても、PostgreSQLが初期化中であるために失敗する可能性があります。

アプリケーションでリトライを使用して準備完了を処理するか、ヘルスチェックと、Composeのバージョンとワークフローに応じてサービスヘルスを尊重するCompose設定を使用します。それでも、データベースは初期起動後に再起動する可能性があるため、アプリケーションレベルのリトライロジックが最も信頼性の高い習慣です。

実用的なAPI設定では、DATABASE_HOST=dbを使用し、明確なエラーで終了する前に短時間接続をリトライします。

カスタムサブネットは便利だが、使いすぎない

サブネットを選択できます。

docker network create --subnet 172.28.0.0/16 app-net

Composeの場合:

networks:
  backend:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

これは、Dockerの自動サブネットがVPN、オフィスネットワーク、またはホスト上の別のルートと重複する場合に役立ちます。ほとんどのプロジェクトでは必要ありません。コンテナIPをハードコーディングすることは稀であるべきです。サービス名が通常はより良い契約です。

ネットワーク通信のトラブルシューティング

あるコンテナが別のコンテナに到達できない場合、次の順序で確認します。

  1. 両方のコンテナが実行中ですか?
  2. 同じネットワークに接続されていますか?
  3. クライアントはサービス/コンテナ名を使用していますか?localhostではありませんか?
  4. サーバーは期待されるポートとインターフェースでリッスンしていますか?
  5. ポートはホストアクセスが必要な場合にのみ公開されていますか?

localhostの間違いは特に一般的です。コンテナ内では、localhostは同じコンテナを意味し、Dockerホストや別のサービスではありません。APIがlocalhost:5432に接続しようとする場合、APIコンテナ内でPostgreSQLを探しています。データベースサービスがdbという名前の場合は、db:5432を使用します。

ネットワークを検査します。

docker network inspect app-net

同じネットワーク上で一時的な診断コンテナを実行します。

docker run --rm -it --network app-net alpine sh

その中で、必要に応じてツールをインストールまたは使用します。

getent hosts db
nc -vz db 5432

最小限のイメージには、nccurl、またはDNSツールがインストールされていない場合があります。短命のデバッグコンテナは、アプリケーションイメージにトラブルシューティングパッケージを追加するよりもクリーンなことがよくあります。

賢明なデフォルトパターン

ほとんどのシングルホストアプリでは、Composeを使用してプロジェクトネットワークを作成させます。frontendbackendなどの分離が必要な場合に、明示的なネットワークを追加します。内部トラフィックにはサービス名を使用します。人間、リバースプロキシ、または外部システムが到達する必要があるポートのみを公開します。

これにより、説明が簡単なセットアップが得られます。

  • ブラウザはwebがポートを公開しているため、localhost:8080に到達します。
  • webはDockerネットワークを介してapiに到達します。
  • apiはバックエンドネットワークを介してdbに到達します。
  • dbには、実際の運用上の理由がない限り、ホストポートはありません。

カスタムDockerネットワークは単なる便利な機能ではありません。それらは、たまたま同じマシンで実行されているコンテナと、明確な通信モデルを持つサービスの違いです。

ネットワークエイリアスは移行を容易にする

アプリケーションが、Composeサービス名として使用したくないホスト名を期待する場合があります。ネットワークにエイリアスを追加できます。

services:
  postgres:
    image: postgres:16
    networks:
      backend:
        aliases:
          - database

networks:
  backend:
    driver: bridge

backend上のコンテナは、サービスにpostgresまたはdatabaseとして到達できるようになりました。これは、すでにDATABASE_HOST=databaseを使用している古いアプリを移行するときに便利ですが、どこでもエイリアスを使用するわけではありません。アプリケーション設定を制御できる場合は、サービス名の方がシンプルです。

ホストアクセスは別の問題

コンテナが別のコンテナと通信することは、コンテナがDockerホストと通信することとは異なります。Docker Desktopでは、host.docker.internalが一般的に利用可能です。Linuxでは、サポートはDockerのバージョンと設定に依存します。多くのチームは、必要なときに明示的に追加します。

docker run --add-host=host.docker.internal:host-gateway ...

これは控えめに使用します。コンテナがホスト上で直接実行されているサービスに大きく依存している場合、セットアップがCIや別の開発者のマシンで再現しにくくなる可能性があります。データベースやキャッシュの場合、依存関係を同じDockerネットワーク上の別のサービスとして実行する方が通常はクリーンです。

内部ポートはDockerfileのコメントではなく、プロセスに一致させるべき

Dockerネットワーキングは、ツールがメタデータとして使用しない限り、DockerfileのEXPOSE行を気にしません。アプリケーションは、呼び出すポートで実際にリッスンする必要があります。Nodeアプリが3000でリッスンする場合、誰かが誤ってEXPOSE 8080と書いたとしても、他のコンテナはapi:3000を使用する必要があります。

また、バインドアドレスも確認します。コンテナ内で127.0.0.1でリッスンしているサービスは、他のコンテナから到達できない場合があります。コンテナ間トラフィックの場合、プロセスは通常0.0.0.0またはコンテナのネットワークインターフェースでリッスンする必要があります。

ネットワーク設計は退屈に保つ

機能が存在するからといって、多くのネットワークを作成したくなります。実際に必要な通信パスから始めます。小さなアプリでは、デフォルトのComposeネットワークだけが必要かもしれません。より現実的なWebアプリでは、frontendbackendが必要になるかもしれません。それを超えて、各新しいネットワークには、インシデント中に誰かが説明できる理由が必要です。

優れたネットワーク設計はトラブルシューティングを容易にします。webdbに到達できず、意図的にネットワークを共有していないことがわかっている場合、答えは神秘的ではなくアーキテクチャ上のものです。すべてのサービスがすべてのネットワークに接続されている場合、ネットワークはもはや何も文書化しません。

出荷前の現実世界のレビューパス

スクリプトやコンテナのセットアップが完了したと見なす前に、午前2時にデバッグしなければならない次の人のつもりで一度読んでください。それによって気づくことが変わります。スクリプト作成時に意味があったプロンプトが、CIログに表示されると曖昧になる可能性があります。明白に思えたDockerサービス名が、アプリケーションの変数名と一致しない場合があります。開発には安全だが本番環境では危険なBashのデフォルトがあるかもしれません。

私は、意図的に扱いにくい値を使って短いドライランを行うのが好きです。スペースを含むパスを使用します。空のオプション値を使用します。ダッシュで始まるファイル名を試します。異なる作業ディレクトリからスクリプトを実行します。期待される環境変数の1つなしでコンテナを起動します。これらのテストは派手ではありませんが、通常最初に壊れる前提をキャッチします。

また、失敗メッセージも確認します。出力がfailedだけの場合、記事のアドバイスは実装に反映されていません。有用な失敗は、使用された値、失敗したチェック、オペレーターが変更できる内容を示します。それはすべての環境変数をダンプしたり、シークレットを印刷したりすることを意味しません。具体的な情報が役立つ場合に具体的であることを意味します。設定パス、欠落しているコマンド名、ネットワーク名、サービスホスト名、プロセスがバインドしようとしたポートなどです。

最後の習慣は、例をシステムが実際に実行される方法に近づけることです。本番環境でComposeを使用する場合は、Composeでテストします。スクリプトがsystemdによって起動される場合は、systemdまたは同様に最小限の環境でテストします。コマンドがコピー&ペーストしても安全であるべき場合は、引用符、--セパレーター、検証を例自体に含めます。読者は、警告よりも動作するパターンをコピーすることがよくあります。

そのレビューパスは官僚主義ではありません。それは、小さな自動化を退屈に保つ方法です。退屈とは、シェルプロンプト、設定ローダー、変数展開、コンテナ診断、Dockerネットワーキングに求めるものです。動作が驚き少なければ少ないほど、次のオペレーターがそれを信頼しやすくなります。

Dockerネットワーキングについては、Composeファイルの横またはサービスREADMEに意図したトラフィックパスを文書化します。web -> api:3000 -> db:5432のような短いメモは、多くの混乱を防ぎます。また、レビューも容易になります。誰かがデータベースポートを公開したり、webをバックエンドネットワークに接続したりした場合、その変更は意図したパスに対して正当化する必要があります。

アプリが成長したら、ネットワークマップを再検討します。古いエイリアス、未使用の公開ポート、もう必要のないネットワークに接続されたサービスは、静かな運用リスクの原因です。