Docker コンテナに対するポートフォワーディングはプロジェクト外部からの接続に対してのみ行われる
言いたいことはタイトルの通りである。 Docker にはポートフォワーディング機能があり、これを使うとコンテナのポートをホストのポートに割り当てることができる。
この目的から考えれば、ポートフォワーディングがプロジェクト内部のコンテナからの接続に対して働かないのは当たり前なことのように思えるが、本当にそうであることを確かめたかったので実験してみた。
結論だけ知りたい場合は、実験部分を飛ばして最後の「ねたばらし」の部分だけ読んでほしい。
もしかすると本稿の内容には間違いがあるかもしれないので、その場合はご指摘いただきたい。
実験の手順
適当な場所に test
ディレクトリを作り、その中で以下のような docker-compose.yml
を作成する。
version: "3.9" services: nginx: image: nginx ports: - 8080:80 networks: - mynetwork - default client1: image: curlimages/curl command: "sh -c 'sleep 10 && curl nginx:80 && curl nginx:8088'" networks: - mynetwork depends_on: - nginx client2: image: curlimages/curl command: "sh -c 'sleep 20 && curl nginx:80 && curl nginx:8088'" networks: - default depends_on: - nginx networks: mynetwork:
この状態で docker-compose up
すると、以下のコンテナが作成される。
- nginx
- 80 番ポートで接続を待ち受ける。これはホストの 8080 番ポートにバインドされている。
- client1, 2
- それぞれ異なるネットワークから nginx コンテナに
curl
して通信がつながるか確認する。
- それぞれ異なるネットワークから nginx コンテナに
さらに、以下のネットワークが作成される。
- test_default という名前の bridge ネットワーク(暗黙的に作られる)
- test_mynetwork という名前の bridge ネットワーク
これら全体の構成を図にすると、以下のようになる。
この構成のもとで host、client1、client2 からそれぞれ nginx コンテナの 80 番と 8080 番ポートに curl
して、接続が通じるか調べる。
実験の結果
host から curl
した結果を以下に示す。80 番ポートへの接続は失敗し、8080 番ポートへの接続は成功している。
$ curl localhost:80 curl: (7) Failed to connect to localhost port 80: Connection refused $ curl localhost:8080 <!DOCTYPE html> <html> (省略) </html>
次に、client1 の curl
実行ログを以下に示す。
前半部分は nginx:80
に対する接続で、成功している。後半は nginx:8080
に対する接続で、失敗している。
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 <!DOCTYPE html> <html> (省略) </html> 100 615 100 615 0 0 597k 0 --:--:-- --:--:-- --:--:-- 600k % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 curl: (7) Failed to connect to nginx port 8080 after 0 ms: Connection refused
最後に、client2 の curl
実行ログを以下に示す。
client1 のときと同じような結果となった。
% Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 <!DOCTYPE html> <html> (省略) </html> 100 615 100 615 0 0 599k 0 --:--:-- --:--:-- --:--:-- 600k % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0 curl: (7) Failed to connect to nginx port 8080 after 0 ms: Connection refused
実験の考察
まず、host からコンテナの接続について考える。
そもそも host 自体は 80 番ポートを LISTEN していないので、ここへの接続が失敗するのは自明である。 また、8080 番ポートへ接続すると、以下のようにポートフォワーディングが行われ、接続に成功する。
次に、コンテナからコンテナへの接続について考える。
各コンテナから nginx:80
には以下のようにアクセスできるが......
どのようなネットワーク構成であっても Docker プロジェクト内からの接続にはポートフォワーディングが効かないため、8080→80 の変換が行われず、結果として nginx:8080
への接続は以下のように失敗する。
ねたばらし
では、どうしてポートフォワーディングは外部からの接続のみに対して効くのだろうか?
Docker のドキュメント を読めば分かるが、Docker サーバが起動するとホストの iptables
にマスカレーディングルールが追加される。今回の実験の構成を起動したときの iptables
を実際に確認してみると、以下のようになっていることがわかる。
$ sudo iptables-legacy -t nat -L -n (省略) Chain DOCKER (2 references) target prot opt source destination (省略) DNAT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:8080 to:172.27.0.2:80
iptables
の DNAT ルールは Destination Network Address Translation、つまりネットワークパケットの宛先アドレスを書き換える機能を持つ。
上記の例では、dpt(destination port:宛先ポート)が 8080 な TCP パケットの行き先を、172.27.0.2
(nginx コンテナのアドレス)の 80 番ポートに書き換えている。
このルールによってポートフォワーディングが実現され、コンテナの特定のポートをホストの特定のポートに紐付けることが可能となる。
この他にも通信をうまく取り扱うためのルールが複数追加されるので、気になる方はぜひ確かめてみてほしい。
一方、コンテナ間の通信にはこのルールが適用されないため、結果としてポートフォワーディングも行われないのである。
結論
Docker コンテナに対するポートフォワーディングはプロジェクト外部からの接続に対してのみ行われる。
この実験を通して、普段あまり意識しないような Docker の内部の処理を垣間見ることができ、とても興味深いわねえという気持ちになった。