1. Docker网络:部署时最容易被低估的一环
先说一句大实话:干过几年容器化部署的人,十有八九都被 Docker 网络坑过。明明两个服务都在同一台机器上,docker ps 看着也都活着,端口映射也写了 -p,结果业务容器就是连不上旁边那个 MySQL;要么是两个容器能互相访问,重启一次宿主机之后 IP 全变了,写死在配置里的内网地址全部作废。这类“看起来简单、查起来头大”的问题,几乎都集中在 Docker 网络这一层。
这篇内容主线是我自己在真实部署中反复走过的路:从 Docker 网络底层那几样关键东西讲起,再落到单机、跨主机不同场景怎么选型,然后给出一套可以照抄的容器编排网络配置,最后把高频网络故障的排查顺序整理成清单。不管你是刚装完 Docker Desktop 准备跑第一个容器的小白,还是正在做 docker 部署微服务项目、Redis 主从、MySQL 8.0 这类实际业务的运维同学,按这条路线往下捋,基本能绕开大多数常见的 Docker 网络不通问题。
1.1 Docker 网络到底在解决什么问题
没有网络隔离之前,跑在宿主机上的一堆服务共用一套 IP 和端口,你装了 Nginx 占掉 80,另一个应用想用 80 就得改端口,时间一长端口规划就是一锅粥。Docker 把每个容器放进独立的网络命名空间(network namespace),让每个容器有自己单独的虚拟网卡、IP、路由表、iptables 规则,这是容器之间不互相干扰的根基。
但隔离带来的副作用就是“通信变难了”。容器要访问外部网络,要上网;外部要访问容器里的服务,要进得来;容器和容器之间要互相调用,得找到对方。Docker 网络体系解决的就是这三件事:隔离、连通、暴露。
1.2 你会最早撞上的三类问题
以我见过的求助帖和实际踩坑记录来看,可以归成三类:
第一类是容器之间互相访问不了。最典型的例子:你启动了一个 MySQL 容器,又在另一个容器里跑业务程序,代码里写的连接地址是 localhost:3306,结果发现永远连不上。原因不复杂——业务容器里的 localhost 指它自己,不是宿主机,更不是 MySQL 容器。
第二类是端口映射写了,但外部死活访问不到。比如 docker run -p 8080:80 启动了 Nginx,容器内 wget 正常,宿主机浏览器访问 8080 却是空白页或者连接超时。这类问题常常不在 Docker 本身,而是卡在监听地址、防火墙规则、或者 iptables 的 NAT 表状态上。
第三类是容器的 IP 经常变,一重启就换地址。默认 bridge 网络下这几乎是必然的,只要容器重建,DHCP 分配就可能变。这个问题在写死 IP 的旧项目里特别讨厌,解决办法也很简单:换自定义网络,给容器固定 IP,或者直接用服务名解析。
这三个问题背后,对应的恰恰是 Docker 网络的三个关键机制:veth 虚拟网卡、iptables 端口映射、内嵌 DNS 解析。
2. 搞懂容器网络,先拆开这几层“胶带”
2.1 namespace、veth、docker0:数据包是从哪里走到哪里的
很多人对 Docker 网络的第一印象是一团乱麻,好像有无数个网桥、网卡、虚拟设备。其实你只需要盯住一条数据路径,就能把整件事串起来。
一旦你执行 docker run,Docker 会做三件事:先创建一个独立的网络命名空间给容器;然后创建一对 veth 虚拟网卡(veth pair),这对网卡像一根虚拟“网线”,一头插进容器的网络命名空间里变成 eth0,另一头插到宿主机上的 Linux 网桥上;最后给容器分配 IP 地址。默认情况下,这个网桥叫 docker0,桥接网段通常是 172.17.0.0/16。
数据包在容器里从 eth0 发出来,会经过这根虚拟网线到达 docker0 网桥;如果目标地址是宿主机网段或者外网,内核会走路由转发,再通过 NAT 把源地址替换成宿主机 IP 送出去。如果用 ASCII 画一下核心链路,就是这么个意思:
容器A (eth0 172.17.0.2) | veth (虚拟网线) | docker0 网桥 (172.17.0.1) | 内核路由 / iptables NAT | 宿主机物理网卡 eth0容器 A 想访问容器 B 的时候,数据包会在 docker0 这一层直接找到了目标 MAC 地址,在内核里交换过去,根本不经过外部网络,所以速度飞快。而容器 A 想访问外网,就必须依靠宿主机的 NAT 转发出去。
实操里我最常用的一条检查命令是 docker network inspect bridge,它能把该网络下的容器 IP、网关、连接的 veth 设备一一列出来,相当于一张内部“电话本”。遇到过网络不通的读者,第一件事就该先跑这条命令确认容器真正的 IP 和网段,而不是去猜。
2.2 五大内置网络驱动:bridge、host、none、overlay、macvlan
Docker 官方内置了五个网络驱动,名字分别是 bridge、host、none、overlay、macvlan。实际项目里最常用的是 bridge 和 overlay,但另外几个也有明确的适用场景。我把对比信息整理成了一张表。
| 驱动 | 模式 | 典型用途 | 主要注意点 |
|---|---|---|---|
| bridge | 默认,NAT 桥接到宿主机 | 单机容器、docker run 单容器、自定义子网 | 跨主机通信困难,需要额外路由规划 |
| host | 直接共享宿主机网络栈 | 对网络性能极其敏感的业务 | 容器没有独立 IP,端口冲突会直接炸 |
| none | 只有回环接口 lo | 仅做离线计算、临时调试 | 没有网络,不可外部访问 |
| overlay | 跨主机二层网络 | Swarm 集群、K8s 集群内部通信 | 有封装开销,加密模式更吃 CPU |
| macvlan | 把容器直连物理局域网 | 容器需要物理网段 IP 的场景 | 需要交换机允许,部分云环境不支持 |
很多人一上来就选 host,理由是“少一层 NAT,性能好”。确实,host 模式少了网桥和地址转换,网络延迟略低,但代价是容器完全没有网络隔离,宿主机的 80、3306、6379 被你自己的容器抢了之后,另一个容器就没法再用。微服务场景下一旦端口规划失控,排查起来比 NAT 那点开销痛苦得多。
我这边踩过的一个真实教训是:公司内部有个老项目为了图快全部用了 host 网络,结果部署第二套环境时端口冲突导致服务互相覆盖。后来统一改成自定义 bridge 网络 + 服务名互访,问题才算根除。
2.3 端口映射背后的 iptables 逻辑
很多教程把 -p 8080:80 当成“魔法参数”,但它背后完全是一套具体的 iptables 规则。Docker 其实是自动帮你加了两条关键规则:一条在 nat 表的 PREROUTING 链里做 DNAT,把访问宿主机 8080 的流量目的地址改写为容器 IP 的 80 端口;另一条是 POSTROUTING 链里的 MASQUERADE 规则,容器出外网时做源地址转换。
所以当你发现端口映射失效时,先不要急着重启容器,可以先看规则还在不在,用下面这条命令:
iptables -t nat -L -n | grep 8080如果规则确实存在但访问还是不通,那问题就出在规则之前的环节:比如防火墙的 FORWARD 链把转发流量挡了,或者监听地址被绑定到了 127.0.0.1,外部网卡根本收不到。这也是为什么我遇到“映射成功但外面访问不了”时,必查三件事:容器状态、iptables 规则完整性、以及服务进程有没有监听在 0.0.0.0。
我自己的习惯是给数据库这类关键服务绑定回环地址,比如 -p 127.0.0.1:3306:3306,只允许本机访问,不把 3306 直接暴露到局域网。后面第 6 部分细聊。
3. 选型思路:不同场景该用哪种网络策略
3.1 本地开发:默认 bridge 够用,但要搞清楚三个边界
如果你只是在本机装 Docker,跑几个练习用的容器,默认 bridge 网络完全可以应付。你只需要记住三个边界。
第一,容器访问宿主机,不要用 localhost。容器里的 localhost 是自己,想连宿主机的某个端口,可以用宿主机的局域网 IP,或者网关地址 172.17.0.1,再或者 Docker Desktop 提供的 host.docker.internal。很多新人就是死在这一步,把数据库地址写成 localhost,结果容器里运行的应用始终连不上外面。
第二,容器和容器之间,默认 bridge 下用 IP 访问还行,用容器名访问经常不通。因为默认 bridge 网络不自动做容器名 DNS 解析。Windows 上装 Docker Desktop 的同学感受会更明显,因为容器实际上跑在虚拟机里,网络链路又多了一层 NAT。
第三,默认 bridge 不保留 IP。每次容器重建,IP 都可能重新分配。本地开发无所谓,但生产环境不建议直接用默认网络跑微服务。
3.2 微服务和容器编排:自定义网络 + 服务名 DNS 才顺手
从本地开发往生产部署迈一步,第一件事就是创建自定义 bridge 网络。自定义网络和默认 bridge 相比,最大的区别是自带内嵌 DNS,可以通过容器名直接解析到对应 IP。这意味着你可以在代码里把 MySQL 的地址写成 mysql:3306,把 Redis 地址写成 redis:6379,完全不用关心容器底层到底拿到了什么 IP。
Docker Compose 就是默认这么工作的。你写一个 docker-compose.yml,里面声明两个服务,Compose 会自动创建一个独立网络,并把服务名注册进这个网络的 DNS。这也是 Docker 网络部署微服务项目时最推荐的起步姿势。
version: "3.8" services: mysql: image: mysql:8.0 networks: [app-net] app: image: your-app-image networks: [app-net] networks: app-net: driver: bridge这样配置之后,app 容器里连接数据库只需要 jdbc:mysql://mysql:3306/app_db,省去查 IP 的麻烦。我特别喜欢这个机制的原因是,它让服务之间的依赖关系从“IP 硬编码”变成了“逻辑名称”,代码从一个环境搬到另一个环境几乎不用改连接串。
3.3 跨主机分布式:overlay 网络和它要付出的代价
单机部署玩熟之后,你迟早会遇到跨主机的需求:两台服务器上的容器要互相访问。默认 bridge 网络是 NAT 到各自宿主机的,两个容器天然不在同一个二层网络里,直接 ping 对方的容器 IP 大概率不通。
Docker 给出的正规解法是 overlay 网络,它通常配合 Docker Swarm 使用。Swarm 会在每个节点上创建一个 overlay 网络,容器之间通信时,数据包会通过宿主机之间的物理链路封装转发。如果你启用了加密模式,节点间传输还会额外加密,代价是 CPU 开销更高、吞吐量下降。
实际项目中,用 overlay 网络有必要先想清楚一个问题:你的业务能不能接受集群网络带来的额外复杂度?如果只有两台机器,容器数量也不多,直接使用 macvlan 把容器接到物理网段,或者用宿主机端口映射 + 服务发现,可能比上 overlay 更轻。但如果你准备上 K8s,或者已经有 Swarm 集群,那 overlay 基本是默认选项,因为它让跨主机的服务发现和动态迁移变得非常自然。
不过要提醒一句:overlay 网络的前提是宿主机之间要能直接通信,防火墙千万不能把节点间数据链路堵死。这也是很多人在搭建集群时遇到“跨主机节点之间能通、容器之间不通”的根源。
4. 动手实例:把 Redis 主从、MySQL 和业务服务放进同一张安全网络
理论讲完,进入能直接抄作业的部分。我用一个最常见的业务形态做例子:MySQL 8.0 存储数据、Redis 做缓存(带主从)、后端业务服务通过服务名去访问它们,最外层用 Nginx 做入口网关。全部塞进 Docker Compose,网络使用自定义 bridge 子网。
4.1 创建自定义子网并分配静态 IP
如果你喜欢先用 docker network create 手动建网,命令可以是这样的:
docker network create \ --driver bridge \ --subnet=172.28.0.0/16 \ --gateway=172.28.0.1 \ app-net我之所以特意指定 subnet,是为了避免和宿主机局域网网段冲突。因为 Docker 默认的 172.17.0.0/16、172.18.0.0/16 这类网段,在一些公司内网里恰好和真实路由网段重叠,一旦重叠,NAT 转发会出现各种诡异的“时通时不通”。
固定 IP 的容器启动方式用 --ip 参数:
docker run -d --name mysql --network app-net --ip 172.28.0.10 -e MYSQL_ROOT_PASSWORD=yourpass mysql:8.0但说句实话,生产上我不太建议手动固定 IP 来维护服务,因为 IP 一旦写进配置文件,你就又退化回了“IP 硬编码”。自定义网络最舒服的点是用服务名解析,这一步只在个别场景有用,比如容器里跑的应用不支持自定义端口配置、只能认一个固定地址。
4.2 docker-compose 把一套完整业务串起来
更省心的做法是直接用 Compose 一次性创建整个网络和全部服务:
version: "3.8" services: mysql: image: mysql:8.0 container_name: mysql environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: appdb command: - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci ports: - "127.0.0.1:3306:3306" volumes: - mysql-data:/var/lib/mysql networks: app-net: ipv4_address: 172.28.0.10 redis-master: image: redis:7 container_name: redis-master command: redis-server --appendonly yes ports: - "127.0.0.1:6379:6379" networks: app-net: ipv4_address: 172.28.0.11 redis-slave: image: redis:7 container_name: redis-slave command: redis-server --slaveof redis-master 6379 depends_on: - redis-master networks: app-net: ipv4_address: 172.28.0.12 app: image: your-app-image depends_on: - mysql - redis-master environment: DB_URL: jdbc:mysql://mysql:3306/appdb REDIS_HOST: redis-master networks: app-net: {} nginx: image: nginx:1.25 ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf depends_on: - app networks: app-net: {} volumes: mysql-data: networks: app-net: driver: bridge ipam: config: - subnet: 172.28.0.0/16 gateway: 172.28.0.1这个 Compose 文件里有几个细节值得注意。第一个是表明内部服务优先用服务名,比如 redis-slave 的 --slaveof redis-master 6379,不会依赖 IP;app 里配置的 REDIS_HOST 也是 redis-master,查 IP 的工作全部交给 Docker 内置 DNS。第二个是 MySQL 和 Redis 的端口,我只映射到了宿主机回环地址 127.0.0.1,局域网其他机器无法直连,最大程度缩小了暴露面。第三个是 redis-slave 的命名,从 Redis 7 开始官方更推荐 replicaof,但 slaveof 仍然可用,我只是用它来贴合老项目里的习惯写法。
4.3 连通性验证:从 ping 服务名到检查外部访问
整套栈起来之后,验证顺序从内到外一层层做。先看都有哪些容器活着:
docker ps然后进入 app 容器,验证服务名解析和连通性:
docker exec -it app ping -c 2 mysql docker exec -it app ping -c 2 redis-master正常情况下,你会看到 app 容器把 mysql 解析到 172.28.0.10,把 redis-master 解析到 172.28.0.11。这证明自定义网络的 DNS 已经生效。
再验证端口映射链路,从宿主机连 MySQL:
telnet 127.0.0.1 3306如果 telnet 能进入一个空连接界面,说明容器端口映射正常。最后从另一台局域网机器访问宿主机 IP 的 80 端口,确认 Nginx 网关能正常引入流量。这一套走下来,网络链路每一环都验证过,日志里再出现连不上的问题,就基本能排除网络层因素了。
5. 高频踩坑实录:容器网络不通,到底怎么查
5.1 一条命令清单定位九成问题
我这些年排查 Docker 网络问题,发现只要按顺序走下面几步,九成情况都能迅速缩小范围。每次查网络问题,我都会按这个顺序来:
- 在容器里 ping 127.0.0.1,确认容器网络栈本身活着。
- ping 容器所在网络的网关,确认网桥转发正常。
- 从容器 A ping 容器 B 的 IP,确认二层链路。
- 从容器 A ping 容器 B 的服务名,确认 DNS 解析。
- 在宿主机上 telnet 容器 IP 加端口,确认宿主机到容器端口链路。
- 在局域网另一台机器 telnet 宿主机 IP 加映射端口,确认完整链路。
- 最后再查 docker network inspect 和 iptables -t nat -L -n。
这个顺序暗含一个逻辑:从离问题最近的地方开始,逐步往外扩展。很多同学一上来就在第 6 步折腾外部防火墙,结果发现是第 4 步的 DNS 没通,白白浪费半小时。
5.2 Docker Desktop 的几个特有问题
在 Windows 和 macOS 上使用 Docker Desktop 的同学,会遇到 Linux 服务器上完全碰不到的一类怪问题。
第一个高频报错是 virtualizaition support not detected,docker desktop failed to start。这个报错绝大多数是 CPU 虚拟化没开。Windows 下你要进 BIOS 或者 UEFI,把 Intel VT-x 或 AMD SVM 打开,然后再启用 Windows 的 Hyper-V 和 WSL2 功能。我见过很多人装好 Docker Desktop 之后没重启系统就开始排查,其实只差一次彻底重启。macOS 上如果用的是旧款 Intel 芯片,也要确认系统设置里“虚拟化”选项没有被关掉。
第二个高频报错是 failed to connect to the docker api at npipe:////./pipe/dockerdesktoplinuxen。这个常见于 Docker Desktop 刚启动、引擎还没就绪的时候,或者引擎异常退出。我的处理习惯是先完全退出 Docker Desktop,再重新打开,等托盘图标变成绿色后再跑 docker version。如果还不行,去任务管理器把 Docker Desktop 后台进程全部杀掉再重启,通常能恢复。
第三个并不是网络本身的报错,但经常伴随着网络问题一起出现:共享文件夹时提示“输入的文件夹似乎无效”。这个多半是 Windows 共享路径的格式问题。Docker Desktop 设置里添加文件共享时,路径要按 Windows 规范写,比如 C:\Users\you\project,不要手贱改成反斜杠和正斜杠混用。此外还要确认 Windows 的 SMB 功能已经启用,防火墙没有拦掉 445 端口。
这三个问题都属于“环境层故障”,学会了排查思路,比背错误码答案更有用。
5.3 防火墙、网段冲突与 DNS 还原
除了 Docker 本身,环境层面的网络冲突也很要命。最常见的是网段冲突。不少家用路由器默认使用 192.168.1.0/24 或者 172.16.0.0/12,公司内网里也常出现 172.22.0.0/16 这类网段。镜像 Docker 的自定义网络恰好默认落在 172.x.0.0/16 的范围内,两边一撞,容器访问内网路由就会出现“一会儿通一会儿不通”的玄学现象。解决办法很简单:docker network create 时显式指定一个不冲突的网段,比如 172.28.0.0/16,或者 10.88.0.0/16,并保证物理网络中没有相同路由。
另一个常见问题是 DNS。Linux 虚拟机或者物理机改了 /etc/resolv.conf 之后,一旦重启网络服务,配置会被还原,容器里的 DNS 也会跟着受影响。这是因为 Docker 守护进程会读取宿主机的 resolv.conf 作为容器内 DNS 解析的默认上游。遇到容器能 ping IP 但 ping 不通域名时,第一反应不是骂运营商,而是先看下这个文件:
cat /etc/resolv.conf如果想避免宿主机改 DNS 导致容器解析异常,可以在 docker run 时加上 --dns 参数,或者 Compose 里写 dns: - 8.8.8.8 - 223.5.5.5,让容器绕开宿主机的 DNS 配置变化。
5.4 防火墙重启后端口映射失效的经典坑
还有一个我印象极深的坑:宿主机防火墙服务一重启,Docker 的端口映射就失效了。原因是某些防火墙会把 iptables 规则整个清空重建,而 Docker 不会每时每刻去检查 NAT 表是否需要恢复。遇到这种问题,一条命令常常直接解决:
systemctl restart docker重启 Docker 守护进程会让它重新同步一遍 iptables 规则。如果不想重启 Docker,也可以手动恢复对应的 DNAT/MASQUERADE 规则,但操作复杂很多。我自己的经验是,防火墙规则批量操作前先备份一套,真的出问题就重启 Docker 解决,别硬手动修 NAT 表,容易越改越乱。
6. 给容器网络做一次“体检”并减少暴露面
6.1 用 iperf3 给容器做网络测速
容器网络性能到底怎么样,不能靠“感觉”。我一般在容器里跑一个 iperf3 服务端,宿主机上跑客户端,测一次 TCP 带宽。步骤很简单:
docker run -d -p 5201:5201 --name iperf-server networkstatic/iperf3 -s iperf3 -c 127.0.0.1 -p 5201 -t 10输出结果里最需要关注的是 Transfer 和 Bandwidth 两列。bridge 网络下,单线程吞吐通常会比纯物理网卡低一些,原因是多了一层 veth 转换和网桥转发,但一般不会低到离谱。如果测试数据明显低得异常,先怀疑是不是测试走的是 Docker Desktop 的虚拟机 NAT,再检查是否撞上了 CPU 单核瓶颈。我这里遇到过一次容器网络吞吐上不去,最后发现是镜像里没有启用多核 iperf3 参数,加 -P 4 打开并行流后带宽立刻翻倍。
所以做网络测速不要只看一个数值,一定要做单线程和多线程对比。单线程代表的是“单条 TCP 连接上限”,多线程代表的是“整体可用容量”,两者在真实业务里含义不同。
6.2 最小暴露原则:别把容器端口随便打到宿主机
最后聊聊生产环境里我会怎么控制网络暴露面。很多同学部署完项目后,喜欢把 MySQL 3306、Redis 6379、后端 8080 全用 -p 打到宿主机上,图的是排查方便。但这个习惯在正式环境里挺危险的。
正确思路是分层:对外统一入口只保留 Nginx 或别的网关服务,业务应用只加入自定义网络,数据库和缓存干脆不映射到宿主机,让其他容器通过服务名访问。如果你在 Compose 里非要映射数据库端口给运维临时用,也请只绑定 127.0.0.1。这样即便应用层被人攻破,数据库和缓存的直接暴露面也被压缩到了最小。
Docker 网络还有一种“出网方向”的隔离手段:创建 internal 网络。带 --internal 标记的网络不给容器配置默认网关,容器只能和同网络内的服务通信,无法访问外网。需要把某些数据层服务彻底锁死在内部时,我会单独建一个 internal 网络来放这些服务。注意,internal 网络不能同时带端口映射到宿主机,因为外网链路根本不通,使用时要想清楚场景。
7. 写在最后:从一个反复踩坑的人的角度给你三条建议
到现在我回头看,早期折腾 Docker 网络时踩过的坑,十有八九不是命令记不住,而是没把数据流的路径放在心里。先确认容器在哪个网络,再查网关、查 DNS、查端口映射,一整套排查下来基本都能落地。如果你刚接触 Docker,别一上来就研究跨主机和集群网络,先把单机自定义网络里的服务名互访和固定 IP 玩熟练,大部分微服务项目已经够用了。
最后给你留三个我觉得最值得养成的习惯:第一,数据库、缓存这类中间件容器,端口只映射回环地址或者干脆不映射;第二,写 Compose 文件时总是显式声明网络,不要隐式依赖默认网络;第三,遇到网络不通先按第 5 节的那条命令清单走一遍,不要先急着重启容器。这三个习惯帮我省下了非常多“看起来死活用不了”的排查时间。等以后你开始接触 K8s,回过头来会发现很多网络概念在 Docker 阶段已经埋下了伏笔。