背景与问题
在中级项目里,Docker 网络问题很少表现为“完全不通”,更多时候是这种让人皱眉的情况:
- 容器之间有时能通,有时不能通
- 宿主机能访问容器,容器却访问不了宿主机服务
localhost在容器里明明能通,换成另一个容器就不通- 切到
host网络后问题“突然好了”,但又带来端口冲突和安全疑虑 docker-compose里服务名解析正常,手工docker run却不行
我自己排这类问题时,最容易浪费时间的不是命令不会,而是没有先搞清楚容器当前到底在哪个网络模型里。bridge、host、自定义 bridge 网络,看起来都像“能联网”,但连通性边界完全不一样。
这篇文章不打算泛讲 Docker 网络大全,而是围绕一个典型故障排查思路展开:
先复现,再定位网络层级,再用 bridge、host、自定义网络逐个验证,最后给出止血与长期方案。
背景示意:三种常见网络模式的差异
flowchart LR
subgraph Host[宿主机]
APP[宿主机服务: 127.0.0.1:8080]
subgraph BR[Docker 默认 bridge]
C1[容器 A]
C2[容器 B]
end
subgraph CNET[自定义 bridge 网络]
C3[web]
C4[api]
end
H1[host 网络容器]
end
C1 -. 不能直接用服务名 .-> C2
C3 -->|服务名解析| C4
H1 -->|共享宿主机网络栈| APP
从排障视角,可以先记住一句话:
- 默认
bridge:能出网,容器间不天然靠名字互通,偏“单机临时运行” host:容器直接用宿主机网络栈,简单粗暴,但隔离弱- 自定义
bridge:容器间支持内置 DNS 解析,最适合一组业务服务互联排障
现象复现
下面我们故意造一个常见问题:
- 启动一个 HTTP 服务容器
web1 - 再启动一个诊断容器
client1 - 在默认
bridge网络中,尝试通过容器名访问web1 - 观察失败,再切换到自定义网络验证成功
1)启动一个测试服务
docker run -d --name web1 nginx:alpine
确认服务在运行:
docker ps
2)启动一个诊断容器并测试访问
docker run --rm -it --name client1 alpine:3.19 sh
进入容器后安装基础工具:
apk add --no-cache curl bind-tools iproute2
先用容器名访问:
curl http://web1
大概率你会得到类似错误:
curl: (6) Could not resolve host: web1
这就是默认 bridge 下非常典型的“我以为容器名能通,其实不能”。
核心原理
Docker 网络排障如果只记命令,很快就乱。中级项目建议按这三个问题理解:
- 当前容器在哪个网络里?
- 目标地址是 IP、宿主机地址,还是容器服务名?
- 报错属于 DNS 解析失败、路由失败,还是端口未监听?
1)默认 bridge 与自定义 bridge 的关键差别
默认 bridge 网络是 Docker 装好就有的。很多人第一次接触时会误以为:
只要都在 bridge,容器之间就能互相用名字访问。
其实不是。
容器名自动 DNS 解析,主要发生在“用户自定义 bridge 网络”中。
查看网络:
docker network ls
你通常会看到:
bridgehostnone
如果执行:
docker network inspect bridge
你会看到加入默认 bridge 的容器信息,但这不等于有稳定的服务发现能力。
2)host 网络为什么“看起来很灵”
host 模式下,容器不再拥有独立网络命名空间的虚拟网卡视角(更准确说,是直接共享宿主机网络栈)。所以:
- 容器访问
127.0.0.1,实际就是宿主机的127.0.0.1 - 不需要 Docker 做端口映射
- 网络性能路径更短一些
但代价也很明显:
- 端口冲突直接暴露
- 容器网络隔离基本没了
- 排障时容易把“容器问题”误判成“宿主机问题”
3)排障时要区分三类失败
DNS 解析失败
典型报错:
Could not resolve host
说明你连“目标是谁”都没找到,先查:
- 容器是否在同一自定义网络
- 容器名/服务名是否正确
/etc/resolv.conf和 Docker 内置 DNS 是否正常
TCP 连不上
典型报错:
Connection refused
No route to host
Operation timed out
它们意义不同:
Connection refused:对方地址可达,但端口没监听或被拒绝No route to host:路由/网络路径有问题timed out:常见于防火墙、丢包、目标未响应
HTTP 层失败
比如 502、404、握手失败。
这时网络可能已经通了,问题在应用协议层,不要继续死磕 Docker 网络。
一次完整定位路径
我习惯按下面顺序走,基本不会绕太远。
flowchart TD
A[现象: 访问失败] --> B{能解析目标吗?}
B -- 否 --> C[查网络类型/服务名/DNS]
B -- 是 --> D{能建立TCP连接吗?}
D -- 否 --> E[查监听端口/端口映射/防火墙/路由]
D -- 是 --> F{应用响应正常吗?}
F -- 否 --> G[查HTTP协议/应用配置/反向代理]
F -- 是 --> H[网络正常, 问题已定位]
可以把它理解为:
名字 -> 路 -> 门 -> 服务
- 名字:DNS / 服务名
- 路:IP 路由 / 网络模式
- 门:端口监听 / 防火墙
- 服务:应用本身
实战代码(可运行)
下面用一组最小可运行命令,把三种网络模式都跑一遍。
场景 A:默认 bridge 下的连通性问题
启动服务容器
docker run -d --name web1 nginx:alpine
查看容器网络信息
docker inspect web1 --format '{{json .NetworkSettings.Networks}}'
启动诊断容器
docker run --rm -it alpine:3.19 sh
容器内执行:
apk add --no-cache curl bind-tools iproute2
ip addr
ip route
nslookup web1 || true
curl -I http://web1 || true
你会看到:
- 可能 DNS 无法解析
web1 - 即使知道 IP,也不推荐依赖动态 IP 做业务调用
用容器 IP 临时验证
在宿主机查 IP:
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web1
假设得到 172.17.0.2,再到诊断容器里:
curl -I http://172.17.0.2
如果能通,说明:
- 网络路径基本通
- 问题更可能在服务发现/名称解析而不是 TCP
场景 B:自定义 bridge 网络解决服务互访
创建自定义网络
docker network create app-net
在该网络中启动两个容器
docker run -d --name web2 --network app-net nginx:alpine
docker run --rm -it --name client2 --network app-net alpine:3.19 sh
进入 client2 后执行:
apk add --no-cache curl bind-tools iproute2
nslookup web2
curl -I http://web2
这时通常就能成功。
查看自定义网络详情
docker network inspect app-net
这里能看到容器都加入了同一个用户自定义网络,Docker 内置 DNS 会对容器名做解析。
场景 C:host 网络快速止血验证
有些时候你怀疑问题不是应用本身,而是 NAT、端口映射或者容器网络隔离。
这时可以临时用 host 模式做对照实验。
用 host 网络启动 nginx
docker run -d --name web-host --network host nginx:alpine
然后在宿主机上查看监听:
ss -lntp | grep :80 || true
如果容器启动成功,你会发现宿主机直接监听了 80 端口。
在容器内/宿主机验证
宿主机执行:
curl -I http://127.0.0.1:80
如果通,而 bridge 模式不通,就说明排查重点要回到:
- 端口映射是否正确
- 容器是否监听在
0.0.0.0而不是127.0.0.1 - 网络策略/iptables 是否拦截
注意:
host更适合作为“对照组”或临时止血,不一定适合作为长期方案。
场景 D:宿主机服务无法被容器访问
这也是高频坑。
假设宿主机跑了一个只监听本地回环的服务:
python3 -m http.server 8080 --bind 127.0.0.1
宿主机本地访问:
curl http://127.0.0.1:8080
没问题。
但容器里访问宿主机:
docker run --rm -it alpine:3.19 sh
容器内:
apk add --no-cache curl
curl http://host.docker.internal:8080
在 Linux 环境里,host.docker.internal 不一定天然可用。可以这样启动:
docker run --rm -it --add-host=host.docker.internal:host-gateway alpine:3.19 sh
然后再测试:
apk add --no-cache curl
curl http://host.docker.internal:8080
如果仍然失败,很可能不是名字问题,而是宿主机服务只绑定了 127.0.0.1。
改为监听所有地址:
python3 -m http.server 8080 --bind 0.0.0.0
这时容器再访问,通常就通了。
一张更贴近排障过程的时序图
sequenceDiagram
participant U as 排障者
participant C as client容器
participant D as Docker网络
participant W as web容器/宿主机服务
U->>C: curl http://web
C->>D: 解析 web
alt DNS 失败
D-->>C: Could not resolve host
U->>D: 检查 network / inspect / 服务名
else DNS 成功
C->>W: 发起 TCP 连接
alt 连接被拒绝
W-->>C: Connection refused
U->>W: 检查监听地址和端口
else 连接超时
U->>D: 检查路由/防火墙/NAT
else HTTP 成功
W-->>C: 200 OK
end
end
常见坑与排查
下面这些坑,我基本都见过,很多还踩过不止一次。
1)把 localhost 当成“另一个容器”
这是最经典的误区。
在容器里:
localhost127.0.0.1
指向的是当前容器自己,不是宿主机,也不是别的容器。
错误示例
api 容器里配置数据库地址:
DB_HOST=127.0.0.1
如果数据库并不在同一个容器里,那几乎必挂。
正确思路
- 同网络容器之间:用服务名,如
mysql - 访问宿主机:用
host.docker.internal或宿主机网关地址 host网络模式下才可把容器与宿主机的回环视角合并考虑
2)服务只监听在 127.0.0.1
这个现象非常像“网络不通”,但本质不是网络。
比如应用在容器里只监听:
127.0.0.1:3000
即使做了:
docker run -p 3000:3000 ...
宿主机访问仍可能失败,因为容器外部流量无法进入容器内部回环接口上的服务。
如何验证
进容器看监听地址:
docker exec -it <container_name> sh
容器内:
ss -lntp
如果看到的是:
127.0.0.1:3000
就要改应用配置,让它监听:
0.0.0.0:3000
3)误以为 -p 影响容器间通信
-p 8080:80 是宿主机到容器的端口映射。
它不决定容器之间能否互通。
容器之间通信主要看:
- 是否在同一网络
- 是否能解析服务名
- 容器内服务是否监听正确端口
所以一个服务即便没做 -p,在同一自定义网络里的其他容器也依然可以访问它。
4)默认 bridge 下硬编码 IP
有些项目图省事,直接把一个容器 IP 写进配置:
REDIS_HOST=172.17.0.3
短期可能有效,但重建容器后 IP 常常变化。
中级项目一旦开始扩缩容、重启、CI/CD 重建,这种配置会变成隐患。
建议
- 单机多容器:用自定义网络 + 服务名
- 复杂环境:交给 Compose、Swarm 或 Kubernetes 的服务发现机制
5)宿主机防火墙/iptables 干扰
有时你命令都对,配置也对,就是超时。
这时候要怀疑宿主机安全策略。
常用检查
iptables -t nat -L -n
iptables -L -n
或者在新系统上:
nft list ruleset
如果宿主机本身对 FORWARD、DOCKER 链做了额外限制,Docker 自动生成的网络规则可能被覆盖或冲突。
6)容器加入了错误网络
一个容器明明跑着,但访问不到另一个服务,最后发现压根不在同一个网络里。
快速检查
docker inspect <container_name> --format '{{json .NetworkSettings.Networks}}'
如果两个容器不在同一自定义网络,服务名解析和连通性就不能按你预期工作。
动态补救
docker network connect app-net <container_name>
必要时断开错误网络:
docker network disconnect bridge <container_name>
止血方案
当线上故障已经影响业务时,排障不只讲“优雅”,还得讲“先恢复”。
止血方案 1:切到 host 做对照验证
适用场景:
- 怀疑是端口映射或容器网络层问题
- 允许临时暴露到宿主机网络
- 服务简单、单实例、端口冲突可控
不适合:
- 多实例并存
- 安全隔离要求高
- 宿主机端口紧张
止血方案 2:改用自定义 bridge 并统一服务名
适用场景:
- 单机或单节点多容器项目
- 需要稳定的容器间互访
- 当前问题集中在容器名解析和通信混乱
这是我更推荐的中长期方案。
止血方案 3:先用 IP 验证路径,再回收为服务名
适用场景:
- 必须尽快判断“是 DNS 问题还是 TCP 问题”
- 需要快速缩小故障范围
但只建议用于验证,不建议长期硬编码。
安全/性能最佳实践
网络能通只是底线,项目要稳定跑,还得考虑安全和性能边界。
1)优先使用自定义 bridge,而不是默认 bridge
原因很实际:
- 服务发现更清晰
- 网络边界更可控
- 便于按项目划分网络域
例如:
docker network create project-a-net
然后服务都加入这个网络,而不是全堆在默认 bridge 里。
2)谨慎使用 host 网络
host 的优点是少一层 NAT 和映射,排障时很直接。
但长期使用要满足这些前提:
- 明确知道容器会占用哪些宿主机端口
- 业务可接受较弱的网络隔离
- 宿主机上的其他服务不会发生端口冲突
如果是多租户、混部、或者安全要求高的环境,host 往往不是优选。
3)服务监听地址统一为 0.0.0.0
这是容器化应用的常见要求。
尤其 Web 服务、API 服务、消息网关,最好明确配置监听:
0.0.0.0
而不是开发环境习惯性的 127.0.0.1。
4)保留最小化诊断工具镜像
生产镜像可以很瘦,但排障时别临时抓瞎。
我通常会准备一个轻量诊断容器,带上:
curldig/nslookupiproute2netcattcpdump(按需)
例如临时排障:
docker run --rm -it --network app-net nicolaka/netshoot bash
它对中级项目定位网络问题非常省时间。
5)把“网络验证”写进交付流程
很多连通性问题不是线上才出现,而是发布前没人验证。
建议至少固化这几个检查项:
- 容器是否加入预期网络
- 服务是否监听在
0.0.0.0 - 容器间是否能通过服务名访问
- 宿主机暴露端口是否符合设计
- 是否存在与宿主机服务端口冲突
如果用 Compose,可以在部署脚本后追加健康检查。
一份实用排查清单
当你下次再遇到“容器不通”,可以直接按这个顺序走:
# 1. 看容器和端口
docker ps
# 2. 看容器在哪个网络
docker inspect <container_name> --format '{{json .NetworkSettings.Networks}}'
# 3. 看有哪些网络
docker network ls
# 4. 看目标网络详情
docker network inspect <network_name>
# 5. 进容器看IP、路由、DNS
docker exec -it <container_name> sh
ip addr
ip route
cat /etc/resolv.conf
# 6. 测服务名解析
nslookup <service_name> || true
# 7. 测TCP/HTTP
curl -v http://<service_name>:<port> || true
nc -zv <service_name> <port> || true
# 8. 看服务监听地址
ss -lntp
# 9. 看宿主机端口与规则
ss -lntp
iptables -t nat -L -n
如果你把每一步都能回答清楚,Docker 网络问题通常不会失控。
总结
排查 Docker 容器网络连通性,最怕的是一上来就“猜”。
更稳的方式是先搞清楚容器处在哪种网络模型里:
- 默认
bridge:适合简单场景,但别指望天然服务发现 host:适合快速对照和临时止血,长期使用要谨慎- 自定义
bridge:单机多容器项目里最实用,服务名解析也最省心
如果你想要一句可执行建议,我会这么说:
- 业务容器默认用自定义 bridge 网络
- 服务监听地址统一用
0.0.0.0 - 排障时先分清是 DNS、TCP 还是应用层问题
host只做对照实验或明确评估后的特例方案- 不要长期依赖容器 IP,尽量使用服务名
边界条件也要说明白:
- 如果是单机开发环境,
host有时确实方便 - 如果是多容器协作,自定义 bridge 几乎总比默认 bridge 更合适
- 如果已经进入编排阶段,网络问题还要结合 Compose/Swarm/Kubernetes 的服务发现一起看
很多所谓“Docker 网络玄学”,其实拆开后就是:
名字有没有解析到、路能不能到、端口有没有开、服务是不是监听对了。
按这个顺序查,问题通常会快很多。