2-3. Docker ネットワーク - bridge, ports, サービス分離
所要時間: 40-50分 ゴール: Docker ネットワークの仕組みを語れ、フロント / API / DB を適切なネットワークに分離して設計できる コミット内容:
~/learn/infra/day10/の3層構成 compose.yml と動作確認ログ
このレッスンのゴール
- ネットワークドライバ (bridge / host / overlay / macvlan / none) の使い分けを言える
- bridge ネットワーク内部(docker0、NAT、iptables)の仕組みを語れる
-
portsとexposeの違いを即答できる - フロント / API / DB を別ネットワークに分離する3層構成を書ける
- コンテナ内 localhost とホスト localhost の違いで詰まらない
なぜ学ぶか(実務悩みベース)
- 「コンテナ間で繋がらない」「ホストから見えない」が一生のテーマになる
- DB ポートを本番でうっかり公開して情報漏洩する事故を避ける
- マイクロサービス化したときのネットワーク分離設計の基礎
- Kubernetes の Service / NetworkPolicy への前段階知識
前章とのつながり
2-2 でサービス名 DNS による接続を見た。今回はその裏で動く ブリッジネットワーク・NAT・iptables の仕組みと、複数ネットワークによる分離設計に踏み込む。
大前提: ネットワークが分からないと「繋がらない」が一生解決しない
Compose を触り始めて多くの人が遭遇するのが「コンテナ間で繋がらない」「ホストから見えない」「localhost が違うものを指している」の3大トラブル。
- 「
mysql -h localhostが動かない!」(コンテナ内の localhost は自分自身) - 「
ports: 3306:3306書いたのに外から繋がらない!」(ファイアウォール、bind アドレス) - 「
depends_onだけ書いたのに DNS が引けない!」(別ネットワークだとそもそも見えない)
これらは全部 Docker のネットワーク設計を理解すれば一発で見通せる。今日は ネットワークドライバ・bridge の仕組み・DNS・分離設計 の4本柱を押さえる。
本日の到達点
- ドライバ(bridge / host / overlay / macvlan / none)の使い分け
- bridge ネットワークの内部実装(docker0, NAT, iptables)
portsとexposeの違い、0.0.0.0バインドの危険性- フロント / API / DB を別ネットワークに分けるパターン
- localhost の意味がコンテナとホストで違うことを完全に理解する
セッション①: ドライバと bridge の仕組み(25-30分)
1. 5つのネットワークドライバ
docker network ls
# NETWORK ID NAME DRIVER SCOPE
# abc... bridge bridge local
# def... host host local
# ghi... none null local5つの主要ドライバ
ドライバ 用途 概念 bridgeデフォルト。単一ホスト内のコンテナ間通信 仮想スイッチ + NAT hostホストのネットワークスタックをそのまま使う コンテナの分離を捨てる overlay複数ホスト跨ぎの通信(Swarm/Kubernetes 系) VXLAN でホスト間トンネル macvlanコンテナに物理 NIC の MAC を割り当てる LAN 上に直接コンテナを並べる noneネットワーク無効 バッチ処理など外部通信不要 普段使うのは
bridge(99%) とhost(パフォーマンス重視のとき)。overlayは K8s 時代に CNI(Calico, Cilium 等)に取って代わられた。
それぞれのユースケース
- bridge: 通常の Web アプリ。デフォルトでこれ。Compose のデフォルトネットワークも bridge
- host: 「コンテナの分離より低レイテンシ優先」のとき。L4 LB、パケット解析、性能テスト用ベンチマーク
- overlay: Docker Swarm 環境(今は減少傾向)。K8s では別の仕組み(CNI)
- macvlan: 監視・侵入検知ツールがLAN上の機器として見える必要があるとき。IoT 系
- none: 完全オフライン処理。
docker run --network none ...
2. デフォルトの bridge ネットワーク(docker0)
# Docker をインストールすると自動で作られる
ip addr show docker0
# docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
# inet 172.17.0.1/16 scope global docker0docker0 とは何か
docker0は Linux カーネルの仮想ブリッジ(virtual switch)。Docker デーモンが起動時に作る。役割:
- すべてのコンテナはこの仮想スイッチに繋がる(仮想 NIC
vethペアで)- コンテナ間通信は docker0 を経由
- ホスト外への通信は NAT(iptables MASQUERADE) でホスト IP に書き換え
[container1]---veth---+ | [container2]---veth---+--[docker0 (172.17.0.1)]---[NAT iptables]---[eth0]---[INTERNET] | [container3]---veth---+macOS / Windows では事情が違う: コンテナは Linux VM の中で動くため、docker0 は VM 内に存在。ホスト OS の
ifconfigには出てこない。
デフォルト bridge とユーザー定義 bridge の違い
docker network lsで見えるbridge(デフォルト)と、docker network create my-netで作るカスタム bridge は 別物。違いは:
項目 デフォルト bridge カスタム bridge 内蔵 DNS なし あり(コンテナ名で名前解決) コンテナ間アクセス デフォルトで全許可 デフォルトで全許可(同一ネットワーク内) 隔離 弱い 強い(別カスタムネットワークとは別物) 重要: コンテナ名で DNS 解決したいなら 必ずカスタムネットワーク を使う。Compose は自動でカスタムネットワークを作ってくれる(プロジェクト名_default)。
3. ports と expose の違い
services:
api:
image: myapi:1.0
expose:
- "8080" # コンテナ間ではアクセス可能(内側だけ)
ports:
- "8080:8080" # ホストにも公開(外から見える)expose と ports の違い
設定 コンテナ間アクセス ホストからアクセス 外部ネットワークからアクセス 何も書かない OK(同一ネットワーク内) NG NG expose: 8080OK(同一ネットワーク内) NG NG ports: 8080:8080OK OK OK(ホストの 8080 が公開されてれば) expose はドキュメント目的。Dockerfile の
EXPOSEと同じで、「このコンテナは内部で 8080 使ってますよ」と宣言するだけ。実際の通信ルールには影響しない。ports は実際にホストにポートを公開する。
-pフラグの YAML 表現。重要な原則: 「コンテナ間でしか使わないサービスは ports を書かない」。
- DB は外部に公開しない →
ports書かない- API は外部公開する →
ports: 8080:8080
ports の正確な記法
ports: - "8080:80" # ホストの 8080 → コンテナの 80 - "127.0.0.1:8080:80" # ホストの localhost にだけバインド(外部から見えない) - "8080-8090:80-90" # 範囲指定 - "8080:80/udp" # UDP 指定 - target: 80 # 構造体形式 published: 8080 protocol: tcp mode: host「ホスト側 : コンテナ側」の順。ホスト側を省略すると
0.0.0.0(全インターフェース)にランダムポート割当。
アンチパターン: 本番で全部 0.0.0.0 バインド
services: db: image: postgres:16 ports: - "5432:5432" # NG: 0.0.0.0:5432 = インターネットから見えるなぜNGか:
- クラウド VM のセキュリティグループ設定漏れで PostgreSQL が世界に晒される
- SSH 経由の踏み台アクセスを想定していたのに、直接攻撃される
- 実際に AWS の RDS じゃない自前 DB がインターネット公開されて漏えいする事故は毎年起きる
正解1: そもそも公開しない
services: db: image: postgres:16 # ports は書かない(コンテナ間通信のみ)正解2: ローカルバインドのみ
services: db: image: postgres:16 ports: - "127.0.0.1:5432:5432" # ホストの localhost にだけSSH トンネル経由でしかアクセスできない(運用ツールから繋ぐ時の典型パターン)。
4. localhost の意味がコンテナとホストで違う
# ホスト OS で
curl http://localhost:8080/ # ホストのポート 8080(コンテナを ports で公開してれば届く)
# コンテナの中で
docker compose exec api sh
curl http://localhost:8080/ # コンテナ自身の 8080。隣の DB コンテナには届かない
curl http://db:3306/ # サービス名 'db' でコンテナ間通信localhost と 127.0.0.1 はネットワーク名前空間ごとに別物
Linux ではネットワーク名前空間(network namespace)という機構があり、コンテナごとに独立した「ループバック」を持つ。
- ホストの localhost (127.0.0.1) : ホストの自分自身
- コンテナ A の localhost : コンテナ A の自分自身(他のコンテナとは別)
- コンテナ B の localhost : コンテナ B の自分自身
全部 IP は同じ
127.0.0.1だが、属しているネットワーク名前空間が違うので 別物。よくある事故: アプリの設定で
DATABASE_HOST=localhostと書いて、コンテナ内で起動 → コンテナ自身の 3306 を見にいって「接続拒否」エラー。サービス名(db)を使うのが正解。
サービス名で名前解決される仕組み
Compose が作る bridge ネットワークには Docker の内蔵 DNS サーバー(
127.0.0.11:53)が刺さっている。# コンテナ内で cat /etc/resolv.conf # nameserver 127.0.0.11 # options ndots:0アプリが
dbを解決しようとすると、127.0.0.11がdbサービスの現在の IP を返す。コンテナが再起動して IP が変わっても、DNS が新しい IP を返すので アプリは IP を気にしなくていい。これは Kubernetes の Service と同じ思想。「IP ではなく名前で繋ぐ」が現代のお作法。
5. host ネットワーク(諸刃の剣)
services:
api:
image: myapi:1.0
network_mode: "host" # コンテナの分離を捨てる
# ports は書けない(書いても無視される)host モードはいつ使う?
コンテナの ネットワーク名前空間をホストと共有 する。コンテナの 8080 = ホストの 8080。NAT が無くなるので速い。
使う場面
- ベンチマークでネットワークオーバーヘッドを排除
- 大量のポートを公開する必要があるサービス(IRC ボット、SIP サーバー等)
- パケットキャプチャ・侵入検知(プロモードで NIC を見る必要)
デメリット
- コンテナの分離が弱まる(ホストの全 NIC に直アクセス可能)
- ポート衝突: ホストで 8080 使ってたら起動不可
- Mac/Windows では Linux VM 内のネットワークになるので、期待した挙動にならない
アンチパターン: 何でもかんでも host
services: web: image: nginx network_mode: "host" api: image: myapi network_mode: "host" db: image: postgres network_mode: "host"なぜNGか:
- ポートが衝突しやすい(5432 使う DB が複数あったら?)
- コンテナ間通信が ホストの localhost 経由になる。サービス名で引けない
- DB がホストの 5432 に直に晒される(前述のとおり危険)
- 「Docker 使ってる意味」がほぼ無くなる
host は最後の手段。
bridgeで困らない限り選ぶな。
セッション②: ネットワーク分離設計(25-30分)
6. なぜネットワーク分離が必要か
[インターネット]
|
| 公開すべきは 443/80 だけ
|
[Web (nginx)]
|
| API への内部通信
|
[API (Go)]
|
| DB への内部通信
|
[DB (MySQL)]
3層モデルとネットワーク分離の意義
アプリケーションを論理的に Web / API / DB の3層に分ける時、通信できる範囲もそれに合わせて絞る のが defense in depth(多層防御)の基本。
- Web 層: インターネットからアクセスされる。最も危険なゾーン
- API 層: Web からのみアクセスされる
- DB 層: API からのみアクセスされる
仮に Web が侵害されても、Web から DB に直接届かなければ被害が限定される。これは AWS の VPC + Security Group、Kubernetes の NetworkPolicy と同じ思想。
Compose ではこれを「複数の bridge ネットワーク + 所属サービスの絞り込み」で表現する。
7. 3層構成の compose.yml
services:
web:
image: nginx:1.27-alpine
ports:
- "80:80" # 外部に公開
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- frontend
depends_on:
- api
api:
build: ./api
networks:
- frontend # web から見られる必要
- backend # db に届く必要
environment:
DATABASE_URL: "app:apppass@tcp(db:3306)/app"
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: rootpass
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD: apppass
volumes:
- db_data:/var/lib/mysql
networks:
- backend # api からしか見えない
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-prootpass"]
interval: 5s
retries: 10
start_period: 20s
networks:
frontend:
backend:
volumes:
db_data:nginx.conf:
server {
listen 80;
server_name _;
location / {
proxy_pass http://api:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location = /healthz {
access_log off;
return 200 "ok\n";
}
}このネットワーク分離が実現すること
接続元 → 接続先 通る? 理由 インターネット → web:80 OK ports: 80:80で公開インターネット → api:8080 NG ports書いてないインターネット → db:3306 NG ports書いてないweb → api OK 両者とも frontend に所属 web → db NG 別ネットワーク(重要) api → db OK 両者とも backend に所属 「web が乗っ取られても db には届かない」 を Compose 側で担保している。
実務で起きる「web → db ショートカット」事故
開発者がアプリの設定をデバッグしたくて「とりあえず Web から DB が見えるように」と全サービスを
defaultネットワークに入れてしまう。その状態で本番デプロイ → 何ヶ月後、Web の脆弱性(古い nginx、SSRF)から DB を直接叩かれる。
設計時の分離を守る: 開発中も本番と同じネットワーク分離を維持する。
8. 内部 DNS の名前解決の挙動
docker compose up -d
# api コンテナの中から
docker compose exec api sh
nslookup db # backend にいるので解決できる
nslookup web # frontend にもいるので解決できる
# web コンテナの中から
docker compose exec web sh
nslookup api # frontend にいるので解決できる
nslookup db # NG: 別ネットワーク
# *** Can't find db: No answerDNS で解決できない = ネットワーク的にも届かない
Docker の内蔵 DNS は「同じネットワークに所属しているコンテナの名前」しか返さない。これがネットワーク分離の実装の肝。
アンチパターン: アプリのコードで IP 直書き
// NG: ハードコード db, _ := sql.Open("mysql", "app:pass@tcp(172.18.0.2:3306)/app")なぜNGか:
- コンテナ IP は再起動で変わる
- 別環境(ステージング、本番)では違う IP
- ネットワーク分離の意図を破壊
正解: サービス名で繋ぐ。
db, _ := sql.Open("mysql", "app:pass@tcp(db:3306)/app")
9. internal ネットワーク(外部から完全遮断)
networks:
backend:
internal: true # このネットワークから外部に出られない
internal: trueの効果このネットワークに所属するコンテナから インターネットへの egress(出口)通信ができなくなる。
用途:
- DB 層を完全に隔離(DB から外部 API は叩かないはず)
- 漏えい時の影響を最小化(マルウェアが C2 サーバーに通信できない)
注意:
- DNS 解決自体はできる(同一ネットワーク内のサービス名)
- もし「DB が外部にバックアップ送信したい」みたいな要件があるなら使えない
Kubernetes の
NetworkPolicy egress: []と同じ思想。
10. ネットワーク図を描く習慣
┌───────────────────┐
│ インターネット │
└─────────┬──────────┘
│ 80, 443
┌─────────▼──────────┐
frontend │ web │
───────── │ (nginx) │
└─────────┬──────────┘
│ 8080
┌─────────▼──────────┐
frontend + │ api │
backend │ (Go service) │
└─────────┬──────────┘
│ 3306
┌─────────▼──────────┐
backend │ db │
(internal) │ (MySQL) │
└────────────────────┘
アーキ図を README に貼る
ネットワーク分離は YAML を読むだけだと気づきにくい設計判断。
README.mdにアーキ図(ASCII でいい)を貼って、「なぜこの分離なのか」をコメントで残すと、後から来た人が分離を破らない。
練習課題
mkdir -p ~/learn/infra/day10
cd ~/learn/infra/day10
script ~/log/infra_day10.log- 上記の3層構成(web / api / db)を作成し、
docker compose up -dで起動 - ホストから
curl http://localhost/で web 経由でアクセス確認 docker compose exec web shしてnslookup dbを実行 → 解決できない ことを確認docker compose exec api shしてnslookup dbを実行 → 解決できることを確認dbサービスにports: "3306:3306"を一時的に追加 → ホストからmysql -h 127.0.0.1 -uapp -papppass appで繋がる(本番では絶対やらない)dbの所属をbackendに戻し、internal: trueを追加 → web からの再確認exitで script 終了
考察課題
- Web 開発者が「ローカルで DB に GUI ツールで繋ぎたい」と言ってきた。本番の compose を壊さず、開発時だけポートを公開するにはどうする?(ヒント: override ファイル)
- 3層を全部同じ
defaultネットワークに入れたとき、attacker が web を侵害してから db に届くまでに何ステップ?分離した場合は?
締め: git で証跡を残す
cd ~/learn/infra/day10
git init -q 2>/dev/null || true
git add compose.yml api/ nginx.conf
git commit -m "feat(infra): 3層ネットワーク分離構成(web / api / db)"チェックリスト
- 5つのドライバ(bridge / host / overlay / macvlan / none)を用途で分けられる
- bridge の内部(docker0, NAT, iptables)を頭の中で描ける
-
portsとexposeの違いを説明できる -
0.0.0.0バインドの危険性を理解した - コンテナ内の
localhostがホストのlocalhostと違う理由を語れる - 3層構成でネットワークを分離できる
- サービス名でコンテナ間通信できる仕組みを説明できる
-
internal: trueの効果を説明できる
詰まった時のチートシート
| やりたいこと | コマンド/書き方 |
|---|---|
| ネットワーク一覧 | docker network ls |
| ネットワーク詳細 | docker network inspect <名前> |
| カスタム作成 | docker network create my-net |
| ホスト公開(全インターフェース) | ports: "8080:80" |
| ホスト公開(localhost のみ) | ports: "127.0.0.1:8080:80" |
| 公開しない(コンテナ間のみ) | ports を書かない |
| サービス名で接続 | tcp(db:3306) のように書く |
| 別ネットワークに所属 | networks: [frontend, backend] |
| 外部遮断 | networks: backend: { internal: true } |
| コンテナ IP 確認 | docker compose exec <svc> ip addr |
| 名前解決確認 | docker compose exec <svc> nslookup <name> |
やらかし事例: ネットワーク絡みの定番惨事
事例1: DB の
ports: 3306:3306を本番にそのままデプロイインターネットから直接 MySQL に届く状態に。bot に発見されて1時間で侵入される。本番では DB に
portsを書かない。
事例2:
0.0.0.0:3306:3306で世界公開「localhost だけにしたつもり」が
0.0.0.0バインドで全インターフェース公開。127.0.0.1:3306:3306のようにホスト側を localhost に絞る。
事例3: コンテナ内で
mysql -h localhostコンテナ内の localhost は そのコンテナ自身 を指す。サービス名
mysqlを使うのが正解。
事例4: 同じ Compose プロジェクトじゃないコンテナと繋ぐ
別 Compose の DB に繋ごうとして DNS が引けない。
external: trueの共有ネットワークを作って両方に attach する。
事例5: ufw / iptables との競合
ホストの ufw で「3306 ブロック」したのに、Docker は iptables を直接書き換えるので素通り。
DOCKER-USERチェーンで明示的にブロック。
物語型対比: bridge vs host vs overlay
シーン1: 「とりあえず動く」 = bridge
開発で docker run すると勝手に bridge ネットワークに繋がる。これは Docker が作った 仮想スイッチ (docker0) にコンテナがぶら下がる構成。
[コンテナA] [コンテナB]
|veth0 |veth1
└────┬─────┘
docker0 (192.168.x.x)
|
[iptables NAT]
|
eth0 (ホストの物理NIC)
メリット: 隔離されて安全、サービス名で DNS が引ける デメリット: NAT 経由で少しだけ遅い、ホスト側からコンテナ IP を直接見られない
シーン2: 「最速で速度欲しい」 = host
ホストのネットワークスタックを そのまま使う。NAT 無し、隔離無し。
[コンテナ] = [ホスト] のネットワークと同一視
メリット: 最速、性能ベンチマーク向き デメリット: ポート衝突しまくる、隔離が無いのでセキュリティ最悪、Mac/Windows では使えない(Linux 限定)
シーン3: 「複数ホストを跨いで通信」 = overlay
Swarm や Kubernetes で使う。異なる物理ホスト上のコンテナ同士 を VXLAN で繋ぐ。
[ホスト1のコンテナA] ─ VXLAN ─ [ホスト2のコンテナB]
メリット: マルチホストオーケストレーションの前提 デメリット: 設定複雑、Swarm/K8s 前提
使い分けの直感
| 場面 | ドライバ |
|---|---|
| 通常の開発・本番 | bridge |
| ベンチマーク・ホスト一体運用 | host (Linux のみ) |
| 複数ホスト間連携 | overlay |
| 完全隔離(外部通信なし) | none |
| 物理 LAN にコンテナを直接配置 | macvlan |
対比表: ports vs expose
| 項目 | ports | expose |
|---|---|---|
| 効果 | ホストから到達可能 | コンテナ間のみ(ドキュメント) |
| 書き方 | ports: ["8080:80"] | expose: [80] |
| 本番DBで使う | NG | OK(必要なら) |
| iptables 編集 | する | しない |
「実務OK」基準
- 「繋がらない」と言われた時、ネットワーク図を頭に描いて切り分けできる
- 本番の公開ポートを最小限にできる: DB に
portsを書かないのが当たり前 - 3層モデルを compose で表現できる: 設計を YAML に落とせる
- localhost の罠で詰まらない: コンテナ内 / ホストの違いを瞬時に判断
自己評価チェックリスト
知識レベル
- bridge / host / overlay の使い分けを語れる
-
portsとexposeの違いを即答できる - コンテナ内 localhost が「自分自身」を指すことを忘れない
- iptables / NAT がどこで動いているか説明できる
実行レベル
- フロント / API / DB の3層分離 compose を1から書けた
-
internal: trueで外部遮断ネットワークを作った -
nslookupでサービス名解決を確認した
メタ認知
- 自分のプロジェクトで「不要に公開しているポート」が無いか棚卸しした
- 「localhost の罠」をチームで共有する説明を準備した
さらに深掘るなら
- Docker docs: Networking overview - 公式の網羅的な解説
- Docker docs: Networking with overlay - Swarm のオーバーレイ
- Kubernetes NetworkPolicy - K8s でのネットワーク分離
- 書籍: 「マスタリング TCP/IP - 入門編」- TCP/IP の基礎は Docker ネットワーク理解の前提
- 書籍: 「Container Security」(Liz Rice)- コンテナのセキュリティ全般、ネットワーク章が秀逸
- 記事: Iptables and Docker - iptables ベースの仕組み
次のレッスン
2-4_環境変数とシークレット.md: 環境変数とシークレット管理、12 Factor App の Config 原則、.env の作法、Docker Secrets と本番でのシークレットマネージャ連携へ進む。