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)の仕組みを語れる
  • portsexpose の違いを即答できる
  • フロント / 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)
  • portsexpose の違い、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      local

5つの主要ドライバ

ドライバ用途概念
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 docker0

docker0 とは何か

docker0Linux カーネルの仮想ブリッジ(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(同一ネットワーク内)NGNG
expose: 8080OK(同一ネットワーク内)NGNG
ports: 8080:8080OKOKOK(ホストの 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.11db サービスの現在の 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:80OKports: 80:80 で公開
インターネット → api:8080NGports 書いてない
インターネット → db:3306NGports 書いてない
web → apiOK両者とも frontend に所属
web → dbNG別ネットワーク(重要)
api → dbOK両者とも 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 answer

DNS で解決できない = ネットワーク的にも届かない

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
  1. 上記の3層構成(web / api / db)を作成し、docker compose up -d で起動
  2. ホストから curl http://localhost/ で web 経由でアクセス確認
  3. docker compose exec web sh して nslookup db を実行 → 解決できない ことを確認
  4. docker compose exec api sh して nslookup db を実行 → 解決できることを確認
  5. db サービスに ports: "3306:3306" を一時的に追加 → ホストから mysql -h 127.0.0.1 -uapp -papppass app で繋がる(本番では絶対やらない)
  6. db の所属を backend に戻し、internal: true を追加 → web からの再確認
  7. 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)を頭の中で描ける
  • portsexpose の違いを説明できる
  • 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

項目portsexpose
効果ホストから到達可能コンテナ間のみ(ドキュメント)
書き方ports: ["8080:80"]expose: [80]
本番DBで使うNGOK(必要なら)
iptables 編集するしない

「実務OK」基準

  • 「繋がらない」と言われた時、ネットワーク図を頭に描いて切り分けできる
  • 本番の公開ポートを最小限にできる: DB に ports を書かないのが当たり前
  • 3層モデルを compose で表現できる: 設計を YAML に落とせる
  • localhost の罠で詰まらない: コンテナ内 / ホストの違いを瞬時に判断

自己評価チェックリスト

知識レベル

  • bridge / host / overlay の使い分けを語れる
  • portsexpose の違いを即答できる
  • コンテナ内 localhost が「自分自身」を指すことを忘れない
  • iptables / NAT がどこで動いているか説明できる

実行レベル

  • フロント / API / DB の3層分離 compose を1から書けた
  • internal: true で外部遮断ネットワークを作った
  • nslookup でサービス名解決を確認した

メタ認知

  • 自分のプロジェクトで「不要に公開しているポート」が無いか棚卸しした
  • 「localhost の罠」をチームで共有する説明を準備した

さらに深掘るなら


次のレッスン

2-4_環境変数とシークレット.md: 環境変数とシークレット管理、12 Factor App の Config 原則、.env の作法、Docker Secrets と本番でのシークレットマネージャ連携へ進む。