Docker・インフラ構築

Dockerブリッジネットワークの内部構造:コンテナ間通信とiptablesの制御ルール

seoTitle: Dockerブリッジネットワークの内部構造とiptables…

Dockerブリッジネットワークの内部構造:コンテナ間通信とiptablesの制御ルール

metaDescription: Dockerブリッジネットワークの仕組みを、veth・docker0・iptablesまで順に解説。コンテナ間通信やDNS、ポート公開の内部動作が分かります。

Dockerブリッジネットワークの内部構造:コンテナ間通信とiptablesの制御ルール

Dockerコンテナ同士が、同じホスト上で特別な設定をしなくても通信できるのは、コンテナが単独でネットワークに参加しているからではありません。Linuxカーネルが提供するNetwork Namespace、vethペア、仮想ブリッジ、そしてiptablesによるパケット転送が、ひとつの仕組みとして連動しているためです。

ところが、通信できるはずのコンテナから外部へ接続できなかったり、公開したポートへのアクセス制御が効かなかったりすると、原因をDocker Composeの設定だけで探してしまいがちです。その場合、実際にパケットがどの経路を通っているのかが見えないため、設定を変更しても再現性のある解決にたどり着きにくくなります。

ここでは、Dockerブリッジネットワークの仕組みを、コンテナの仮想インターフェースからiptablesのチェーンまで順番に確認していきましょう。コマンドの結果を暗記するのではなく、なぜその通信が成立するのかを追える状態にすることが目標です。

Dockerのブリッジネットワークは何を接続しているのか

Dockerをインストールしてデーモンを起動すると、多くの環境ではデフォルトのブリッジネットワークが作成されます。ホスト側には仮想ブリッジである docker0 が現れ、一般的には 172.17.0.1 がゲートウェイとして割り当てられます。

同じネットワーク上に接続されたコンテナには、172.17.0.0/16 の範囲からIPアドレスが割り当てられます。ただし、このサブネットやゲートウェイIPはすべての環境で固定される値ではありません。既存のネットワークとの競合を避けるために変更されていることもありますので、実際の状態は docker network inspect bridgeip addr show docker0 で確認してみましょう。

構成を整理すると、次のようになります。

構成要素役割確認の観点
Network Namespaceコンテナごとに独立したネットワーク空間を提供するコンテナ内部の eth0 がホストと分離されている
vethペアコンテナ側とホスト側をつなぐ仮想ケーブル片側がコンテナ、もう片側がブリッジに接続される
docker0複数のコンテナを同じL2ネットワークへ接続するブリッジとしてARPやフレーム転送を行う
iptables転送、NAT、ポート公開などを制御するFORWARDDOCKERDOCKER-USER を確認する
Docker内部DNSユーザー定義ネットワークで名前解決を提供するコンテナ内のDNSサーバーは通常 127.0.0.11

ここで大切なのは、コンテナの eth0 がホストの物理NICに直接つながっているわけではない、という点です。コンテナの中から見える eth0 は、vethペアの片側です。もう片側はホスト側のネットワーク空間に置かれ、docker0 のような仮想ブリッジへ接続されます。

Network Namespaceでコンテナのネットワークを分離する

LinuxのNetwork Namespaceは、ネットワークインターフェース、ルーティングテーブル、ARPテーブル、ソケットなどを分離する仕組みです。Dockerはこの仕組みを使い、コンテナごとに独立したネットワーク環境を用意します。

そのため、ホスト上で ip addr を実行したときに見えるインターフェースと、コンテナ内で ip addr を実行したときに見えるインターフェースは一致しません。ホストから見ればコンテナ内部の eth0 は別のネットワーク名前空間に存在し、コンテナから見ればホストの物理インターフェースは直接見えない、という関係です。

ただし、完全に切り離されているわけではありません。vethペアという仮想的なケーブルによって、コンテナのネットワーク空間とホストのブリッジが接続されています。

vethペアは仮想的な両端のインターフェース

vethは、常にペアで作られる仮想ネットワークインターフェースです。片側で送信したパケットは、もう片側から受信されます。

コンテナを起動すると、概念的には次のような接続が作られます。

  • コンテナのNetwork Namespace内に eth0 を配置する
  • 対になるvethインターフェースをホスト側に配置する
  • ホスト側のvethを docker0 ブリッジへ接続する
  • コンテナ側の eth0 にIPアドレスとデフォルトルートを設定する

ホスト側のvethには、分かりやすい名前ではなく、生成された長いインターフェース名が付けられることがあります。そのため、ホストの ip link を見たときに、コンテナ名と直接対応するインターフェースが見つからない場合もあります。

このとき、コンテナから外部へ送信したパケットは、まずコンテナ内部の eth0 からvethペアを通り、ホスト側のブリッジへ入ります。ここから先は、同じブリッジに接続された別のコンテナへ進むのか、ホストのルーティングとNATを通って外部へ進むのかによって経路が分かれます。

Dockerのネットワーク障害は、コンテナの設定だけでなく「どのNetwork Namespaceから、どのブリッジを通り、どのチェーンへ入ったか」を追うと再現性を持って切り分けられます。

コンテナ間通信が成立するプロセス

同じブリッジネットワークに参加しているコンテナ同士は、基本的に同じL2セグメント上のホストのように通信します。たとえば、コンテナAからコンテナBのIPアドレスへTCP接続を行う場合、コンテナAは宛先IPが同一サブネットにあるかどうかを判断し、ゲートウェイではなくARPによって宛先のMACアドレスを解決します。

パケットの流れは次のように考えると分かりやすいでしょう。

1. コンテナAのアプリケーションが宛先IPへ接続を開始する

2. コンテナAの eth0 が宛先IPのMACアドレスをARPで確認する

3. パケットがvethペアを通ってホスト側へ出る

4. docker0 が宛先MACアドレスを見て、コンテナB側のポートへ転送する

5. コンテナBのNetwork Namespace内にある eth0 がパケットを受信する

この通信では、外部向けのIPマスカレードは通常必要ありません。宛先が同じブリッジ内にあるため、ホストの外部インターフェースへ出ていく必要がないからです。

一方、コンテナAがインターネット上のサーバーへ接続する場合は、宛先が同じブリッジ内にありません。コンテナAはデフォルトゲートウェイである docker0 側のアドレスへパケットを送り、ホストのルーティング処理を経て外部へ出ていきます。

デフォルトブリッジとユーザー定義ブリッジの違い

Dockerには、最初から用意されるデフォルトの bridge ネットワークと、利用者が作成するユーザー定義ブリッジネットワークがあります。どちらもLinuxのブリッジを利用しますが、名前解決の扱いに違いがあります。

ユーザー定義ブリッジネットワークでは、Dockerの内部DNSがコンテナ名やネットワークエイリアスを解決します。コンテナ内部の /etc/resolv.conf を確認すると、通常は 127.0.0.11 がDNSサーバーとして設定されています。

たとえばComposeで次のようなサービスを定義した場合、アプリケーションコンテナからデータベースコンテナへ接続するとき、IPアドレスではなくサービス名をホスト名として利用できます。

  • Webアプリケーションのサービス名を app
  • データベースのサービス名を db
  • app からデータベースへ db:3306 で接続

この名前解決は、ホストのDNSサーバーがDockerコンテナの名前を知っているからではありません。Dockerエンジンがネットワークに参加しているコンテナの情報を管理し、内部DNSがその名前を対応するIPアドレスへ解決しています。

ここで注意したいのは、デフォルトの docker0 上で、常にコンテナ名による名前解決が使えるわけではないことです。Composeを使う場合は通常、プロジェクト用のユーザー定義ネットワークが作られますが、手動で docker run --network bridge を使っている環境では、名前解決の前提が異なります。

実際にどのネットワークへ接続されているかは、次のように確認できます。

  • docker network ls でネットワーク一覧を表示する
  • docker network inspect ネットワーク名 で接続中のコンテナを確認する
  • docker inspect コンテナ名 でIPアドレスとネットワーク設定を見る
  • コンテナ内で cat /etc/resolv.conf を実行し、DNS設定を確認する

IPアドレスを環境変数へ固定してしまうと、コンテナの再作成やスケールによってアドレスが変わったときに接続が壊れます。サービス名を利用できる構成なら、名前解決の仕組みに任せたほうが設定の再現性を保ちやすくなります。

外部通信を支えるIP転送とIPマスカレード

コンテナから外部のパッケージリポジトリ、API、メールサービスなどへアクセスできるのは、Dockerがホストのネットワーク機能を利用しているためです。ここでは、LinuxカーネルのIP転送と、iptablesのNAT処理が関係します。

まず、Linuxホストは受信したパケットを別のインターフェースへ転送できなければなりません。この設定が net.ipv4.ip_forward=1 です。値が無効になっていると、コンテナからパケットがホストへ到達しても、外部インターフェースへ進めません。

現在の値は、ホスト上で sysctl net.ipv4.ip_forward を実行すると確認できます。Dockerの起動時に関連設定が変更される場合もありますが、クラウドイメージやセキュリティ設定、独自の初期化処理によって状態が変わることもあります。通信障害があるときは、Docker Composeの設定だけでなくカーネルパラメータも確認してみましょう。

次に必要になるのが、IPマスカレードです。

コンテナから送信されるパケットの送信元IPアドレスは、たとえば 172.17.0.x のようなDocker内部のプライベートアドレスです。このアドレスは、インターネット上の戻り先としてそのまま利用できません。そこでホストの外部インターフェースから送信する際に、送信元をホストのIPアドレスへ変換します。

この処理がNAPT、一般的にはIPマスカレードと呼ばれるものです。iptablesのnatテーブルにあるMASQUERADEルールによって、外部へ出るパケットの送信元アドレスが書き換えられます。通信の戻りパケットは、ホスト側で保持している変換情報を使って、元のコンテナへ戻されます。

通信の流れを、役割ごとに分けると次のようになります。

  • コンテナの eth0 が外部宛てのパケットを送信する
  • vethペアを経由して docker0 へ入る
  • ホストのルーティングが外部インターフェースへの転送を判断する
  • FORWARD チェーンなどで転送可否が処理される
  • natテーブルのMASQUERADEによって送信元アドレスが変換される
  • ホストの外部インターフェースからインターネットへ送信される

この仕組みを理解しておくと、コンテナ内ではDNSだけ失敗する、IPアドレスへの接続はできるが名前解決だけできない、外部接続だけタイムアウトする、といった症状を分けて考えられます。

DNS障害と外部通信障害は分けて調べる

コンテナから外部へ接続できないとき、最初からiptablesのルールを変更するのはおすすめしません。まず、問題が名前解決なのか、IPレベルの到達性なのか、TCPポートの応答なのかを分けます。

たとえば、次の順番で確認してみましょう。

1. コンテナからデフォルトゲートウェイへ到達できるか確認する

2. 外部のIPアドレスへ通信できるか確認する

3. ドメイン名の名前解決ができるか確認する

4. 対象サービスのTCPポートへ接続できるか確認する

5. ホストの転送設定とNATルールを確認する

DNS名だけでなくIPアドレスでも接続できないなら、Docker内部DNSだけが原因とは限りません。逆にIPアドレスへ接続できるのにドメイン名だけ失敗するなら、127.0.0.11 から上流DNSサーバーへ問い合わせる経路や、ホスト側のDNS設定を確認する必要があります。

なお、コンテナ内に pingdig がインストールされていないこともあります。診断用のツールがないこと自体をネットワーク障害と誤認しないようにしてください。アプリケーションが実際に使うTCP接続を、curl やアプリケーションのログで確認するほうが適切な場合もあります。

ポート公開はINPUTチェーンではなくFORWARDの経路で考える

Docker Composeで 8080:80 のようなポートマッピングを設定すると、ホストの8080番ポートへの接続がコンテナの80番ポートへ転送されます。これは単なるプロセス間のリダイレクトではなく、宛先IPアドレスとポート番号を変換するDNATを含むネットワーク処理です。

外部クライアントからホストへパケットが届いた場合、Dockerは宛先をコンテナのIPアドレスと公開先ポートへ書き換え、その後にパケットをコンテナへ転送します。このパケットはホスト自身がローカルプロセスとして受信するのではなく、別のネットワーク名前空間へ転送されます。

そのため、Dockerのポートフォワーディングを制御したいときに、ホストの INPUT チェーンへルールを追加しても期待どおりに効かないことがあります。確認すべき中心は、パケットが通過する FORWARD チェーンと、Dockerが用意する関連チェーンです。

ここは非常に間違えやすい部分です。ホストの8080番ポートを公開しているため、つい「ホストへの入力」と考えてしまいますが、パケットの最終的な宛先がコンテナである場合、Linuxカーネルから見れば転送パケットです。

公開ポートを止めたいのにINPUTチェーンだけを見ているなら、見ている場所が違う可能性があります。Dockerのポート転送は、FORWARDされるパケットとして追いかけてみましょう。

DNATとフィルタリングの順番

iptablesの処理は、テーブルとチェーンが単純に一列へ並んでいるわけではありません。受信インターフェース、宛先、ルーティング判断、NAT、フィルタリングなどが関係し、パケットの方向によって通過する場所も変わります。

Dockerが公開ポートを作成すると、natテーブルのPREROUTINGなどを利用して宛先をコンテナへ変換します。そのうえで、フィルタリングのFORWARD経路を通り、コンテナ側のインターフェースへ進みます。

調査の際は、次のような情報を別々に確認すると整理しやすくなります。

  • docker ps で公開ポートの設定を確認する
  • docker port コンテナ名 でホスト側とコンテナ側の対応を見る
  • iptables -t nat -S でDNATやMASQUERADEのルールを見る
  • iptables -S FORWARD で転送ポリシーと関連ルールを見る
  • iptables -S DOCKER でDockerが生成した許可ルールを確認する
  • iptables -S DOCKER-USER で利用者側の制御ルールを確認する

環境によってはiptablesコマンドの背後でnftablesが利用されている場合もあります。LinuxディストリビューションやDockerの設定によって表示や管理方法が変わるため、ルールの確認結果をそのまま別環境へ当てはめないように注意してください。

DOCKERチェーンとDOCKER-USERチェーンの役割

Dockerは、ネットワーク作成やポート公開の状態に応じてiptablesのルールを自動生成します。これらのルールはコンテナの起動・停止、ネットワークの再作成、Dockerデーモンの再起動などに応じて変化します。

ここで、Dockerが管理する DOCKER チェーンへ直接カスタムルールを追加するのは避けてください。Dockerが自動的に再構成した際に、利用者が追加したルールが消えたり、想定しない順序になったりする可能性があるためです。

利用者が独自のiptablesルールを追加する場所として推奨されるのが DOCKER-USER チェーンです。Dockerの自動ルールより前に、利用者が用意したフィルタリングを適用するためのチェーンとして扱えます。

たとえば、外部からコンテナへのアクセスを送信元ネットワーク単位で制限したい場合、Dockerの DOCKER チェーンを編集するのではなく、DOCKER-USER チェーンに条件を追加します。こうすると、コンテナの再作成によって公開ポートのルールが変わっても、利用者側のアクセス制御を分離して管理できます。

DOCKER-USERにルールを置く理由

Dockerの自動生成領域と、利用者が管理する領域を分離することには、三つの利点があります。

第一に、責任範囲が明確になります。Dockerが作ったルールはDockerの構成に由来し、独自のアクセス制御は運用者が管理する、という境界を作れます。

第二に、コンテナの再作成に強くなります。Composeでサービスを再構築すると、コンテナのIPアドレスや内部インターフェースが変わることがあります。Dockerの自動ルールに依存した変更は再生成の影響を受けますが、DOCKER-USER 側で送信元ネットワークや公開ポートなどを条件にすれば、より安定して管理できます。

第三に、調査の順番が分かりやすくなります。通信が失敗したとき、まず DOCKER-USER に意図しないDROPやREJECTがないかを見ることができます。その後でDockerの自動ルール、FORWARDポリシー、NATへ進めば、確認範囲を段階的に広げられます。

ただし、DNAT後の宛先ポートを条件にしたい場合は注意が必要です。パケットが DOCKER-USER に到達する時点で、宛先アドレスやポートが変換済みになっていることがあります。元の接続先を条件にしたい場合は、接続追跡を利用して変換前の情報を参照する設計が必要です。

この部分はルールの見た目だけで判断しにくいため、いきなり本番環境へ投入せず、検証用のネットワークでパケットカウンターやログを確認しながら適用してみましょう。

ルールを追加するときに確認する順番

iptablesの設定は、一行追加すれば終わるものではありません。既存の許可ルールやチェーンの戻り先によって結果が変わるため、次のような順序で考えると事故を減らせます。

1. どの方向の通信かを特定する

外部から公開ポートへ入る通信なのか、コンテナから外部へ出る通信なのか、コンテナ同士の通信なのかを分けます。

2. パケットがどのインターフェースを通るかを見る

docker0、ユーザー定義ブリッジ、ホストの物理NICなど、入口と出口を確認します。

3. INPUT、OUTPUT、FORWARDのどれかを判断する

宛先がホスト自身ならINPUT、送信元がホスト自身ならOUTPUT、コンテナなど別のネットワーク空間へ通過するならFORWARDが中心になります。

4. NAT前後のアドレスとポートを区別する

公開ポートや外部接続では、DNATやMASQUERADEによって情報が変わります。

5. DROPの位置と、最後に戻るチェーンを確認する

ルールが一致しなければ許可されるのか、チェーンの末尾でRETURNするのか、FORWARDのポリシーがDROPなのかを見ます。

6. ルールの永続化方法を決める

手動で追加したルールは、ホスト再起動やDockerの構成変更後も残るとは限りません。OSや運用方式に合わせて、永続化の仕組みを用意します。

コマンドの構文だけを覚えるより、この順番でパケットの経路を整理するほうが、環境が変わっても応用しやすくなります。

コンテナ間通信を制限するICCの考え方

同じブリッジネットワーク上のコンテナは、デフォルトではコンテナ間通信が許可されています。これは開発環境では便利です。Web、PHP、データベース、キャッシュなどを同じネットワークへ配置し、サービス名で接続できるからです。

一方で、同じネットワークに参加しているコンテナが、必ずしもすべての相手と通信する必要があるとは限りません。侵害されたコンテナから、同一ネットワーク上のデータベースや管理用サービスへ横方向に接続されるリスクを考えると、ネットワークを分割する設計が有効になります。

Dockerでは、ブリッジネットワークのコンテナ間通信、つまりICCを制限する設定があります。環境によっては icc=falsecom.docker.network.bridge.enable_icc=false が使われ、iptablesのDROPやREJECTによって同一ブリッジ内の直接通信を制限します。

ただし、ICCを無効にするだけで理想的なアクセス制御が完成するわけではありません。アプリケーションが複数ネットワークへ参加している場合や、公開ポートを経由する通信、ホストを経由した通信などは別の経路になります。

個人開発のCompose環境であれば、まずネットワークを役割ごとに分ける設計が現実的です。

  • 外部公開が必要なリバースプロキシ用ネットワーク
  • WebアプリケーションとPHP実行環境のネットワーク
  • データベースやRedisなど、外部公開しない内部ネットワーク
  • バッチや管理ツール専用の一時的なネットワーク

たとえば、リバースプロキシとアプリケーションを外部接続可能なネットワークへ置き、データベースはアプリケーションとだけ共有する内部ネットワークへ置く、といった構成です。この場合、ネットワーク分割そのものが通信可能範囲を絞る境界になります。

ここで、サービス間の接続先をIPアドレスで指定してはいけない、という話と混同しないようにしてください。ネットワークを分割しても、同じネットワーク上ではサービス名による名前解決を利用できます。むしろ、名前解決を活かして接続先をサービス名にし、IPアドレスの変動をアプリケーションから隠すほうが、コンテナ運用には向いています。

通信制限はアプリケーションの依存関係を見てから行う

通信を制限するときは、最初にセキュリティルールを追加するのではなく、アプリケーションの依存関係を一覧にしてみましょう。

  • WebサーバーはPHP-FPMへ接続する必要があるか
  • PHPアプリケーションはデータベースの特定ポートだけを利用するか
  • キュー処理はRedisへ接続するか
  • マイグレーション用のコンテナはデータベースへ接続するか
  • 監視用コンテナは各サービスのヘルスチェック用ポートへ接続するか

この関係が分からないままDROPを追加すると、Web画面は表示されるのにキューだけ止まる、デプロイ時のマイグレーションだけ失敗する、といった分かりにくい障害になります。

私がネットワーク制限を設計するときは、まず「必要な通信を許可する」より先に、「どのサービスがどのサービスへ接続するのか」を図ではなく表で整理します。表にすると、想定していなかった直接接続や、不要な公開ポートが見つかりやすくなります。

通信元通信先ポート用途
リバースプロキシWebアプリケーションアプリケーション用ポートHTTPリクエストの転送
Webアプリケーションデータベースデータベース用ポートアプリケーションデータの読み書き
WebアプリケーションRedisRedis用ポートキャッシュやキュー
バッチコンテナデータベースデータベース用ポートバッチ処理とマイグレーション
外部クライアントリバースプロキシ公開ポートインターネットからの入口

この表にない通信を拒否する方針にすると、ルールの目的を後から説明しやすくなります。

Dockerネットワークを調査するときの実践的な見方

ネットワーク障害では、いきなりホストのiptablesを編集するより、まずDockerが認識している状態とLinuxカーネルが認識している状態を分けて確認します。

Docker側の情報を確認する

最初に、コンテナがどのネットワークへ参加しているかを確認します。

  • docker network ls でネットワーク名とドライバーを見る
  • docker network inspect ネットワーク名 でサブネット、ゲートウェイ、接続中のコンテナを確認する
  • docker inspect コンテナ名 でNetworkSettingsを確認する
  • docker exec コンテナ名 cat /etc/hosts でローカルな名前解決情報を見る
  • docker exec コンテナ名 cat /etc/resolv.conf でDNSの向き先を見る

docker network inspect の結果では、コンテナごとのIPアドレスや、ネットワークに付けられたエイリアスを確認できます。ここで、接続したつもりのネットワークにコンテナが存在しなければ、iptablesを調べても解決しません。

Composeでは、サービス名とコンテナ名を混同しないように注意してください。アプリケーションが接続先として利用すべきなのは、通常はComposeのサービス名です。container_name を固定すると一見分かりやすくなりますが、スケールやプロジェクト単位の分離を妨げることがあるため、必要性を考えて使うようにしましょう。

ホスト側のブリッジとインターフェースを見る

次に、ホスト上で ip linkip addr を実行し、docker0 が存在するか、アドレスが設定されているかを確認します。

ブリッジへ接続されたインターフェースの一覧は、環境によっては bridge link で確認できます。ここでvethがブリッジのポートとして表示されれば、コンテナ側とホスト側の仮想ケーブルが接続されています。

ただし、コンテナ内の eth0 とホスト側vethを名前だけで結び付けるのは難しい場合があります。必要に応じて、コンテナのNetwork Namespaceからインターフェース情報を確認し、ホスト側のインターフェースと対応付けます。調査の目的が単純な疎通確認であれば、まずはネットワークの所属、IPアドレス、ゲートウェイ、DNSを確認し、vethの詳細調査は必要な場合に絞っても構いません。

iptablesのポリシーとカウンターを確認する

iptablesのルールを見るときは、ルールの存在だけでなく、パケットカウンターも見てください。ルールが書かれていてもカウンターが増えていなければ、想定した経路をパケットが通っていない可能性があります。

確認したい観点は次のとおりです。

  • FORWARD のデフォルトポリシーが何になっているか
  • DOCKER-USER にDROPやREJECTがないか
  • DOCKER に公開ポート用のルールがあるか
  • natテーブルのPREROUTINGにDNATルールがあるか
  • natテーブルのPOSTROUTINGにMASQUERADEルールがあるか
  • 入出力インターフェースの条件が想定と一致しているか

Dockerがネットワークを作成した後に、別のファイアウォール管理ツールがFORWARDポリシーを変更しているケースもあります。Dockerだけを見ていると、作成されたルールは正しいのに通信が落ちているという状態を見逃してしまいます。

また、iptablesの表示形式や実装の裏側は、利用しているディストリビューションによって異なる場合があります。nftables互換レイヤーが使われている環境では、iptablesコマンドの結果だけでなく、ホストのファイアウォール管理方針も合わせて確認してください。ここは環境差が出やすい部分ですので、別のサーバーで同じコマンドを実行した結果が一致しないこと自体は不自然ではありません。

よくある失敗から見るブリッジネットワークの境界

Dockerのネットワーク設定で起きる失敗には、似たパターンがあります。原因を知っておくと、余計な変更を増やさずに済みます。

localhostで別コンテナへ接続しようとする

コンテナ内の localhost は、そのコンテナ自身を指します。ホストや別コンテナのサービスを指すわけではありません。

Webコンテナからデータベースへ接続する場合、接続先は localhost ではなく、同じユーザー定義ネットワーク上のデータベースサービス名にします。ホスト側で公開したデータベースポートを経由する構成も可能ですが、内部通信にホスト公開ポートを使うと経路が複雑になり、アクセス制御の意図も分かりにくくなります。

コンテナIPを設定ファイルへ固定する

コンテナは作り直すとIPアドレスが変わる可能性があります。固定IPが必要な特殊な要件を除けば、サービス名による名前解決を利用するほうが自然です。

IPアドレスを固定してしまうと、環境ごとに設定ファイルを変えたり、再構築のたびにアドレスを確認したりする必要が生まれます。これは個人開発で大切な再現性を損なう要因になります。

デフォルトブリッジで名前解決できると思い込む

コンテナ間通信とコンテナ名の名前解決は、同じ話ではありません。デフォルトの docker0 でIPレベルの通信ができても、コンテナ名をDNS名として解決できるとは限りません。

Composeで複数サービスを連携させる場合は、プロジェクト専用のユーザー定義ネットワークを使い、サービス名で接続する構成にしてみましょう。

INPUTにルールを追加すれば公開ポートを制御できると考える

前述のとおり、公開ポートからコンテナへ転送される通信はFORWARDの経路を通ります。INPUTだけにルールを追加しても、コンテナ宛ての転送通信を制御できないことがあります。

DOCKERチェーンを直接編集する

Dockerが管理するチェーンへ直接変更を加えると、デーモンやネットワークの状態変化によって消える可能性があります。独自ルールは DOCKER-USER を中心に設計し、Dockerの自動生成領域と分離して管理してください。

ネットワークを分ければすべて安全だと考える

ネットワーク分割は有効ですが、コンテナが複数ネットワークへ参加している場合、別の経路で通信できることがあります。また、ホストの公開ポートやアプリケーション内部の認証認可も別途必要です。

Dockerネットワークは境界のひとつであり、セキュリティ対策のすべてではありません。ネットワーク構成、公開ポート、アプリケーションの認証、ホストのファイアウォールを一つのプロセスとして設計することが大切です。

再現性のある構成にするための設計方針

Dockerブリッジネットワークを扱うとき、目指したいのは「その場で通信できる状態」ではなく、誰が環境を作り直しても同じ経路と依存関係を再現できる状態です。

まず、ネットワークの役割をComposeファイルへ明示します。アプリケーションとデータベースを同じネットワークへ置くのか、外部公開用のネットワークと内部ネットワークを分けるのかを、サービス定義から読み取れるようにしておきましょう。

次に、接続先はサービス名を使います。Docker内部DNSの 127.0.0.11 をアプリケーション側で直接意識する必要はありませんが、名前解決がDockerエンジンのネットワーク機能に依存していることは理解しておくと、障害時にログを読みやすくなります。

さらに、ホスト側のiptablesルールを手動変更する場合は、変更内容をスクリプトや構成管理の対象にします。ターミナルで一度だけ実行したルールは、再起動やサーバー交換で失われる可能性があります。運用手順に「実行したコマンド」を残すだけでなく、適用順序、前提条件、戻し方まで管理しておくと、トラブル時の復旧が安定します。

最後に、通信確認を自動化してみましょう。たとえば、デプロイ後に次のような確認を行います。

  • アプリケーションコンテナからデータベースのサービス名が解決できる
  • アプリケーションからデータベースの必要なポートへ接続できる
  • 外部公開ポートからリバースプロキシへ到達できる
  • データベースポートがホストの公開ポート経由では外部公開されない
  • コンテナから必要な外部APIやパッケージリポジトリへ接続できる

ここで、ブリッジネットワーク上のコンテナに対するホストからの到達性には、ひとつ注意しておきたい点があります。docker run -p などで明示的にポート公開をしていない場合、ホストの公開ポート経由ではコンテナ内部のリスニングポートへ到達できません。しかしこれは、コンテナがホストから完全に隔離されているという意味ではありません。ブリッジネットワークに参加するコンテナには 172.17.0.x172.18.0.x のようなIPアドレスが割り当てられ、ホスト側からも到達できる状態が作られます。つまり、ホスト上でコンテナのIPアドレスを把握できれば、そのIPアドレスとコンテナ内でリッスンしているポートを直接指定して、ホストからコンテナへ接続できてしまう場合があります。これはDockerがコンテナに対するホストからの通信をiptablesで一律に拒否する設定になっていないためで、デフォルトブリッジでもユーザー定義ブリッジでも同様に起こりえます。データベースのように機密性の高いサービスは、外部ネットワークに公開しないことに加えて、ネットワーク自体を分離する、ホスト側のファイアウォールで送信元を絞る、コンテナ側の認証を強化する、といった多層的な防御を組み合わせて考えるのが安全です。

こうした確認をヘルスチェックやCI/CDパイプラインへ組み込むと、ネットワーク設定の変更による影響を早い段階で発見できます。特に、サービス名の名前解決とTCP接続を別々に確認しておくと、DNS障害とファイアウォール障害を区別しやすくなります。

まとめ:パケットの通り道を理解すると設定が安定する

Dockerブリッジネットワークの仕組みは、docker0 という一つの仮想インターフェースだけで成立しているわけではありません。コンテナごとのNetwork Namespace、vethペア、仮想ブリッジ、LinuxのIP転送、iptablesのNATとFORWARD処理が連携しています。

コンテナ間通信では、同じブリッジ上のvethを通じてパケットが転送されます。外部通信では、net.ipv4.ip_forward=1 によるパケット転送と、MASQUERADEによる送信元アドレス変換が必要です。ポート公開ではDNATとFORWARDの経路が関係するため、ホストのINPUTチェーンだけを見ても十分ではありません。

また、ユーザー定義ブリッジではDocker内部DNSの 127.0.0.11 によって、サービス名を使った名前解決が利用できます。ネットワークを役割ごとに分け、サービス名で接続し、独自のフィルタリングは DOCKER-USER へ配置する。この組み合わせが、個人開発の環境を壊れにくくする基本になります。

次にネットワーク障害が起きたときは、設定を増やす前に、コンテナの eth0 からveth、ブリッジ、FORWARD、NAT、外部インターフェースまで、パケットがどこを通るはずなのかを一つずつ書き出してみてください。仕組みが見えれば、Docker Composeの修正が必要なのか、DNSの確認が必要なのか、あるいはホストのiptablesを調べるべきなのかを落ち着いて判断できるようになります。

よくある質問

Dockerコンテナ同士はどのように通信しますか?
同じブリッジネットワーク上のコンテナは、コンテナ側のeth0、vethペア、docker0を経由して通信します。宛先が同じサブネットにある場合、ARPでMACアドレスを解決して転送します。
Dockerコンテナから外部へ接続できない原因は何ですか?
Linuxホストのnet.ipv4.ip_forwardが無効だと、コンテナから外部インターフェースへパケットを転送できません。ほかにも、FORWARDチェーンの制御や、natテーブルのMASQUERADEルールが関係する場合があります。
Dockerの公開ポートはINPUTチェーンで制御できますか?
公開ポートからコンテナへ転送される通信は、ホスト自身が受信するローカル通信ではなく、別のネットワーク名前空間への転送通信です。そのため、主にFORWARDチェーンとDocker関連チェーンを確認します。
DockerのiptablesルールはDOCKERチェーンとDOCKER-USERチェーンのどちらに追加すべきですか?
利用者が独自のフィルタリングルールを追加する場合は、DOCKER-USERチェーンを中心に設計します。DOCKERチェーンはDockerが管理するため、直接編集したルールは構成変更などで消える可能性があります。
Docker Composeで別コンテナへ接続するときはlocalhostを使えますか?
コンテナ内のlocalhostは、そのコンテナ自身を指すため、別コンテナへの接続には使いません。ユーザー定義ネットワーク上では、通常、接続先のサービス名をホスト名として利用します。

参考情報