Docker容器无法访问外网怎么办?DNS、路由与网络模式排查

宿主机 curl 一切正常,进到容器里 ping 8.8.8.8 却超时,或者能 ping 通 IP 但 apt install 一直卡在"正在连接"——这是 Docker 使用中最典型的网络故障。更让人困惑的是:同一台宿主机上,有的容器能上网,有的不能;重启 Docker 服务后又莫名其妙好了。这些现象背后其实是三条彼此独立的链路在起作用:容器的网络模式决定了它走哪条路,宿主机的转发与 NAT 决定了流量能否出去,DNS 配置决定了域名能否解析。本文按这三条链路把 Docker 容器无法访问外网的排查方法完整梳理一遍,给出可直接复制执行的命令。

一、先理解默认 bridge 模式是怎么出网的

Docker 安装后会在宿主机创建一张名为 docker0 的 Linux 网桥,默认网段 172.17.0.0/16,网桥自身地址通常是 172.17.0.1。每启动一个使用默认 bridge 网络的容器,就会创建一对 veth pair(虚拟以太网设备对):一端放进容器里成为 eth0,另一端挂在 docker0 上。

容器访问外网的完整路径是:容器内 eth0 → veth → docker0 网桥 → 宿主机内核路由 → iptables NAT 表 POSTROUTING 链做 MASQUERADE(源地址伪装) → 宿主机物理网卡 → 外部网络。这条链条上任何一个环节断掉,表现都是"容器没有网络"。

# 查看宿主机上的网桥与容器 veth
ip addr show docker0
ip link show | grep -E 'docker0|veth'
brctl show            # 需安装 bridge-utils

# 查看 Docker 有哪些网络
docker network ls
# 默认至少应有 bridge / host / none 三个

# 查看默认 bridge 的详细配置(子网、网关、MTU、连接的容器)
docker network inspect bridge

# 只看关键信息
docker network inspect bridge --format '{{.IPAM.Config}} {{.Options}}'

# 查看某个容器用的什么网络模式
docker inspect <容器名或ID> --format '{{.HostConfig.NetworkMode}}'
docker inspect <容器名或ID> --format '{{json .NetworkSettings.Networks}}' | python -m json.tool

二、分层测试:精确定位断在哪一层

不要一开始就重启 Docker。先跑一个干净的测试容器,按四层顺序测,能立刻把问题范围缩小到一个点。

# 起一个带完整网络工具的临时容器(用完即删)
docker run -it --rm --network bridge busybox sh

# 在容器内按顺序执行:
# 第 1 层:看自己有没有 IP 和网关
ip addr
ip route
# 应看到 eth0 有 172.17.0.x 的地址,默认路由指向 172.17.0.1

# 第 2 层:ping 网桥(验证 veth 与网桥连通)
ping -c 2 172.17.0.1

# 第 3 层:ping 宿主机物理网卡 IP(验证宿主机没拦 ICMP)
ping -c 2 192.168.1.10

# 第 4 层:ping 公网 IP(验证 NAT 与出口)
ping -c 2 8.8.8.8

# 第 5 层:解析域名(验证 DNS)
nslookup www.baidu.com
getent hosts www.baidu.com

# 第 6 层:TCP 层连通性(排除 ICMP 被禁的误判)
wget -O- -T 5 https://www.baidu.com >/dev/null && echo OK

结果判定:

测试现象断点位置下一步
容器内没有 IP / eth0 DOWN容器网络命名空间或 IPAM 分配失败重建网络:docker network rm 后重启 Docker
能 ping 通 172.17.0.1,ping 不通 8.8.8.8宿主机转发或 NAT 未生效检查 ip_forward 与 MASQUERADE 规则
能 ping 通 8.8.8.8,解析不了域名DNS 配置错误检查容器 resolv.conf 与 daemon.json dns
IP 通域名通,但 curl 超时目标端口被封 / MTU 问题 / 代理问题测 MTU、检查 http_proxy 环境变量
host 网络正常,bridge 网络不通bridge 链路本身有问题重建 docker0 网桥

三、网络模式:选错模式是最高频的原因

Docker 有四种主要网络模式,很多"不能上网"其实是选错模式造成的,特别是误用了 none 或自定义的内网 bridge。

模式启动参数能否访问外网说明
bridge(默认)--network bridge可以经 docker0 做 NAT 出网,最常用
host--network host可以共享宿主机网络命名空间,无 NAT,性能最好但端口易冲突
none--network none不能只有 lo 回环,完全隔离,用于纯计算任务
container--network container:NAME取决于被共享的容器与指定容器共享网络命名空间
自定义 bridge--network mynet可以支持容器名互解析,推荐生产使用
# 创建自定义 bridge 网络并指定子网、网关、MTU
docker network create \
  --driver bridge \
  --subnet 172.20.0.0/16 \
  --gateway 172.20.0.1 \
  --opt com.docker.network.driver.mtu=1450 \
  mynet

# 用指定网络启动容器
docker run -d --name web --network mynet -p 8080:80 nginx:1.25

# 把已运行的容器接入另一个网络(网络可以在线变更)
docker network connect mynet web
docker network disconnect bridge web

# 排查时临时切到 host 网络做对照测试
docker run -it --rm --network host busybox ping -c 2 8.8.8.8

# 查看某个网络下挂了哪些容器
docker network inspect mynet --format '{{range .Containers}}{{.Name}} {{.IPv4Address}}{{"\n"}}{{end}}'

四、DNS 专项排查:能 ping IP 但不能解析域名

这是最容易被误判成"网络不通"的场景。Docker 在不同网络模式下 DNS 行为不一样,必须先看清容器里的 /etc/resolv.conf。

# 查看容器实际使用的 DNS
docker exec <容器> cat /etc/resolv.conf
docker inspect <容器> --format '{{.HostConfig.Dns}} {{.ResolvConfPath}}'

# 用户自定义 bridge 网络:nameserver 应为 127.0.0.11
# (Docker 内嵌 DNS 服务器,它再转发给守护进程配置的 DNS)
# 默认 bridge 网络:通常是宿主机 /etc/resolv.conf 的拷贝

四种修法,按影响范围从小到大:

# 方式 1:单个容器启动时指定(影响最小,优先级最高)
docker run -d --name app --dns 223.5.5.5 --dns 114.114.114.114 nginx

# 方式 2:写入 docker-compose.yml
# services:
#   app:
#     dns:
#       - 223.5.5.5
#       - 8.8.8.8

# 方式 3:全局配置 /etc/docker/daemon.json(对所有新容器生效)
cat > /etc/docker/daemon.json <<'EOF'
{
  "dns": ["223.5.5.5", "114.114.114.114", "8.8.8.8"],
  "dns-opts": ["timeout:2", "attempts:3", "rotate"],
  "iptables": true,
  "ip-forward": true
}
EOF

# 方式 4:确认宿主机自身 DNS 正常(容器内嵌 DNS 会依赖它)
cat /etc/resolv.conf
dig www.baidu.com +short
systemctl status systemd-resolved

# 修改 daemon.json 后必须重载并重启 Docker
systemctl daemon-reload
systemctl restart docker

# 注意:daemon.json 的 dns 只对"新创建"的容器生效
# 已有容器需要重建才能拿到新配置

一个常见陷阱:如果宿主机 /etc/resolv.conf 里写的是 nameserver 127.0.0.53(systemd-resolved 的存根),容器会继承这个地址,但容器内的 127.0.0.53 并不是宿主机的解析服务,于是解析全部失败。解决方式是显式配置公网 DNS,或者用 host 网络。

五、宿主机侧:转发、NAT、防火墙与 MTU

如果容器 ping 不通 8.8.8.8 但能 ping 通 172.17.0.1,问题一定在宿主机这一层。

5.1 IP 转发是否开启

# 必须为 1,为 0 则容器完全出不了网
sysctl net.ipv4.ip_forward
cat /proc/sys/net/ipv4/ip_forward

# 临时开启
sysctl -w net.ipv4.ip_forward=1

# 永久生效
echo 'net.ipv4.ip_forward = 1' > /etc/sysctl.d/99-docker.conf
sysctl --system

# IPv6 场景
sysctl net.ipv6.conf.all.forwarding

5.2 iptables NAT 规则是否丢失

# 看 POSTROUTING 是否有 MASQUERADE 规则
iptables -t nat -L POSTROUTING -n -v --line-numbers
# 正常应看到类似:MASQUERADE all -- 172.17.0.0/16 !172.17.0.0/16

# 看 FORWARD 链策略,若为 DROP 且没有放行规则,容器也出不去
iptables -nvL FORWARD

# 看 Docker 自建链是否存在
iptables -t nat -L DOCKER -n
iptables -L DOCKER-USER -n -v

# 检查 daemon.json 里是否误关了 iptables
grep iptables /etc/docker/daemon.json
# 若为 false,Docker 不会写任何 iptables 规则,容器必然不能出网

如果 POSTROUTING 里完全没有 MASQUERADE 规则,通常有两种原因:一是 daemon.json 中 "iptables": false;二是宿主机重启后 Docker 服务启动顺序早于某些网络组件。重启 Docker 守护进程会重建这些规则,这也是"重启一下就好了"的根本原因:systemctl restart docker。

5.3 firewalld / ufw 冲突

CentOS/RHEL 上 firewalld 默认开启 MASQUERADE 限制,Ubuntu 上 ufw 默认拒绝 FORWARD,两者都会掐断容器出网。推荐做法是显式放行,而不是关掉防火墙。

# firewalld 场景
firewall-cmd --permanent --zone=public --add-masquerade
firewall-cmd --permanent --zone=public --add-port=8080/tcp
firewall-cmd --reload
firewall-cmd --list-all

# 或者把 docker0 加入 trusted 区域
firewall-cmd --permanent --zone=trusted --add-interface=docker0
firewall-cmd --reload

# ufw 场景:编辑 /etc/ufw/default 或 /etc/default/ufw
sed -i 's/^DEFAULT_FORWARD_POLICY=.*/DEFAULT_FORWARD_POLICY="ACCEPT"/' /etc/default/ufw
# /etc/ufw/before.rules 的 *nat 段中确保有:
# -A POSTROUTING -s 172.17.0.0/16 ! -o docker0 -j MASQUERADE
ufw reload

# 看 nftables 是否有冲突规则(新版系统 iptables 是 nft 兼容层)
nft list ruleset | head -50

5.4 MTU 不匹配导致的"能 ping 不能下载"

这是最隐蔽的问题:小包能通,大包(HTTP 响应、TLS 握手)卡死。常见于宿主机用了 VXLAN、PPPoE 或云厂商的 overlay 网络(MTU 通常 1450,而 Docker 默认 1500)。

# 查看物理网卡 MTU
ip link show eth0

# 查看 docker0 MTU
ip link show docker0

# 对比后修正:在 daemon.json 中统一 MTU
cat /etc/docker/daemon.json
# { "mtu": 1450 }

# 或对单个网络指定
docker network create --opt com.docker.network.driver.mtu=1450 mynet

# 容器内实测最大不分片包(-M do 禁止分片)
ping -c 2 -s 1472 -M do 8.8.8.8
# 1472 + 28(IP+ICMP头)= 1500;若失败逐步减小到 1422(对应 1450)

六、标准排查步骤清单

  1. 确认容器网络模式:docker inspect <容器> --format '{{.HostConfig.NetworkMode}}',排除误用 none。
  2. 容器内看 IP、路由:ip addr、ip route,确认有 172.17.0.x 且默认网关为 172.17.0.1。
  3. 分层 ping:172.17.0.1 → 宿主机 IP → 8.8.8.8 → 域名,定位断点层。
  4. 若 IP 通域名不通:查容器 /etc/resolv.conf,在 daemon.json 里补 dns 与 dns-opts。
  5. 若网关通外网不通:查 sysctl net.ipv4.ip_forward 必须为 1。
  6. 查 iptables:iptables -t nat -L POSTROUTING -n -v 确认有 MASQUERADE,iptables -nvL FORWARD 确认策略非 DROP。
  7. 查防火墙:firewalld 加 --add-masquerade,ufw 改 DEFAULT_FORWARD_POLICY=ACCEPT。
  8. 若小包通大包卡:对比物理网卡与 docker0 的 MTU,统一后重启 Docker。
  9. 检查 daemon.json 是否被误配 "iptables": false 或 "ip-forward": false。
  10. 最后手段:docker network prune 清理无用网络,再 systemctl restart docker 重建 docker0 与全部规则。
# 一次性收集诊断信息(贴给同事或工单很有用)
echo '--- docker version ---'; docker version --format '{{.Server.Version}}'
echo '--- networks ---'; docker network ls
echo '--- ip_forward ---'; cat /proc/sys/net/ipv4/ip_forward
echo '--- nat ---'; iptables -t nat -S POSTROUTING
echo '--- forward ---'; iptables -S FORWARD
echo '--- daemon.json ---'; cat /etc/docker/daemon.json
echo '--- resolv ---'; cat /etc/resolv.conf
echo '--- docker logs ---'; journalctl -u docker --no-pager -n 50

小结

Docker 容器无法访问外网,九成以上问题落在四个地方:网络模式选错、DNS 配置错误、宿主机 ip_forward 未开、iptables/firewalld 拦截了转发。剩下的疑难杂症基本是 MTU 不匹配和自定义网络的路由冲突。按"容器内 → veth/网桥 → 宿主机转发 → NAT → DNS"这条链路逐层测试,比盲目重启 Docker 高效得多。

避坑建议:

  • 生产环境用自定义 bridge 网络而非默认 bridge,自定义网络支持容器名互解析,且隔离性更好。
  • 不要把 iptables 设为 false,除非你非常清楚自己在自建网络方案。
  • daemon.json 改完只对新建容器生效,老容器必须重建才能拿到新的 DNS / MTU 配置。
  • daemon.json 必须是合法 JSON,一个多余逗号会导致 Docker 直接启动失败,改完用 cat /etc/docker/daemon.json | python -m json.tool 校验。
  • 不要长期用 --network host 绕问题,它牺牲了端口隔离,多实例部署时必然冲突。
  • MTU 问题优先怀疑 overlay/VPN 环境,特征是小包通、大文件下载卡在 0%。
  • 排查时保留现场,别急着 systemctl restart docker,重启会重建规则、把可复现的问题藏起来。

延伸阅读:容器能上网但把宿主机磁盘写满,通常是日志没有做轮转,可继续了解 Docker 日志驱动的容量限制配置。