ブラウザーで開く Web サーバーと、その裏で動くアプリやデータベース。
Docker Compose では、この 3 つを別々のコンテナにして、名前で接続できます。
ホストの 80 番ポートを Web サーバーに公開すれば、外からもアクセスできます。
同じマシンにいるコンテナ同士は、どこを通って通信しているのでしょうか。 図の配線をたどりながら、コンテナ同士の通信と、外から入ってくる通信を見ていきます。
GOAL この章のゴール
- コンテナ同士の通信と、外部からの通信の経路を図で追える
- コンテナのネットワーク接続口がホストのどこにつながるか分かる
- コンテナ名で接続できる理由を説明できる
よくある構成: 80 番だけ公開する
Docker Compose で web、app、db の 3 つを動かす構成を例にします。
docker compose up を実行すると、Compose は <プロジェクト名>_default という名前のネットワークを 1 つ作り、全サービスをそこにつなぎます。
ports: ["80:80"] を書くのは web だけで、app は app:3000、db は db:3306 としてコンテナ同士の通信だけに使います。
この構成には 2 種類の通信があります。
外からホストの 80 番に入ってくる通信と、コンテナ同士が app:3000 のような名前で話す通信です。
前者では、ホストの Linux カーネルがパケットの宛先アドレスを web コンテナの IP に書き換えています (この書き換えを NAT と呼びます)。
後者では、ホスト内のソフトウェアスイッチ (ブリッジ) がコンテナ同士をつなぎ、Docker の DNS サーバーが名前を IP に変えています。
以下、ブリッジ、NAT、DNS の順に見ていきます。
docker0 ブリッジと veth
Docker をインストールすると、ホストに docker0 という Linux ブリッジができます。
物理スイッチのポートに NIC をケーブルでつなぐのと同じように、ここへ NIC をつなぐと、同じブリッジにつながった NIC 同士が直接パケットをやり取りできます。
Docker のドキュメントは bridge ネットワークを「同じブリッジにつながったコンテナ同士が通信でき、つながっていないコンテナからは隔離されるソフトウェアブリッジ」と説明しています。
ホストで ip link show type bridge を実行すると見えます。
既定では 172.17.0.0/16 が docker0 に、ユーザー定義のネットワークには 172.18.0.0/16 以降が順に割り当てられます。
コンテナを起動すると、Docker は vethブイイーサ仮想イーサネットのペア。片方に入ったパケットがもう片方から出てくる、ケーブル 1 本相当の仮想デバイス。用語集で見る を 1 組作ります。
veth は必ず 2 つ 1 組で作られ、片方で送信したパケットは即座にもう片方で受信されます (veth(4))。
Docker はその片方をコンテナの中に移して eth0 という名前にし、もう片方を docker0 に接続します。
コンテナの中だけで見える NIC とルーティング表の組は、コンテナ専用に用意されたものです (この仕組みの名前は次の章で付けます)。
コンテナの中で ip addr を打つと普通の NIC が 1 枚見えるだけですが、そのパケットはケーブルの反対側にあたる veth の片割れを通って、ホストの docker0 に出ていきます。
コンテナから外に出る通信は、docker0 からホストの eth0 に渡されます。
コンテナの IP (172.17.0.2 など) のまま外に出ても返事が戻ってこないので、カーネルは送信元アドレスをホストのアドレスに書き換えて送り出します。
Docker のドキュメントはこれを masquerading と呼び、「ホストの外側のネットワークにある機器からは Docker ホストの IP アドレスしか見えない」と説明しています。
-p 80:80 はその逆向きの書き換えで、ホストの 80 番宛てのパケットの宛先を 172.17.0.2:80 に変えます (DNAT)。
どちらの書き換えも、Docker が nat テーブルの DOCKER チェーンにルールとして登録したもので、実行しているのはカーネルのパケット処理機構 (netfilter) です。
ネットワークドライバー
docker run --network で選ぶネットワークの種類をドライバーと呼びます。
組み込みのドライバーは bridge、host、none、overlay、ipvlan、macvlan の 6 つで、1 台のホストでよく出会うのは次の 4 つです。
違いは、コンテナ専用のネットワーク設定に何を繋ぐかです。
- bridge:既定。上で見た docker0 かユーザー定義のブリッジに繋ぐ。同じブリッジのコンテナ同士は直接通信でき、外へは masquerading で出る。
- host:コンテナとホストの間のネットワークの隔離を無くし、ホストのネットワークをそのまま使う。ブリッジも NAT も無いが、ポートの衝突はホスト上のプロセスと同じように起きる。
- none:ホストからも他のコンテナからも完全に隔離する。NIC は
loだけ。 - macvlan:コンテナの仮想 NIC に独自の MAC アドレスを与え、物理ネットワークに直接つながった機器のように見せる。VM からの移行に向くが、Linux カーネルの制約で、macvlan につないだコンテナはホストと直接通信できない。
overlay は複数の Docker デーモンを Swarm でまたぐときのドライバーで、ipvlan は MAC アドレスの数に制限がある環境向けです。 サードパーティのネットワークプラグインを入れる口もあります。 Kubernetes では、この「ネットワークを差し替える口」が独立した規約になっていて、第 4 章で扱います。
コンテナ名で名前が引ける理由
Compose ファイルに db:3306 と書けるのは、dockerd が内蔵 DNS サーバーを持っているからです。
ユーザー定義ネットワーク (Compose が作るネットワークもそうです) に繋いだコンテナの /etc/resolv.conf には nameserver 127.0.0.11 が書かれていて、そのアドレスで dockerd が問い合わせに答えます。
コンテナ名以外の問い合わせは、ホストに設定された DNS サーバーへ転送されます。
既定の docker0 につないだコンテナはホストの /etc/resolv.conf をそのまま引き継ぎ、互いに IP でしか届きません (Docker のドキュメントは既定ブリッジを legacy と位置づけています)。
サービスを作り直すと、コンテナは同じ名前のまま別の IP でネットワークに参加し直します。 名前を引き直せば新しい IP が返るので、他のコンテナは名前で接続し続けられます。 この「名前で相手を見つける」しくみを サービスディスカバリサービスディスカバリサービスの所在 (IP とポート) を名前などの安定した識別子から動的に引ける仕組み。用語集で見る と呼びます。 内蔵 DNS はその簡易版で、Kubernetes では CoreDNS と Service がこの役割を担います。
ネットワークとボリュームは外部アタッチ
ここまでに見たとおり、docker0 も veth も、コンテナのプロセスとは別に Docker が作ってホスト側に置いたものです。
コンテナ本体は隔離されたプロセスとルートファイルシステムだけで、ネットワークもボリュームも、コンテナの外で作って起動時に差し込んでいます。
そのため docker network ls と docker volume ls は、コンテナとは別の寿命を持つ資源を一覧します。
コンテナを消してもネットワークとボリュームは残ります。
NIC を付けるかどうかも起動時に選べます (--network none なら付きません)。
この構造は Kubernetes の Pod でも同じで、Pod のネットワークは Pod の外で用意されます。
手を動かす: Pod の中から同じものを見る
ハンズオン環境の Web ターミナルは、Kubernetes 上の Pod の中で動いています。
中から見ると、Docker コンテナと同じく eth0 が 1 枚あるだけです。
ip addr show eth0
cat /etc/resolv.conf出力例
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1450 ...
inet 10.244.0.23/32 scope global eth0
nameserver 10.96.0.10
search handson-you.svc.cluster.local svc.cluster.local cluster.localnameserver の行には、Docker の 127.0.0.11 に相当する位置に 10.96.0.10 (CoreDNS の Service) が書かれています。
dockerd が名前を答えていた場所に、Kubernetes では CoreDNS が居ます。
IP が /32 なのは、このクラスタのネットワークプラグイン (Cilium) の流儀で、第 4 章で戻ってきます。
手元に Docker がある人は、同じことをコンテナ側とホスト側の両方から見ると腑に落ちます。
docker network create lab
docker run -d --name db --network lab alpine sleep 1d
docker run --rm --network lab alpine nslookup db
ip link show type bridge # ホスト側: br-<id> が増えている
ip link show type veth # ホスト側: veth の片割れ
ふりかえり
Q同じ bridge ネットワーク上のコンテナ同士が直接通信できるのはなぜ?
コンテナの eth0 は veth ペアの片割れで、もう片方が docker0 (またはユーザー定義ブリッジ) に刺さっています。同じブリッジにつながった NIC 同士なので、ルーターを介さず直接届きます。
QCompose で db:3306 と書いて繋がる理由として正しいものは?
ユーザー定義ネットワークのコンテナは resolv.conf に 127.0.0.11 を持ち、dockerd がコンテナ名を IP に解決します。既定の docker0 では名前解決が働かず、IP でしか届かない点も覚えておくと良いです。
Q--network host を選んだときに起きることは?
host モードではネットワークが隔離されません。ポートの衝突はホスト上のプロセスと同じように起きますが、ブリッジも NAT も無い分だけ速くなります。
参考
- Docker ドキュメント「Networking overview」(ドライバー一覧、既定のアドレスプール、内蔵 DNS 127.0.0.11) https://docs.docker.com/engine/network/
- Docker ドキュメント「Bridge network driver」(ソフトウェアブリッジ、masquerading、ユーザー定義ブリッジと既定ブリッジの違い) https://docs.docker.com/engine/network/drivers/bridge/
- Docker ドキュメント「Network drivers」 https://docs.docker.com/engine/network/drivers/
- Docker ドキュメント「Macvlan network driver」(ホストと直接通信できない制約) https://docs.docker.com/engine/network/drivers/macvlan/
- Docker ドキュメント「Docker with iptables」(nat テーブルの DOCKER チェーンで masquerading と port-mapping) https://docs.docker.com/engine/network/firewall-iptables/
- Docker ドキュメント「Packet filtering and firewalls」(firewall-backend に iptables / nftables) https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Docker Engine 29 release notes (nftables backend は experimental) https://docs.docker.com/engine/release-notes/29/
- Docker Compose ドキュメント「Networking in Compose」(
<project>_default、サービス名で到達、再作成時に IP が変わる) https://docs.docker.com/compose/how-tos/networking/ - veth(4) man page https://man7.org/linux/man-pages/man4/veth.4.html