搞Docker最容易劝退人的,不是镜像拉不下来,也不是命令记不住,而是网络冲突。我见过太多例子:容器明明启动了,宿主机访问不到;后端服务跑了一周,前端突然连不上;两个项目都要用8080端口,第二个直接报错。这些问题不算难,但第一次遇到时真的很想摔键盘。
所谓Docker网络冲突,不是一个固定报错,而是一整类症状。按资源类型可以拆成四类:端口冲突、网段冲突、容器互联不通、iptables/防火墙规则被干扰。四类问题的表现和排查手段完全不同,如果只盯着一句报错去搜索,经常越搜越乱。这篇内容我按这个思路展开,尽量给可直接复制的命令和方法,同时解释每个操作背后的原因。适合正在用Docker、Compose以及Docker Desktop的同学,尤其是刚踩坑不久的人。
在讲具体问题之前,把我自己的排查顺序放在最前面。我一般按这个顺序走,能解决80%以上的问题:
- 第一刀:
docker ps+docker port,看容器有没有起来、端口映射是否成功; - 第二刀:
ss -lntp+ip route,看宿主机监听端口和路由表; - 第三刀:
docker network ls+docker network inspect,看容器挂在哪个网络下; - 最后再碰iptables和防火墙。
很多人一上来就重启Docker、把网络删了重建,结果规则没变,该冲突的还是冲突。先看现象,再找原因,这是治本的第一步。
1. 先搞懂Docker的"网络"到底在吵什么
1.1 Docker网络模式速览:bridge、host、none、overlay、macvlan
Docker的默认网络模型是bridge,也就是桥接模式。可以理解成每家每户通过小区大门出入,门禁和网关由物业统一管理。每个bridge网络都会生成一个虚拟网桥,容器挂上去之后有独立IP,通过NAT方式访问外部。容器之间可以互通,但对宿主机外部来说,只有显式映射出来的端口才会被访问。默认bridge对应的网桥是docker0,地址通常是172.17.0.1,容器网段是172.17.0.0/16。
host模式完全不同,容器不创建独立网络栈,直接使用宿主机IP和端口。好处是性能好,没有NAT损耗,坏处是容器里监听的端口会直接占用宿主机端口,端口冲突概率大大增加。none模式则是"关起门来谁也不见",不配置网络,适合调试或纯离线计算任务。
overlay模式用于多台主机之间的容器互通,原理是VXLAN封装,让不同机器上的容器感觉在同一个网段。macvlan模式更直接,容器会拿到物理网络的IP和MAC地址,和宿主机一样作为独立设备接入交换机。这些模式里,bridge和overlay在实际部署中最常用。自定义bridge网络比默认bridge强在支持Docker内置DNS,容器可以用名字互相访问,这一点在微服务部署里特别重要。
1.2 冲突的本质:四类资源的"边界"被挤占
网络冲突的本质,是资源边界被挤占了。我习惯按这样的表格去归类:
| 资源 | 冲突场景 | 典型表现 |
|---|---|---|
| 端口 | 两个容器映射到宿主机同一个监听端口 | 报port is already allocated |
| IP | 容器IP或网桥IP与物理网络里的真实机器相同 | 路由错乱、ping不通 |
| 网段 | Docker默认网段与办公网/IDC内网网段重叠 | 访问某个IP被"劫持"到docker0 |
| 规则 | iptables规则被防火墙清掉或覆盖 | 端口映射看着正常,外部就是不通 |
Docker创建网络时会尽量避开已经被占用的网段,但它并不知道你的办公网、机房内网分配了哪些地址。如果物理网络恰好用了172.17.0.0/16,Docker还是会不客气地在路由表里加一条到docker0的直连路由,容器访问内网时就会走错路。这种"瞎眼"特性,是很多网段冲突的根源。
更麻烦的是,端口冲突至少会明确报错,网段冲突往往是静默发生的。容器启动正常,访问却莫名其妙失败。所以排查一定要按顺序来,别被表面现象带偏。
2. 最常见的冲突:端口映射"撞车"
2.1 症状识别与快速定位
端口冲突是最高发的问题,两个容器都映射8080端口,第二个启动时会看到一条很吓人的日志:
docker: Error response from daemon: driver failed programming external connectivity on endpoint web (hash...): Bind for 0.0.0.0:8080 failed: port is already allocated.请注意,报错其实有两部分:前半句"driver failed programming external connectivity"是Docker网络驱动在报错,后半句"Bind for 0.0.0.0:8080 failed"才是真正原因——宿主机8080端口被占了。
我遇到这种情况,会先执行:
docker ps --format 'table {{.Names}}\t{{.Ports}}' sudo ss -lntp | grep 8080ss -lntp能看到监听端口和对应进程。如果输出里是docker-proxy,说明是另一个容器占的;如果是nginx、java等其他进程,说明宿主机上本来就有程序监听了8080。想确认某个容器的映射关系,可以单独执行:
docker port web这条命令会把容器的端口和宿主端口对应关系列出来,很快能确认是不是映射撞车。
2.2 端口冲突的处理与"伪冲突"辨别
很多新手会疑惑:容器里不是监听80吗,为什么宿主机8080被占了?这里有个关键概念:容器内部端口和宿主机端口是两个体系。默认bridge模式下,容器内80端口和宿主机80端口没有天然关系,只有-p 8080:80才把两者关联起来。真正会冲突的,是宿主机上被占用的那个端口。
只有在host网络模式下,容器监听80才会和宿主机的80端口直接撞车。所以我的建议是:发现端口冲突,优先调整映射,而不是把容器切到host模式。host模式并不能让"撞车"消失,反而让容器直接暴露在宿主机网络里,引发更多冲突。
处理端口冲突有几点实操经验:
- 对外暴露的Web/API服务,宿主端口尽量规划在大号端口范围,比如18080、18081,避开常见端口。
- 像MySQL、Redis这类内部依赖,尽量不要映射到宿主机,让它们留在自定义网络里,只有应用容器能访问。
- 临时排查时可以用
-p 80这种简写,让Docker自动分配宿主端口,但生产环境千万别用,因为容器重建后端口可能就变了,对外配置会乱套。
2.3 Compose多服务场景的端口冲突
Compose同样会撞端口。一次部署里两个服务都写"8080:80",第二个服务启动时会报同样的bind错误。这类问题与其说是技术问题,不如说是端口规划问题。
我习惯在Compose文件里用环境变量控制端口:
services: web: image: nginx ports: - "${WEB_PORT:-8080}:80"这样每个环境可以通过.env文件或部署脚本指定不同端口,不用反复改yaml。启动前先在项目目录里搜索一下:
grep -r "8080:80" --include="*.yml" .如果整个目录里有多个项目都用了8080,趁早改掉一个。
注意:
docker run -p 80表示"容器80端口映射到宿主随机端口",不是等价于-p 80:80。很多人随手一敲,以为把80映射到了宿主80,结果docker ps里看到一串32768开头的随机端口就懵了。需要精确控制时,老老实实写成-p 8080:80。
3. 网段与子网冲突:容器网段"抢地盘"
3.1 Docker为什么喜欢用172网段
Docker默认bridge网络用的是172.17.0.0/16,docker0网桥本身占172.17.0.1。当你创建自定义bridge网络又没有指定IP段时,Docker会从172.18.0.0/16开始往后找一个空闲网段。
这个选择本身没问题,但现实是很多企业的办公网、机房内网使用的也是172.16.0.0/12这个私网段。比如物理网络恰好用了172.17.0.0/16,Docker又把这个网段分配给了docker0,冲突就来了。
我举一个真实例子。某个办公网段是172.17.0.0/16,开发机上装了Docker后,容器要访问办公网里的打印机172.17.0.50,内核路由一查,目标地址落在docker0直连网段内,于是把包送到docker0,打印机自然收不到。这不是防火墙拦截,而是路由被容器网段劫持了。
多台Docker主机也有类似问题。每台机器的172.17.x.x都是自己的,跨主机容器互相访问,很容易出现IP重复、路由错乱的情况。
3.2 调整Docker默认网段与地址池
要解决网段冲突,可以修改Docker守护进程配置,把默认网段换到和物理网络完全不重叠的段。推荐配置写在/etc/docker/daemon.json:
{ "bip": "10.88.0.1/24", "default-address-pools": [ {"base": "10.89.0.0/20", "size": 24} ] }bip是docker0的IP和掩码,也就是默认bridge网络用的网段。default-address-pools是自动创建自定义网络时分配子网的池子,这里base是10.89.0.0/20,size是24,意思是把10.89.0.0/20切成若干个/24子网,每个自定义网络分配一个/24。为什么size设为24?因为一般业务网络不会超过254个容器,/24够用,子网切得小能容纳的网络数量就多。
改完执行:
sudo systemctl restart docker重启后验证:
ip addr show docker0 docker network inspect bridge | grep -E 'Subnet|Gateway'注意:重启Docker服务会中断当前所有运行中容器的网络,容器进程通常不会被杀死,但网络会短暂断开若干秒。有状态服务如果缺少重连机制,可能会报错。业务高峰期不要乱重启,操作前先评估应用是否有自动恢复能力。
另外,如果一台机器上已经建了很多自定义网络,可以用docker network prune清理不用的,避免地址池耗尽。
3.3 手动创建网络时如何选IP段
与其依赖Docker自动分配,我更推荐创建网络时手动指定子网,让规划完全可控。操作流程是先看宿主机路由表:
ip route show ip -4 addr show确认哪些网段已经被操作系统和容器占用,然后挑一个空闲网段。例如:
docker network create \ --driver bridge \ --subnet=10.88.88.0/24 \ --gateway=10.88.88.1 \ myapp这样所有加入myapp网络的容器都会从10.88.88.2开始获得IP。如果需要给某个容器固定IP,可以加--ip 10.88.88.10。但我要提醒一句:能少用固定IP就少用。容器IP一旦固定,扩容和迁移都会多一层约束,服务发现交给Docker内置DNS更省心。
如果项目已经在用Compose,网络定义也建议写清楚:
networks: myapp: driver: bridge ipam: config: - subnet: 10.88.88.0/24规划网段这件事,看起来多花几分钟,实际上能把后面几个月的问题都提前消除。
4. 容器间"通而不达":同主机多网络与跨网络访问
4.1 容器互访的基本前提与多网络挂载
另一个高发问题是"两个容器明明在同一台宿主机上,IP却ping不通"。原因通常是它们不在同一个bridge网络里。
Docker的不同bridge网络之间是逻辑隔离的,即使IP段挨着,也不允许直接路由。可以想象成两栋楼的住户,各有各的门禁,中间没有连廊。要让它们通信,要么把两个容器放进同一个网络,要么让其中一个容器挂到另一个网络。
创建容器时直接指定网络:
docker run -d --name web --network myapp nginx docker run -d --name api --network myapp myapi如果容器已经创建了,不想重建,可以动态添加网络:
docker network connect myapp web容器挂多个网络后会有多个网卡,访问不同网段会走对应路由。这里有个注意点:容器里的应用监听地址最好写成0.0.0.0,否则可能只在其中一个网卡上监听,另一个网络访问不到。
4.2 容器名解析失败?先分清容器名还是服务名
同一个自定义bridge网络里,可以直接用容器名访问,比如docker exec web curl http://api:8080/health。自定义bridge网络内置DNS,容器名会被自动解析。但Compose项目有个特殊点:Compose创建的容器名通常是项目名-服务名-序号,网络里注册的别名却是服务名。所以你在容器里应该访问http://api,而不是http://项目名-api-1,这个细节经常坑人。
排查域名解析问题,我会进容器里直接验证:
docker exec -it web bash getent hosts api如果返回了IP,说明DNS没问题。如果报Temporary failure in name resolution,大概率是容器不在同一个自定义网络里。这时候再看一下容器里的/etc/resolv.conf:
docker exec web cat /etc/resolv.conf正常会看到Docker内置DNS的127.0.0.11。如果看到的是外部DNS,说明容器可能是host网络模式或特殊配置,容器名解析自然不好用。
4.3 跨主机容器怎么避免网段冲突
单机搞清楚之后,跨主机又会有新问题。默认bridge网络在每台主机上是独立网段,多台机器的容器互相访问靠IP不靠谱,因为每台机器的172.17.x.x是重复的。
跨主机容器要互通,标准做法是overlay网络。以Swarm为例:
docker swarm init docker network create -d overlay --attachable app-net加入这个网络的容器,即使在不同宿主机上,IP也是同一个网段。overlay底层通过VXLAN封装,物理网络只需要保证基础路由可达。
这里要特别提醒:overlay网络内部网段也必须避开物理网络。如果物理网络IP段和overlay内部网段重叠,封装后的数据在底层转发时会出问题。很多跨主机集群"时而通时而不通",最后查出来就是网段没规划好。多主机的场景,网络规划要从单机规划扩展成整个集群的规划。
5. Docker网络与系统防火墙/iptables的恩怨
5.1 Docker是如何"借用"iptables的
默认bridge网络能上网、端口映射能生效,靠的是一整套iptables规则。Docker启动时会在nat表里插入规则:容器访问外网做SNAT/MASQUERADE,外部访问映射端口做DNAT;在filter表的FORWARD链放行容器间流量。
这对普通用户是好事,但和系统防火墙的"地盘"产生了重叠。很多Linux发行版默认的firewalld或ufw一旦启动,会把FORWARD链默认策略设成DROP,或者重启时清掉Docker写入的规则。结果就是:容器能启动,docker ps里端口映射也显示正常,但外部访问不到容器服务。更迷惑的是,容器内部的互访也可能断。
这里有一个高频误操作:以为重启容器能恢复网络。实际不行。iptables规则由Docker守护进程管理,不是容器生命周期管理。正确做法是重启Docker服务来重建规则。
5.2 排查iptables和防火墙规则
排查这类问题,我一般看两个地方。
先看NAT规则里Docker的MASQUERADE和DNAT是否还在:
sudo iptables -t nat -L -n --line-numbers再看FORWARD链:
sudo iptables -L FORWARD -n --line-numbers如果整个链看不到DOCKER相关规则,而系统防火墙又处于开启状态,基本可以判定规则被冲掉了。排查防火墙状态:
sudo firewall-cmd --list-all或者:
sudo ufw status verbose恢复的第一选择是重启Docker服务:
sudo systemctl restart docker规则一般会回来。但如果你是生产环境,不建议用iptables -F这种核弹命令,那会把所有业务防火墙规则都清掉,影响面太大。
提示:修改iptables规则前,先执行
sudo iptables-save > /root/iptables-$(date +%F).bak备份。万一改乱了,能原样恢复。
5.3 安全的解决姿势:接管DOCKER-USER链
Docker留了一条自定义链叫DOCKER-USER,它位于FORWARD链中,Docker重启时不会清空你写进去的规则。所以如果要对容器流量做访问控制,应该往DOCKER-USER里插规则,而不是直接改DOCKER链。
例如只允许内网10.0.0.0/8访问容器映射的8080端口,其余来源全部拒绝:
sudo iptables -I DOCKER-USER \ -p tcp --dport 8080 ! -s 10.0.0.0/8 \ -j DROP-I表示插到链顶部,这样优先级最高。这条规则只影响经宿主机转发的流量,不影响Docker内部容器间通信。如果你用的是firewalld,也可以把类似规则写到对应zone里,但每个发行版行为不太一样。相比之下,DOCKER-USER链在绝大多数Linux环境是通用的。
6. 常见报错信息对照速查
6.1 端口冲突类报错
| 报错片段 | 根因 | 处理方式 |
|---|---|---|
Bind for 0.0.0.0:8080 failed: port is already allocated | 宿主机端口被监听占用 | ss -lntp | grep 8080查占用,换端口或清理进程 |
driver failed programming external connectivity on endpoint | 端口或NAT规则冲突,常伴随firewalld重启 | 先查端口,再重启Docker服务 |
iptables failed: -t nat -A DOCKER -p tcp --dport 8080 -j DNAT | iptables权限或内核模块异常,规则被锁 | 确认modprobe iptable_nat,重启Docker服务 |
这些报错都发生在容器创建或启动阶段,比较容易被发现。真正的坑是静默型冲突,比如端口映射看起来正常,外部就是不通,这时要回到上一节去查防火墙和iptables。
6.2 网络不存在或地址池耗尽类报错
| 报错片段 | 根因 | 处理方式 |
|---|---|---|
network xxx not found | 指定了不存在的网络 | 先docker network create创建,或检查网络名拼写 |
could not find an available, non-overlapping IPv4 address pool among the defaults | 自定义网络默认地址池耗尽或与已有网段冲突 | 修改daemon.json里的default-address-pools,或删除无用网络 |
address already in use | 网桥IP与宿主机已有IP冲突 | 检查ip addr show,给网桥指定独立IP |
地址池耗尽这个问题,常在Docker主机长时间部署很多项目后出现。一个项目一两个网络,时间久了数量相当可观。建议定期检查:
docker network ls docker system df不用的网络及时清理。
6.3 最容易混进来的"非网络冲突"报错
有些报错看着像网络冲突,实际不是。比如Windows上用Docker Desktop,看到:
Cannot connect to the Docker daemon at npipe:////./pipe/dockerDesktopLinuxEngine或者Linux下:
Cannot connect to the Docker daemon at unix:///var/run/docker.sock这是Docker引擎没起来或用户权限不够,不是网络冲突。先检查服务状态:
sudo systemctl status docker docker version在Docker Desktop里要看引擎开关是否亮起。权限不够的话,可以把自己的账号加入docker组,然后重新登录。这个点容易被带偏,但既然聊网络排查,我要提醒一句:网络命令全都依赖Docker daemon,如果daemon本身没起来,后面所有排查都是空中楼阁。
6.4 清理网络残留的正确姿势
删掉大量容器后,docker network ls里可能留着很多bridge网络。这些网络占不了多少资源,但会持续消耗地址池,导致新网络创建时地址不够用。清理无主网络用:
docker network prune -f这条命令会删除没有被容器使用的网络,建议在项目部署前执行。如果删除时提示network has active endpoints,说明还有容器挂在上面,先处理容器再删网络。如果某个网络删不掉,可以用:
docker network inspect 网络名看一下谁还占着这个网络。
7. 我怎么用一套简单规则避免Docker网络冲突
最后分享一些我自己的习惯。踩过几次坑之后,我把Docker网络规划固定成了一组规则,基本不再被网络折腾。
第一,动手前先查路由表。无论新装Docker还是部署新项目,先执行ip route show,心里有数,知道自己机器上有哪些网段,再决定Docker用什么段。
第二,每个项目创建独立自定义网络,网络命名统一为"项目名-环境",比如mall-prod、blog-dev。用Compose时,网络明确指定为外部网络,避免Compose默认创建一堆散装网络。
第三,端口映射写全、写明确。不用-P无脑发布所有端口,不依赖随机端口生产。内部依赖如MySQL、Redis,不映射到宿主机,只在自定义网络里被应用容器访问。
第四,用Compose环境变量管理端口。测试环境、生产环境各用各的.env,端口冲突发生后,改一处配置就能解决,而不是改多个yaml。
创建一个标准项目网络可以这样:
docker network create \ --driver bridge \ --subnet=10.88.100.0/24 \ --gateway=10.88.100.1 \ mall-prodCompose文件里声明为外部网络:
networks: mall-prod: external: true这样容器启动时加入的就是预先规划好的网络,IP段、网关、名称都在掌控内。
我这几年的体会是,Docker网络冲突九成发生在第一次部署那天。端口、网段、网络名这三件事,开工前花十分钟列一张表,后面基本不会再被网络问题绊住。如果你已经踩了坑,也不要急着重启Docker,先把现象记录下来,再按上面的顺序一条条查,通常很快能定位到真正原因。