コンテナはお互いをどうやって見つけるのか
一言でいうと
ネットワークネームスペースは、インターフェースとルーティングテーブルだけでなく、ループバックアドレスとポート番号の空間までまるごと複製します。そのため、コンテナの127.0.0.1は、ホストの127.0.0.1とはまったく別のアドレスです。
なぜ必要なのか
初心者が踏む3つの症状は、すべてこの1つの文で説明できます。
- ブラウザーで接続が拒否されます
- 隣のコンテナから
curl http://api:3000を実行しても、名前が見つかりません - コンテナから
curl http://127.0.0.1:5432を実行しても、ホストのDBに接続できません
3つ目が特に紛らわしいです。コンテナの中から127.0.0.1に接続すると、そのコンテナ自身を指します。ホストではありません。
どう動くのか
デフォルトのブリッジネットワークでは、コンテナ名でお互いを見つけられません。
docker run -d --name db postgres:16
docker run --rm alpine:3.20 ping -c1 db
# ping: bad address 'db'
ユーザー定義ネットワークを作って接続すれば解決します。違いは、コンテナ内の/etc/resolv.confにそのまま表れます。ネームサーバーが、コンテナのネームスペース内にだけ存在するアドレスに設定され、そのリクエストがランタイムのリゾルバーに転送されます。リゾルバーは、同じネットワークにいるコンテナの名前とエイリアスを知っているため、答えてくれます。デフォルトのブリッジは、後方互換のために残っているもので、実務では常にユーザー定義ネットワークを作って使います。
2つ目に重要なのは、アプリケーションのバインドアドレスです。
docker exec api ss -tlnp
# LISTEN 0 511 127.0.0.1:3000 ...
このソケットには、ホストから入ってきたパケットが決して届きません。パケットはeth0のアドレスに届くからです。正解は0.0.0.0:3000です。「0.0.0.0は危険ではないのか」という反論がありますが、コンテナ内での0.0.0.0は、そのコンテナのネットワークネームスペース全体を意味するだけで、外部に公開されるかどうかは、パブリッシュのバインドアドレスが決めます。2つの階層を混ぜると、両方とも間違います。
現場での姿
ポートのパブリッシュのデフォルトは、ホストのすべてのインターフェースです。
docker run -d -p 5432:5432 postgres:16 # 위험
docker run -d -p 127.0.0.1:5432:5432 postgres:16 # 안전
最初の行は、サーバーがパブリックIPを持っている場合、インターネットにデータベースを開放したのと同じです。そして、ホストのファイアウォールで防いだからと安心してはいけません。コンテナに向かうパケットは、ホストが最終的な宛先ではないため、通常のINPUTルールを通らず、それより前の段階ですでに宛先が書き換えられます。最も安全でシンプルな解決策は、そもそもバインドアドレスを制限することです。
コンテナ同士でだけ通信するサービスは、そもそもパブリッシュしないのが正しいです。そして、コンテナを複数のネットワークに同時に接続できる点も、覚えておいてください。フロントエンドのネットワークとバックエンドのネットワークを分け、アプリだけを両方に所属させれば、DBは外部からまったく見えなくなります。
ネットワークの種類と使う場面
bridge (기본) 가상 스위치. 컨테이너끼리 사설 IP 로 통신
host 호스트 네트워크를 그대로 씀. 격리 없음
none 네트워크 없음. 계산만 하는 작업
container:<이름> 다른 컨테이너의 네임스페이스 공유
デフォルトのbridgeとユーザー定義のbridgeは違います。デフォルトのネットワークでは、名前でお互いを見つけられず、IPを知っている必要があります。ユーザーが作ったネットワークには、内蔵のDNSが付き、コンテナ名がそのままホスト名になります。
docker network create app-net
docker run -d --name db --network app-net postgres:16
docker run --rm --network app-net alpine ping -c1 db # 이름으로 닿는다
そのため、実務では常にネットワークを1つ作って使います。composeは、これを自動で行ってくれます。
ポートマッピングがすることとしないこと
-p 8080:80は、ホストの8080をコンテナの80につなぐNATルールです。コンテナ同士は、このマッピングとは無関係に、コンテナのポートに直接通信します。
호스트에서 → localhost:8080
다른 컨테이너에서 → web:80 ← 8080 이 아니다
これを取り違えて、コンテナの中でlocalhost:8080を呼び出すミスがよくあります。コンテナのlocalhostは自分自身です。ほかのコンテナを呼び出すには、名前を使います。
-p 127.0.0.1:8080:80のようにアドレスを付けると、外部に公開されません。開発用のデータベースを開くとき、この習慣が事故を防ぎます。-p 5432:5432は、ファイアウォールがなければ、インターネットにDBを開放することです。
ホストからコンテナの中を覗く
コンテナにssやtcpdumpがなくても、ホストからそのネームスペースに入れます。
PID=$(docker inspect -f '{{.State.Pid}}' myapp)
nsenter -t $PID -n ss -ltnp # 컨테이너의 리스닝 포트
nsenter -t $PID -n tcpdump -i any -c 20 port 80
-nは、ネットワークネームスペースにだけ入るという意味なので、ホストのツールをそのまま使いながら、コンテナのネットワークを見ます。distrolessイメージを診断する最も確実な方法です。
次のラボですること
ユーザー定義ネットワークを作って名前でお互いを見つけられるようにし、別のネットワークのコンテナが本当に見えないかを確認したうえで、ループバックにだけポートをパブリッシュしてみます。