目录

Docker-05 网络原理与排障:bridge、veth、NAT 和服务发现

1. Docker 网络的基本模型

默认 bridge 网络中,一个容器访问外部服务大致经过:

容器 eth0
   │ veth pair
docker0 bridge ── 宿主机 eth0 ── 网关/互联网
iptables/nftables NAT(源地址转换)

容器拥有自己的 network namespace,因此有独立的网卡、IP、路由表、端口空间和 netfilter 视图。veth pair 是一对虚拟以太网设备,一端位于容器 namespace,另一端接入宿主机 bridge。

数据包从容器发出时,bridge 在二层转发,宿主机路由和 netfilter 决定是否转发、是否做 SNAT;返回包依赖 conntrack 恢复连接状态。

2. 查看网络对象

docker network ls
docker network inspect bridge
docker inspect -f '{{json .NetworkSettings.Networks}}' web | jq
ip link show
ip addr show docker0
ip route
ss -lntp

创建一个自定义 bridge:

docker network create \
  --driver bridge \
  --subnet 172.30.10.0/24 \
  --gateway 172.30.10.1 \
  app-net
docker run -d --name api --network app-net example/api:dev

自定义 bridge 比默认 bridge 更适合应用,因为容器之间可以使用内置 DNS 通过服务名发现;默认 bridge 的历史行为和名字解析能力较弱。

3. Docker 的网络模式

3.1 bridge

默认的隔离网络。容器有独立 IP,通过 bridge 与宿主连接;对外访问通常做 SNAT,对内暴露服务使用 -p

docker run -d --name web --network bridge nginx:alpine

3.2 host

容器直接使用宿主机 network namespace,没有独立容器 IP,端口也不需要 -p。网络性能和可见性较好,但隔离性明显降低,端口冲突由宿主机直接决定。

docker run --network host nginx:alpine

3.3 none

只保留 loopback,适合完全不需要网络或需要手工配置网络的任务:

docker run --rm --network none alpine ip addr

3.4 container

两个容器共享同一个 network namespace,共享 IP、端口和 loopback,适用于 sidecar 类场景,但端口冲突也会共享:

docker run -d --name main example/api:dev
docker run --rm --network container:main alpine ss -lnt

3.5 overlay

跨主机网络通常需要 overlay 和控制面(例如 Swarm 或其他编排系统)。单机 Docker Compose 不会自动提供跨主机服务发现和高可用网络。

4. 端口映射和 EXPOSE

docker run -d --name web -p 127.0.0.1:8080:80 nginx:alpine
docker port web

-p [host_ip:]host_port:container_port 在宿主机发布端口。绑定到 127.0.0.1 只允许宿主机访问;省略地址通常会监听宿主机所有地址,可能意外暴露到公网。

EXPOSE 80 只是镜像元数据和文档提示,不能代替 -p。而 -P 会把所有 EXPOSE 端口随机映射到宿主机端口,适合临时测试,不适合稳定的服务入口。

5. Compose 中的 DNS 和服务发现

同一个 Compose 网络内,服务名会解析到服务对应的容器 IP:

Compose 项目中如何访问运行在宿主机上的服务(包括 host.docker.internal、Linux 的 host-gateway、监听地址和分层排障),见《Docker Compose:容器访问宿主机端口》

services:
  api:
    image: example/api:dev
  db:
    image: postgres:16

api 容器中应连接 db:5432,而不是 localhost:5432localhost 指的是当前容器的 network namespace;只有共享 network namespace 的容器才会共享 localhost。

容器重建后 IP 可能改变,但服务名保持不变。因此应用应该使用 DNS 名称而不是硬编码容器 IP。连接池还要处理 DNS 变化和数据库重启。

6. 容器之间的网络隔离

可以把前端和数据库放到不同网络:

services:
  proxy:
    image: nginx:alpine
    networks: [edge, app]
  api:
    image: example/api:dev
    networks: [app]
  db:
    image: postgres:16
    networks: [data]

networks:
  edge: {}
  app: {}
  data:
    internal: true

internal: true 主要用于限制网络的外部连通路径;真正的安全边界仍取决于宿主机防火墙、应用认证和编排平台策略。应用网络隔离的目标应是最小可达集合,而不是简单创建很多网络。

7. DNS、代理和 MTU

容器内 DNS 通常由 Docker 提供的 stub resolver 转发到宿主机配置。检查:

docker exec api cat /etc/resolv.conf
docker exec api getent hosts db
docker exec api getent hosts example.com

常见问题:

  • 宿主机能解析,容器不能解析:检查 Docker DNS、公司 VPN、代理和防火墙。
  • 能解析但 HTTPS 失败:可能是 CA 证书、代理或时间问题,不一定是网络不通。
  • 大包或长连接异常:检查 overlay/VPN 场景的 MTU、分片和中间设备。

不要把 /etc/resolv.conf 永久手工改在容器可写层;应通过 daemon、Compose dns 或宿主机网络配置解决。

8. 网络排障命令

8.1 从外到内

# 宿主端口是否监听
ss -lntp | rg ':8080'

# Docker 是否建立端口映射
docker port web
docker inspect web | jq '.[0].HostConfig.PortBindings'

# 容器内应用是否监听正确地址
docker exec web ss -lntp
docker exec web wget -qO- http://127.0.0.1:80

# 同网络容器之间能否访问
docker run --rm --network app-net curlimages/curl:8.10.1 \
  http://api:8080/healthz

应用只监听 127.0.0.1 时,其他容器无法通过容器 IP 访问;容器服务通常应监听 0.0.0.0,再用网络和防火墙控制可达范围。

8.2 进入 network namespace

PID=$(docker inspect -f '{{.State.Pid}}' api)
sudo nsenter -t "$PID" -n ip addr
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n ss -lntp

宿主机的路由表不等于容器内的路由表。排查容器网络时要同时看 namespace 内视图、宿主 bridge、路由和 netfilter 规则。

8.3 抓包

sudo tcpdump -ni any port 8080
sudo tcpdump -ni docker0 host 172.30.10.2

容器没有 tcpdump 时,在宿主机抓 veth/bridge 通常更可靠;先用 iflinkethtoolnsenter 找到对应接口。

9. iptables/nftables 和 Docker

Docker 会创建用于转发、端口发布、隔离和 NAT 的规则。不同发行版可能通过 iptables-nft 兼容层展示规则;不要默认 iptables 和 nftables 是两套互不相关的规则。

sudo iptables -t nat -S DOCKER
sudo iptables -S DOCKER-USER
sudo nft list ruleset

自定义防火墙规则要注意顺序和 owner:Docker、firewalld、kube-proxy、CNI 可能同时管理网络。通常把主机侧额外访问控制放在 Docker 提供的 DOCKER-USER 链或发行版支持的防火墙入口,并记录规则归属,避免重启后被覆盖。

10. 安全建议

  • 不要把数据库端口无条件发布到 0.0.0.0
  • Compose 内部服务只加入需要的网络,不要全部使用 network_mode: host
  • 关闭不必要的容器出网,使用代理或 egress 防火墙控制供应链访问。
  • 不要依赖容器 IP 做认证;使用 TLS、服务身份和应用级鉴权。
  • 网络连通不代表应用健康;健康检查要验证真正的依赖和业务状态。

11. 亲手追踪一对 veth

启动实验容器:

docker network create --subnet 172.30.20.0/24 network-lab
docker run -d --name network-lab-web \
  --network network-lab \
  -p 127.0.0.1:18080:80 \
  nginx:1.27-alpine

PID=$(docker inspect -f '{{.State.Pid}}' network-lab-web)
docker inspect network-lab-web | jq '.[0].NetworkSettings.Networks'

从容器 namespace 看 eth0

sudo nsenter -t "$PID" -n ip -d link show eth0
sudo nsenter -t "$PID" -n ip addr show eth0
sudo nsenter -t "$PID" -n ip route
sudo nsenter -t "$PID" -n cat /etc/resolv.conf

记录容器 eth0iflink,它通常对应宿主侧 veth 的 ifindex:

PEER_INDEX=$(sudo nsenter -t "$PID" -n cat /sys/class/net/eth0/iflink)
ip -o link | rg "^${PEER_INDEX}:"
bridge link

现在可以同时抓宿主入口、bridge 和容器侧:

sudo tcpdump -ni any 'tcp port 18080 or tcp port 80'
curl -v http://127.0.0.1:18080

观察 TCP SYN 在宿主端口发布规则后被送往容器的 80 端口。不同 Engine/内核可能通过 nftables、iptables 兼容层或 userland proxy 的不同组合实现,排障时以当前 docker info、进程、规则集和抓包为准。

11.1 手工断网与恢复

docker network disconnect network-lab network-lab-web
docker inspect network-lab-web | jq '.[0].NetworkSettings.Networks'
curl -v --max-time 2 http://127.0.0.1:18080

docker network connect network-lab network-lab-web
curl -fsS http://127.0.0.1:18080 >/dev/null

断开网络后容器进程仍可能显示 running,这说明进程状态、网络可达和业务健康是三个不同维度。

12. 端口发布的详细数据路径

客户端访问 HOST_IP:8080 时,可以按下面的控制点理解:

客户端 SYN
   ↓ 宿主网卡/loopback
宿主 netfilter PREROUTING/OUTPUT
   ↓ DNAT 或等价转发机制
容器地址:80
   ↓ veth → 容器 network namespace
应用监听 socket
返回包经 conntrack 反向转换

四类常见失败对应不同证据:

失败点典型现象关键证据
宿主未发布端口docker port 无结果inspect PortBindings
防火墙/安全组丢包客户端超时两端抓包、counter、安全组日志
应用监听错误端口或只监听 loopback到容器 IP 被拒绝/超时容器 namespace 内 ss -lntp
应用协议/TLS 错误TCP 建连但 HTTP/TLS 失败curl -v、应用日志、证书检查

ports: ["8080:80"] 通常绑定所有宿主地址;管理面、数据库和本地开发入口更安全的写法是 127.0.0.1:8080:80,公网再经受控代理/防火墙暴露。

13. conntrack、MTU 与连接池的生产问题

13.1 conntrack 表耗尽

Docker NAT 连接通常被 conntrack 跟踪。高并发短连接、DNS 风暴或攻击可能让表接近上限:

sysctl net.netfilter.nf_conntrack_count
sysctl net.netfilter.nf_conntrack_max
sudo conntrack -S 2>/dev/null || true

症状可能是随机超时、丢包,而不是所有容器同时完全断网。先减少不必要短连接、修正连接池与超时,再基于内存和流量评估表上限;只调大参数会把根因推迟。

13.2 MTU 黑洞

VPN、VXLAN/overlay 和云隧道会增加报文头。底层 MTU 没留空间时,小包/Ping 正常,大 HTTP 响应或 TLS 握手可能卡住:

ip link show
docker network inspect network-lab | jq '.[0].Options'
ping -M do -s 1400 TARGET_IP
tracepath TARGET_IP

不要用“能 ping 通”证明业务网络正常。抓包检查重传和 ICMP fragmentation-needed,统一规划宿主、Docker network 和隧道 MTU。

13.3 DNS 与长连接

服务容器重建后 IP 会改变。应用如果只在启动时解析一次 DNS 并永久缓存旧 IP,或连接池不检测断链,就可能持续请求旧实例。生产客户端应有合理 DNS 策略、连接健康检查、超时、重试和退避;重试必须考虑请求幂等。

13.4 清理实验

docker rm -f network-lab-web
docker network rm network-lab

14. 本篇小结

Docker 网络不是一张“虚拟网卡”这么简单,而是 network namespace、veth、bridge、路由、netfilter、NAT 和 DNS 的组合。端口问题按“宿主监听 → 映射规则 → 容器监听 → 应用响应”排查,服务发现优先使用 Compose 服务名,跨主机则交给真正的编排网络。