Docker 基础体系 · 第 8/70 篇。示例以现代 Docker Engine、BuildKit 与 Compose 规范为基础,并明确 Linux 容器边界。

Docker 网络完整指南:Bridge、端口映射、DNS、Overlay 和排障

Docker 网络的核心不是“给容器分配一个 IP”,而是把多个 Linux 网络组件组合起来:网络命名空间隔离进程的网络视图,虚拟以太网设备连接命名空间与宿主机,Linux bridge 提供二层转发,路由与 NAT 负责跨网段通信,Docker 内置 DNS 负责名称解析,端口发布则把宿主机地址映射到容器地址。

本文以现代 Docker Engine、BuildKit 和 Compose 规范为背景,重点讨论 Linux 容器。Docker Desktop 中的容器实际运行在 Linux VM 内,宿主机看到的网络路径可能多一层虚拟机转发;Rootless Docker 也可能使用 slirp4netns、pasta 等用户态网络实现,因此部分底层表现与 rootful Linux Engine 不同。


一、先建立网络模型:容器到底连接在哪里

1. 网络命名空间提供隔离

Linux 网络命名空间(network namespace,简称 netns)为进程提供独立的:

  • 网络接口;
  • IP 地址;
  • 路由表;
  • ARP/邻居表;
  • 防火墙规则视图;
  • 端口监听空间。

容器中的进程通常位于自己的网络命名空间内。因此,容器中监听 0.0.0.0:80,表示监听该容器命名空间内所有接口的 80 端口,而不是直接监听宿主机的 80 端口。

这解释了一个重要事实:

容器端口和宿主机端口是两个不同网络命名空间中的端口,除非使用 host 网络模式,否则它们不会天然相同。

2. veth pair 把容器接入宿主机

普通 Bridge 网络通常通过一对虚拟以太网设备(veth pair)连接容器与宿主机:

容器 netns                         宿主机 netns
+----------------+                 +----------------------+
| eth0           |<==== veth pair =>| vethXXXX             |
| 172.20.0.2/16  |                 |                      |
| default route |                 | docker bridge        |
+----------------+                 | 172.20.0.1/16        |
                                    +----------+-----------+
                                               |
                                               | 路由 / NAT / 防火墙
                                               |
                                         外部网络或宿主机

veth pair 的两端相连:一端放入容器命名空间,通常命名为 eth0;另一端留在宿主机,并接入某个 Linux bridge,例如 docker0 或用户自定义 bridge。

在容器内部执行:

ip addr
ip route

常见结果类似:

2: eth0@if123: <BROADCAST,MULTICAST,UP,LOWER_UP>
    inet 172.20.0.2/16 scope global eth0

default via 172.20.0.1 dev eth0
172.20.0.0/16 dev eth0 proto kernel scope link src 172.20.0.2

这里:

  • 172.20.0.2 是容器在该网络中的地址;
  • 172.20.0.1 通常是 bridge 在宿主机上的网关地址;
  • eth0@if123 中的 if123 是宿主机侧 veth 的接口索引提示;
  • 默认路由把非本地网段的流量交给 bridge 网关。

3. Bridge 是什么

Linux bridge 可以理解为一个虚拟二层交换机。它根据 MAC 地址学习转发表,并在接入端口之间转发以太网帧。

Docker 的 bridge 网络驱动通常完成以下工作:

  1. 创建或使用 Linux bridge;
  2. 为网络分配子网;
  3. 为容器创建 veth pair;
  4. 将宿主机侧 veth 接入 bridge;
  5. 将另一端放入容器命名空间;
  6. 设置容器 IP、默认路由和 DNS 配置;
  7. 配置跨网络或出站流量所需的路由与 NAT;
  8. 根据端口发布规则安装转发规则。

可以用命令观察网络对象:

docker network ls
docker network inspect bridge

用户自定义网络的输出中通常可以看到:

  • Driver: bridge
  • SubnetGateway
  • 已连接容器的名称、IPv4 和 IPv6 地址;
  • 端点配置。

docker network inspect 展示的是 Docker 的网络模型;它不一定直接显示所有内核级转发细节。要观察宿主机接口,还可以执行:

ip link show
ip addr show docker0
bridge link
bridge fdb show

不同发行版和 Docker 配置可能使用 iptables、nftables 或兼容层实现防火墙与 NAT。不要假设一定存在某一套规则格式。


二、Bridge 网络:默认网络和用户自定义网络不是一回事

1. 默认 bridge 网络

如果启动容器时没有指定 --network,传统 Docker Engine 通常会把容器接入名为 bridge 的默认网络:

docker run -d --name web nginx

查看容器地址:

docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web

默认 bridge 支持容器通过 IP 通信,但不应把它当作现代应用编排网络使用。一个关键差异是:

默认 bridge 网络不提供像用户自定义网络那样可靠、自然的容器名称服务发现;不要依赖默认网络中的容器名解析。

在默认网络上,历史上的 --link 可以提供有限的名称和环境变量注入能力,但它是旧机制,不适合作为新系统的服务发现方案。

2. 用户自定义 bridge 网络

创建用户自定义 bridge:

docker network create \
  --driver bridge \
  --subnet 172.30.0.0/16 \
  app-net

启动两个容器:

docker run -d --name api --network app-net nginx
docker run --rm --network app-net busybox \
  wget -qO- http://api

第二条命令能够使用 api 作为主机名,是因为用户自定义网络启用了 Docker 的内置 DNS 和基于容器名称的服务发现。

用户自定义 bridge 的价值不仅是名称解析,还包括:

  • 可以按应用划分网络;
  • 可以在运行时连接和断开容器;
  • 网络隔离边界更清晰;
  • 可以配置子网、网关、IP 范围和部分驱动选项;
  • 不同应用的容器不会默认处于同一个二层广播域。

容器也可以同时连接多个网络:

docker network create frontend
docker network create backend

docker run -d --name api --network backend nginx
docker network connect frontend api

此时 api 拥有两个网络端点。它可以从两个网络收发流量,但这不等于宿主机上的两个 bridge 被直接桥接。Docker 仍然通过容器内的网络接口、路由和网络端点管理隔离边界。

3. 网络隔离的实际含义

假设有:

frontend: 172.31.0.0/16
backend:  172.32.0.0/16

web 连接 frontend
api 连接 frontend、backend
db  连接 backend

那么:

  • web 可以访问 api
  • api 可以访问 db
  • web 默认不能直接解析或访问 db,因为它们不在同一 Docker 网络;
  • api 作为多宿主容器,成为两个网络之间的应用层参与者,但 Docker 不会因为它连接两个网络,就自动把两个网络变成完全互通的二层网络。

这是一种常见的分层方式:前端服务只连接 frontend,数据库只连接 backend,API 同时连接两者。


三、端口映射:EXPOSE-p-P 完全不同

1. 容器监听不等于宿主机可访问

启动一个监听 80 端口的 Nginx:

docker run -d --name web nginx

此时 Nginx 监听的是容器网络命名空间内的 80 端口。宿主机执行:

curl http://127.0.0.1:80

通常不会访问到该容器,因为宿主机的 80 端口没有被发布。

容器之间可以通过容器 IP 或用户自定义网络中的服务名访问:

docker run --rm --network container:web curlimages/curl \
  http://127.0.0.1:80

这里使用 --network container:web,测试容器与 web 共享同一个网络命名空间,所以 127.0.0.1 指向同一网络栈。若改为普通 --network bridge127.0.0.1 就只指向测试容器自己。

2. EXPOSE 只是镜像元数据

Dockerfile 中的:

EXPOSE 8080

不会创建防火墙规则,也不会自动把宿主机端口映射到容器。它主要表达:

该镜像中的应用预计监听容器端口 8080。

可以用以下命令查看镜像声明:

docker image inspect IMAGE \
  --format '{{json .Config.ExposedPorts}}'

真正发布端口需要在运行时指定:

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

这条规则的含义是:

宿主机 127.0.0.1:8080  ->  容器 80

访问方式:

curl http://127.0.0.1:8080

但宿主机其他网卡地址上的 8080 不会因为这条规则自动开放。

如果写成:

docker run -d --name web -p 8080:80 nginx

通常表示绑定宿主机所有适用地址的 8080 端口,包括外部网络可达的地址。是否真正能够从外部访问,还取决于宿主机路由、安全组、云防火墙和主机防火墙。

因此生产环境中,-p 8080:80-p 127.0.0.1:8080:80 的暴露边界完全不同。

3. 端口映射的数据路径

以客户端访问宿主机 192.0.2.10:8080 为例,目标容器地址为 172.30.0.2:80

客户端
  |
  | TCP 192.0.2.20:53000 -> 192.0.2.10:8080
  v
宿主机接收网卡
  |
  | DNAT / 端口发布规则
  | 目标变为 172.30.0.2:80
  v
docker bridge
  |
  v
容器 eth0:80

返回包必须沿着连接跟踪状态返回。NAT 不是单向替换字符串,而是维护连接状态,使返回流量对双方仍表现为原始连接。

端口映射成立通常需要同时满足:

  1. 容器内应用确实监听目标端口;
  2. 应用监听的地址包含容器端点,例如 0.0.0.0 或容器实际 IP;
  3. 容器网络端点存在;
  4. 宿主机发布规则存在;
  5. 宿主机能够路由到容器网络;
  6. 主机防火墙、安全组和云网络没有阻断;
  7. 客户端访问的是正确的宿主机地址和端口。

只要其中一个条件不成立,就可能出现“容器运行正常但端口访问失败”。

4. 常见监听地址错误

应用只监听容器内的回环地址:

127.0.0.1:8080

即使发布了:

-p 8080:8080

Docker 也不能把外部连接转发到该应用,因为转发后的目标地址通常是容器 IP,而应用没有监听容器 IP。

容器中检查监听状态:

ss -lntp

期望看到类似:

LISTEN 0 128 0.0.0.0:8080 ...

如果看到:

LISTEN 0 128 127.0.0.1:8080 ...

应根据应用配置改为监听 0.0.0.0 或明确的容器接口地址。不要简单地把监听地址改成宿主机地址;容器通常并不知道宿主机的业务 IP。

5. Compose 中的端口写法

Compose 服务中的:

services:
  web:
    image: nginx
    ports:
      - "127.0.0.1:8080:80"

等价于发布宿主机回环地址的 8080 到容器 80。

而:

services:
  web:
    image: nginx
    expose:
      - "80"

只表达容器间使用的内部端口,不发布到宿主机。实际上,同一 Docker 网络中的容器可以访问服务监听的端口,即使没有写 exposeexpose 更接近文档和元数据声明,而不是访问控制规则。

Compose 服务间访问通常写:

http://web:80

而不是:

http://localhost:8080

因为 localhostapi 容器内指向 api 自己。只有从宿主机访问时,才使用宿主机发布端口,例如 http://127.0.0.1:8080


四、Docker DNS:容器名、服务名和外部域名

1. 容器内的 /etc/resolv.conf

容器通常拥有自己的 /etc/resolv.conf

docker run --rm alpine cat /etc/resolv.conf

在用户自定义网络中,常见结果包含 Docker 内置 DNS 地址:

nameserver 127.0.0.11
options ndots:0

127.0.0.11 是容器网络命名空间内可访问的 Docker Embedded DNS。这里的 127.0.0.11 不是宿主机的 DNS 地址;它只在该容器的网络命名空间内有意义。

应用查询域名时,大致有两类路径:

查询容器名或服务名
  -> Docker 内置 DNS
  -> 返回同一 Docker 网络中的端点地址

查询公共域名
  -> Docker 内置 DNS
  -> 按宿主机或 Docker 配置转发给上游 DNS
  -> 返回外部解析结果

可以区分测试:

docker run --rm --network app-net busybox nslookup api
docker run --rm --network app-net busybox nslookup example.com

第一条验证 Docker 网络内服务发现,第二条验证外部 DNS 转发。

2. 名称解析的作用域

容器名称解析不是全局 DNS。名称是否可解析取决于查询方和目标是否共享相应网络。

例如:

docker network create net-a
docker network create net-b

docker run -d --name api --network net-a nginx
docker run --rm --network net-a busybox nslookup api
docker run --rm --network net-b busybox nslookup api

net-a 中通常能解析 api;在 net-b 中通常不能。若把容器连接到两个网络,它在两个网络中可能拥有各自的网络别名和可解析性。

3. Compose 的服务名解析

Compose 默认会为项目创建网络,常见名称类似:

项目名_default

在同一 Compose 网络中,服务名通常可以作为 DNS 名称:

services:
  api:
    image: my-api
  db:
    image: postgres

api 应使用:

postgresql://db:5432/app

而不是写死 db 的容器 IP。因为容器重建、扩缩容或网络重新创建后,IP 可能变化,但服务发现名称保持稳定。

Compose 中的 container_name 不应作为常规服务发现基础。它会限制某些扩缩容场景,而且把应用身份绑定到固定容器名。优先使用服务名和网络别名。

可以显式配置别名:

services:
  api:
    image: my-api
    networks:
      app:
        aliases:
          - user-api

networks:
  app:

此时同一网络内可以通过 apiuser-api 访问该服务。

4. 多副本下的 DNS 行为

服务有多个端点时,解析结果可能包含多个地址,或者解析到一个虚拟 IP(VIP),具体取决于编排器和服务发现模式。应用必须把 DNS 结果视为可变化的:

  • 不应永久缓存服务 IP;
  • 不应假设一次解析只返回一个地址;
  • 连接失败后应具备重试和重新解析能力;
  • 长连接应处理后端实例被替换的情况。

在 Docker Swarm 中,服务可以使用 VIP 或 DNS round-robin(DNSRR)模式。VIP 模式由服务虚拟 IP 接收流量,再转发到任务;DNSRR 模式返回任务地址,由客户端自行选择。DNSRR 并不自动提供 HTTP 负载均衡,也不适合所有客户端。

5. DNS 失败和 TCP 失败要分开

以下错误属于不同层次:

lookup api: no such host

表示名称解析失败,可能是网络不一致、名称错误或内置 DNS 异常。

connection refused

表示已经到达目标地址,但目标端口没有接受连接,常见原因是应用未监听或监听地址错误。

i/o timeout

表示连接建立过程没有及时完成,可能是路由、防火墙、MTU、网络策略或目标服务阻塞。

排障时先执行:

getent hosts api

再执行:

nc -vz api 8080

最后才检查应用协议:

curl -v http://api:8080/health

这样可以把 DNS、TCP 和 HTTP 层的问题分离。


五、Overlay 网络:跨宿主机连接容器

1. Overlay 的使用前提

Bridge 网络主要连接同一台宿主机上的容器。Overlay 网络用于跨 Docker 节点连接容器,通常与 Docker Swarm 服务一起使用。

Overlay 的基本前提是:

  1. 节点加入同一个 Swarm;
  2. 节点之间的控制面和数据面端口可达;
  3. 使用兼容的 Docker Engine 网络能力;
  4. 网络被创建为 overlay;
  5. 需要让独立容器接入时,使用可附加(attachable)的 overlay 网络。

创建 Swarm:

docker swarm init --advertise-addr 192.0.2.10

创建可附加 overlay:

docker network create \
  --driver overlay \
  --attachable \
  app-overlay

在 Swarm 中创建服务:

docker service create \
  --name web \
  --network app-overlay \
  nginx

--attachable 允许普通 docker run 容器连接该 overlay;如果只供 Swarm 服务使用,则不一定需要它。

2. Overlay 的两层结构

Overlay 同时涉及控制面和数据面。

控制面负责:

  • 节点成员关系;
  • 网络对象和端点信息;
  • 服务任务的发现;
  • 跨节点网络状态同步。

数据面负责:

  • 把一个节点上容器发出的报文送到另一个节点;
  • 对原始容器网络报文进行封装;
  • 在目标节点解封装并交给目标容器。

常见实现使用 VXLAN 等封装机制。原始包会被包在宿主机之间传输的外层 UDP 包中:

容器 A
  原始包:172.40.0.3 -> 172.40.0.8
      |
      v
节点 1:封装为宿主机间数据包
      |
      | 物理网络 / 云网络
      v
节点 2:解封装
      |
      v
容器 B

因此,Overlay 的 MTU 通常小于物理网卡的有效 MTU。外层封装增加了头部开销。如果路径不支持分片或 PMTU 发现异常,可能出现“小包正常、大包超时”的故障。

3. Swarm 常见端口

节点之间通常需要根据 Docker Swarm 配置放通:

  • 2377/TCP:Swarm 管理通信;
  • 7946/TCP7946/UDP:节点发现和网络成员通信;
  • 4789/UDP:Overlay VXLAN 数据面。

这些端口必须在节点间可达,尤其是在云安全组、主机防火墙和跨子网路由存在时。不要只测试节点之间的 SSH 或 ping;Overlay 数据面使用的端口和协议可能不同。

验证基础连通性:

nc -vz NODE_IP 2377
nc -vz NODE_IP 7946

UDP 不能用普通 TCP 的 nc -vz 充分验证,应结合防火墙规则、抓包和 Swarm 状态检查。命令选项还会因 nc 实现不同而变化。

4. Overlay 与端口发布不是一回事

服务接入 overlay 后,节点之间的服务通信可以使用服务名,但外部客户端访问服务仍需要端口发布:

docker service create \
  --name web \
  --network app-overlay \
  --publish published=8080,target=80 \
  nginx

Swarm 的发布模式还涉及 ingress routing mesh。默认情况下,访问任意合适的 Swarm 节点的发布端口,流量可能被路由到运行该服务任务的节点。

这与普通容器的:

docker run -p 8080:80 nginx

不同:

  • 普通容器端口发布主要是该宿主机上的 DNAT;
  • Swarm 服务发布还可能包含节点间转发;
  • 故障排查必须确认访问的是哪个节点、服务是否有任务、ingress 网络是否正常。

Swarm 也支持 host 发布模式,使端口只在运行任务的节点上直接监听。具体配置应使用当前 Engine 的服务发布语法,并明确节点调度和端口冲突风险。

5. Overlay 加密的边界

Overlay 可以配置加密选项,使节点间数据面使用加密机制。加密只解决相应 Overlay 数据面的机密性和完整性问题,不等于:

  • 容器应用协议自动安全;
  • 宿主机已被保护;
  • 节点管理流量全部按同一方式加密;
  • 应用不再需要 TLS、认证和授权。

启用加密还会引入额外处理开销,必须通过实际流量验证性能和 MTU 行为。不能把“网络已加密”直接等同于“系统端到端安全”。


六、其他网络模式:理解边界才能正确选型

1. host 模式

docker run --rm --network host nginx

在 Linux Engine 上,容器与宿主机共享网络命名空间。此时:

  • 容器不会拥有独立的容器 IP;
  • -p 通常没有普通 bridge 模式下的意义;
  • 容器监听的端口就是宿主机端口;
  • 多个容器可能发生端口冲突;
  • 网络隔离明显减弱。

host 模式适合明确需要减少网络隔离或直接使用宿主机网络的场景,不应因为“访问方便”而默认使用。

Docker Desktop 的 host networking 依赖其具体版本和配置,不能直接将 Linux Engine 行为套用到 Desktop。

2. none 模式

docker run --rm -it --network none alpine

容器只有回环接口或极少的本地网络能力,不连接 Docker 提供的外部网络。它适合需要显式配置网络或测试网络隔离的场景。

3. container:<name> 模式

docker run -d --name app nginx
docker run --rm --network container:app alpine ip addr

第二个容器共享 app 的网络命名空间,因此共享:

  • IP 地址;
  • 网络接口;
  • 路由;
  • 端口空间。

这种模式常用于 sidecar、调试容器或为无法安装诊断工具的容器提供工具。但它不是“把两个容器接入同一 bridge”,而是让两个容器使用同一个网络栈。


七、Compose 网络的生命周期和工程边界

一个典型 Compose 配置:

services:
  api:
    image: nginx:latest
    networks:
      - frontend
      - backend
    ports:
      - "127.0.0.1:8080:80"

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: example
    networks:
      - backend

networks:
  frontend:
  backend:

启动:

docker compose up -d

Compose 通常会:

  1. 创建项目网络;
  2. 创建服务容器;
  3. 将服务接入声明的网络;
  4. 创建服务名对应的 DNS 记录;
  5. 根据 ports 安装宿主机端口发布规则。

此时:

  • 宿主机访问 127.0.0.1:8080,到达 api
  • api 可以通过 db:5432 访问数据库;
  • db 不在 frontend 网络中,不能直接通过 Compose DNS 解析 api
  • api 同时连接两个网络,但数据库端口没有因为 db 使用 backend 就自动暴露到宿主机。

查看实际配置和实际网络:

docker compose config
docker compose ps
docker network ls
docker network inspect PROJECT_backend

docker compose config 用于确认变量替换、合并文件和最终配置;排障时不要只看原始 YAML,因为环境变量和多个 Compose 文件可能改变最终结果。

depends_on 不等于网络就绪

服务依赖关系主要影响启动顺序或健康检查等待逻辑,不会保证数据库已经接受连接,更不会修复 DNS、路由和端口问题。

例如,应用启动时应实现:

  • DNS 失败重试;
  • TCP 连接重试;
  • 数据库协议级健康检查;
  • 合理的超时;
  • 连接池重建。

depends_on 当成“数据库已可用”的证明,会把启动时序问题误判成网络问题。


八、从“服务能否访问”推导检查条件

对于容器 A 访问容器 B 的 TCP 服务,可以把成功条件写成:

S=RDLFPS = R \land D \land L \land F \land P

其中:

  • RR:名称或目标地址可解析;
  • DD:从 A 到 B 的路由和 Docker 网络路径存在;
  • LL:B 内应用在目标地址和端口监听;
  • FF:中间防火墙和网络策略允许连接;
  • PP:协议层请求符合服务要求。

访问宿主机发布端口时,还要增加端口发布条件 NN

Spublished=RhNDLFPS_{\text{published}} = R_h \land N \land D \land L \land F \land P

其中:

  • RhR_h:客户端解析到正确的宿主机地址;
  • NN:宿主机端口到容器端口的 NAT/发布规则存在。

这个分解直接指导排障顺序:

  1. 先确认名称解析;
  2. 再确认 TCP 路径;
  3. 再确认服务监听;
  4. 再确认防火墙和端口发布;
  5. 最后检查 HTTP、TLS、认证等应用协议。

如果一开始就修改 Docker daemon 配置或重建所有容器,通常会丢失故障现场,且无法确定是哪一个条件失败。


九、系统化排障流程

1. 先确认容器和网络状态

docker ps
docker inspect api
docker network ls
docker network inspect app-net

重点看:

  • 容器是否实际运行;
  • 容器是否退出后被重启;
  • 容器是否连接到了预期网络;
  • 容器获得的 IP 是否为空或变化;
  • 网络是否被删除、重建或连接错误;
  • Compose 项目是否使用了另一个项目名导致网络名不同。

查看容器的网络摘要:

docker inspect -f '{{json .NetworkSettings.Networks}}' api

不要只检查容器状态为 UpUp 只表示主进程仍在运行,不表示应用已经监听端口,也不表示网络路径可用。

2. 在容器内部确认监听和路由

docker exec api ss -lntp
docker exec api ip addr
docker exec api ip route

如果镜像没有 ssipnslookup,可以启动临时诊断容器加入同一网络:

docker run --rm -it --network app-net alpine sh

然后执行:

ip addr
ip route
getent hosts api
nc -vz api 8080

如果需要从目标容器的网络命名空间观察,而不是从另一个容器观察,可以在 Linux 宿主机上使用容器 PID:

docker inspect -f '{{.State.Pid}}' api

随后使用 nsenter

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

这要求宿主机具备 nsenter,并且当前用户有足够权限。

3. 区分 DNS、连接和应用错误

在同一网络中运行:

getent hosts api
nc -vz api 8080
curl -v --connect-timeout 3 http://api:8080/health

结果解释:

现象 更可能的层次
Name or service not known 名称、网络作用域、Docker DNS
解析成功但 connection refused 应用未监听、端口错误、监听地址错误
连接超时 路由、防火墙、MTU、服务阻塞或节点间网络
TCP 成功但 HTTP 4xx/5xx 应用路由、认证、配置或上游依赖
宿主机能访问,容器不能访问 容器路由、DNS、网络隔离或代理配置
容器能访问,外部不能访问 端口发布、绑定地址、主机防火墙或云安全组

测试容器内的 localhost 时必须特别谨慎。下面两个命令测试的不是同一对象:

docker exec api curl http://127.0.0.1:8080
docker run --rm --network app-net curlimages/curl \
  http://api:8080

前者测试 api 自己的网络命名空间,后者测试另一个容器通过网络访问 api

4. 检查端口发布

docker port web
docker ps --format 'table {{.Names}}\t{{.Ports}}'
ss -lntp

docker port web 能说明 Docker 记录的发布关系,例如:

80/tcp -> 127.0.0.1:8080

如果发布关系存在但访问失败,再检查:

  • 应用是否监听容器端口;
  • 发布是否绑定到了仅本机地址;
  • 客户端是否访问了错误的宿主机 IP;
  • 主机防火墙是否允许该端口;
  • 云安全组是否允许入站;
  • 是否有其他进程占用了宿主机端口。

不要仅凭 ss 看到宿主机监听端口就断定应用健康。某些端口发布路径可能由内核规则、用户态代理或不同 Engine 配置实现,具体监听表现具有实现差异。应结合端到端连接测试和 Docker 网络信息判断。

5. 检查防火墙和 Docker 规则

在使用 iptables 的系统上,可以查看:

sudo iptables -t nat -S
sudo iptables -S

在 nftables 系统上,可以查看:

sudo nft list ruleset

Docker 会安装与容器转发和端口发布相关的规则。不要直接清空或手工删除 Docker 维护的规则,否则可能同时破坏:

  • 已有容器的出站 NAT;
  • 端口发布;
  • bridge 间隔离;
  • Swarm ingress;
  • Docker 重启时的状态一致性。

如果需要为宿主机统一增加规则,iptables 环境中通常应优先考虑 DOCKER-USER 链,而不是直接修改 Docker 自动生成的链。规则仍需结合当前发行版、防火墙管理器和 nftables 兼容模式验证。

同时检查 Docker daemon 配置:

docker info

关注:

  • Rootless 或 rootful 模式;
  • Swarm 是否启用;
  • IPv6 是否启用;
  • 使用的存储和网络环境;
  • 安全配置和代理配置。

6. 用抓包确定报文停在哪里

Linux 宿主机上可以先抓 bridge:

sudo tcpdump -ni docker0 tcp port 80

如果是用户自定义 bridge,先通过:

docker network inspect app-net

确认对应 bridge 名称,再抓对应接口。

Overlay 排障时,除容器接口外,还要观察节点间的 UDP 数据面,例如:

sudo tcpdump -ni any udp port 4789

抓包结果可帮助区分:

  • 客户端根本没有发包;
  • 宿主机收到了包但没有进行预期转发;
  • 报文到达 bridge 但没有进入容器;
  • Overlay 外层包没有到达目标节点;
  • 目标节点解封装后没有送达任务。

抓包会暴露请求数据、Cookie 或凭据,生产环境中应限制权限和保存范围。


十、典型故障路径和恢复方法

故障一:容器之间使用 localhost 连接

错误配置:

DATABASE_HOST=localhost

如果应用和数据库是两个容器,localhost 指向应用容器自身,不是数据库容器。

修复方式:

DATABASE_HOST=db
DATABASE_PORT=5432

前提是两个服务连接同一 Docker 网络,并且数据库实际监听容器网络地址。

验证:

getent hosts db
nc -vz db 5432

故障二:服务名无法解析

检查三件事:

docker network inspect app-net
docker inspect api
docker inspect db

确认 apidb 的网络端点是否位于同一个网络。常见原因包括:

  • Compose 服务声明了不同网络;
  • 手工 docker run 接入了错误网络;
  • 使用了容器名而不是 Compose 服务名;
  • 网络被删除后容器未重新创建;
  • 查询发生在默认 bridge 网络;
  • DNS 配置被应用或启动脚本覆盖。

修复后应重新测试:

docker exec api getent hosts db

仅重启应用进程通常不能修复“容器未连接正确网络”;必要时应使用正确网络重新创建容器。

故障三:发布端口后仍然 connection refused

按顺序执行:

docker port web
docker exec web ss -lntp
docker inspect web
curl -v http://127.0.0.1:8080

docker port 没有映射,重新创建容器并添加 -p。端口发布是容器创建时的重要配置,不能把 EXPOSE 当成动态发布。

若映射存在但容器内只监听 127.0.0.1,修改应用监听地址。

若容器内服务端口错误,例如应用监听 8080 却发布:

-p 8080:80

则应改为:

-p 8080:8080

宿主机端口和容器端口不要求相同,但目标端口必须匹配应用真实监听端口。

故障四:跨节点 Overlay 中小请求正常,大请求超时

这通常提示 MTU 或路径分片问题。原因链是:

  1. Overlay 为原始报文增加封装头;
  2. 有效负载空间变小;
  3. 某些路径禁止分片或丢弃 ICMP;
  4. 小报文仍能通过,大报文触发丢包;
  5. TCP 可能表现为连接建立后请求卡住或重传。

可以使用逐步增大的探测报文和抓包验证路径,但具体 ping-M 语法因系统而异。修复方向包括:

  • 统一节点间网络 MTU;
  • 为 Overlay 配置合适的 MTU;
  • 允许必要的 PMTU 发现报文;
  • 检查云厂商网络和隧道链路;
  • 不要在未验证的情况下盲目把 MTU 改成某个固定值。

MTU 是路径属性,不是“容器网络越小越安全”的开关。

故障五:Overlay 服务发布可用,但任务间通信失败

这说明 ingress 或发布路径可能正常,但 overlay 数据面或服务发现异常。应分别测试:

  1. 解析服务名;
  2. 访问服务 VIP 或任务地址;
  3. 查看服务任务分布;
  4. 检查节点间 7946 和 4789;
  5. 检查 overlay 网络状态。

命令示例:

docker service ls
docker service ps web
docker network inspect app-overlay
docker node ls

不要因为访问发布端口成功,就推断所有任务间网络都正常;发布路径和服务内部路径可能经过不同组件。


十一、网络安全边界:能连通不代表应当连通

1. 发布端口扩大了入口

不指定绑定地址:

-p 8080:80

通常会让宿主机外部地址也参与监听或转发。只供本机开发工具访问时,优先:

-p 127.0.0.1:8080:80

只供内部应用访问时,通常不需要发布到宿主机,直接把服务放到同一用户自定义网络即可。

2. Bridge 网络不是强安全边界

同一 bridge 中的容器能够直接进行网络通信。应用级认证、数据库授权和 TLS 仍然必要。Docker 网络隔离可以减少不必要的连通性,但不能替代:

  • 身份认证;
  • 最小权限;
  • 主机访问控制;
  • 密钥管理;
  • 加密传输;
  • 审计和监控。

在多租户或高风险环境中,还应评估宿主机权限、容器能力、网络策略和运行时隔离,而不能只依赖“不同 Docker 网络”。

3. internal 网络的含义

Compose 或 Docker 网络可以配置为内部网络,使其不直接连接外部网络路径。它适合数据库、缓存等不需要出站访问的组件,但具体行为仍需结合驱动和 Docker 版本验证。

“内部网络”不等于网络中的服务彼此隔离。相同内部网络中的容器仍可能互相访问,应用层访问控制仍然需要存在。


十二、IPv6、地址规划和网络配置边界

Docker 默认环境常以 IPv4 为主。启用 IPv6 需要同时考虑:

  • Docker daemon 的 IPv6 配置;
  • 网络创建时的 IPv6 子网;
  • 宿主机和上游网络的 IPv6 路由;
  • 防火墙规则;
  • 应用是否监听 IPv6;
  • DNS 是否返回 AAAA 记录。

不能因为容器拥有 IPv6 地址,就推断外部 IPv6 客户端一定能够访问。外部可达性仍取决于路由、发布规则和防火墙。

自定义子网时应避免与宿主机、VPN、云 VPC 或其他 Docker 网络重叠:

docker network create \
  --subnet 172.30.0.0/16 \
  app-net

如果宿主机已经通过 VPN 使用 172.30.0.0/16,容器发往该地址的流量可能被错误地认为是本地 Docker 网络流量。地址规划冲突会表现为间歇性、环境相关的网络故障,尤其难以从单个容器内部判断。


十三、生产环境中的取舍

使用用户自定义 Bridge 的场景

适合:

  • 单机 Compose 应用;
  • 明确划分 frontend、backend 等网络;
  • 服务之间需要通过名称发现;
  • 不需要跨宿主机通信。

使用 Overlay 的场景

适合:

  • 多 Docker 节点;
  • Swarm 服务需要跨节点通信;
  • 需要服务级别的跨节点发现和调度;
  • 能够管理节点间控制面、数据面和防火墙。

如果系统实际运行在 Kubernetes、云负载均衡或其他编排平台上,应使用平台提供的网络抽象,不要把 Docker Compose 的网络模型直接当成跨平台标准。

端口发布的原则

内部服务尽量通过 Docker 网络访问,不发布到宿主机:

services:
  api:
    networks: [backend]

  db:
    networks: [backend]

只有需要被宿主机、反向代理或外部客户端访问的入口才发布端口:

services:
  gateway:
    ports:
      - "127.0.0.1:8080:80"

如果反向代理运行在另一台机器上,则不能绑定 127.0.0.1,因为远程代理无法访问宿主机回环地址;此时应绑定适当的宿主机地址,并通过防火墙限制来源。


十四、最终检查表

遇到 Docker 网络问题时,可以按以下顺序收敛范围:

# 1. 容器是否运行
docker ps -a

# 2. 容器连接了哪些网络
docker inspect CONTAINER
docker network inspect NETWORK

# 3. 目标名称是否能解析
docker exec SOURCE getent hosts TARGET

# 4. 目标端口是否能建立 TCP 连接
docker exec SOURCE nc -vz TARGET PORT

# 5. 目标应用是否监听正确地址和端口
docker exec TARGET ss -lntp

# 6. 宿主机发布关系是否存在
docker port TARGET
ss -lntp

# 7. 宿主机路由、防火墙和 Docker 规则
ip route
sudo nft list ruleset
# 或按系统使用 iptables 检查

# 8. 跨节点 Overlay 状态
docker node ls
docker service ps SERVICE
docker network inspect OVERLAY_NETWORK

# 9. 仍不确定时抓包
sudo tcpdump -ni any ...

核心判断可以归纳为三句话:

  1. 容器间访问优先使用同一用户自定义网络中的服务名,而不是 localhost 或固定 IP。
  2. EXPOSE 不是端口发布;外部访问必须明确配置 -p 或 Compose ports,并检查绑定地址。
  3. Bridge 解决单机容器连接,Overlay 解决跨节点服务网络;两者都依赖 Linux 路由、NAT、防火墙、DNS 和实际监听状态。

理解这些数据流和状态边界后,Docker 网络故障就不再只是“重启容器试试看”,而可以被拆解为名称解析、网络端点、路由转发、端口发布、应用监听和协议响应等可验证的独立条件。


系列导航与关联阅读

官方资料

本文依据 Docker、OCI 与 CNCF 官方文档重新梳理;正文与生产检查清单由 WR BLOG 编写。