前言
容器一多,问题就从"跑起来"变成"怎么连":容器之间怎么互访?端口映射的原理是什么?为什么生产推荐自定义网络?这篇把 Docker 网络讲透。
一、五种网络模式总览
docker network ls
# NETWORK ID NAME DRIVER
# xxx bridge bridge # 默认网桥 docker0
# xxx host host # 共享宿主机网络栈
# xxx none null # 无网络
| 模式 | 命令 | 特点 | 场景 |
|---|---|---|---|
| bridge(默认) | --network bridge |
独立网络栈 + NAT | 绝大多数场景 |
| host | --network host |
直接用宿主机网络,无隔离 | 追求网络性能 |
| container | --network container:xx |
复用另一容器的网络栈 | K8s Pod 的雏形 |
| none | --network none |
只有 lo,完全隔离 | 安全敏感/离线任务 |
| 自定义 bridge | docker network create |
支持 DNS、隔离性好 | 生产推荐 |
二、bridge:默认模式与端口映射原理
docker run -d --name web -p 8080:80 nginx
内部发生了什么:
宿主机 容器 web
┌────────────────────┐ ┌──────────────┐
│ eth0: 10.0.4.13 │ │ eth0: │
│ │ │ │ 172.17.0.2 │
│ docker0: 172.17.0.1│◄──────►│ (veth pair) │
│ │ │ │ gw=172.17.0.1│
│ iptables DNAT │ └──────────────┘
│ :8080 → 172.17.0.2:80
└────────────────────┘
- 安装 Docker 时创建了虚拟网桥
docker0(172.17.0.0/16) - 每个容器拿到一张虚拟网卡(veth pair 一端在容器、一端挂网桥)
-p 8080:80本质是一条 iptables DNAT 规则:
iptables -t nat -L DOCKER -n
# DNAT tcp dpt:8080 to:172.17.0.2:80
- 容器出网靠 SNAT/MASQUERADE,源地址被改写成宿主机 IP
# 在容器里验证
docker exec web ip addr # eth0 172.17.0.2/16
docker exec web ip route # default via 172.17.0.1
IP 不固定——容器重建后 IP 会变,这就是要靠自定义网络 DNS 的原因。
三、host 模式:零开销,全暴露
docker run -d --network host nginx:alpine
- 容器直接用宿主机的 eth0/lo,没有 NAT、没有端口映射(
-p会被忽略) - 性能 = 裸机进程(高吞吐网络应用收益明显)
- 代价:端口冲突风险——宿主机 80 被占,容器 nginx 起不来
- 排障口子:
ss -tlnp看到宿主机端口,实际是容器在听
# 典型用法:监控 agent、需要高性能转发的组件
docker run -d --network host --name node-exporter prom/node-exporter
四、container 模式与 none
# container 模式: 共享另一个容器的网络命名空间(IP/端口/hosts 全共享)
docker run -d --name app myapp
docker run -d --name sidecar --network container:app busybox sleep 3600
# sidecar 里 localhost:8080 就是 app 的 8080 —— K8s Pod 内多容器互通的原理
# none 模式: 只有 lo
docker run --rm --network none busybox ip addr
# 1: lo ... (没有 eth0)
五、自定义网络:生产标配
默认 docker0 网络的痛点:容器间只能靠 IP 互访(没有 DNS),且不同网络完全隔离。
# 创建
docker network create --driver bridge --subnet 172.20.0.0/16 mynet
# 挂到网络上跑
docker run -d --name db --network mynet -e MYSQL_ROOT_PASSWORD=pass mysql:8.0
docker run -d --name app --network mynet myapp
自定义网络自带 DNS:
docker exec app ping -c2 db # 直接用容器名!
docker exec app getent hosts db # 172.20.0.2
# 应用配置里写 db:3306 即可, 不用管 IP
多网络隔离:
docker network create frontend
docker network create backend
docker run -d --name web --network frontend nginx
docker run -d --name api --network frontend --network backend myapi
docker run -d --name db --network backend mysql:8.0
# web ↔ api 通, api ↔ db 通, web ↔ db 不通(按需连通)
docker network inspect mynet # 网络详情: 容器列表/IP/网关
docker network connect mynet web # 给运行中的容器再接一个网络
docker network rm mynet # 删除(需先断开所有容器)
六、容器与外界通信全景
容器 → 外网 ✔ SNAT 改写源地址(bridge 模式开箱即用)
外网 → 容器 ✔ -p 端口映射(DNAT)
容器 → 宿主机 ✔ 网关地址: docker0 的 IP(172.17.0.1)或 host.docker.internal
宿主机 → 容器 ✔ 直接访问容器 IP(bridge 模式同宿主机可直连 172.17.0.2)
容器 ↔ 容器 ✔ 自定义网络 + DNS 容器名互访
一个容易被忽略的点——容器访问宿主机上的服务:
# 宿主机服务必须监听 0.0.0.0 或 docker0 IP, 只听 127.0.0.1 则容器连不上
docker exec app curl http://172.17.0.1:9090/metrics
七、常见坑速查
| 现象 | 原因 |
|---|---|
| 容器间用容器名连不上 | 在默认 docker0 网络上(无 DNS),换自定义网络 |
-p 映射后外部不通 |
安全组/防火墙;或宿主机端口被占 |
| 容器重启后 IP 变了 | 正常现象,永远用 DNS 名不用 IP |
| host 模式容器起不来 | 端口与宿主机冲突 |
| 172.17 网段与公司内网冲突 | --subnet 自定义网段,或改 daemon.json 的 bip |
| 删不掉网络 | 还有容器连着,docker network disconnect |
小结
| 需求 | 方案 |
|---|---|
| 日常单容器对外服务 | bridge + -p |
| 多容器互通 | 自定义网络 + 容器名 DNS |
| 网络性能敏感 | host 模式(注意端口冲突) |
| 按需隔离分组 | 多网络 + 按容器挂载 |
| 理解底层 | veth pair + docker0 + iptables NAT |
本文是「云原生」系列第 5 篇。