Docker网络配置与排错:端口映射、自定义网络与容器互访 刚上手 Docker 的人几乎都会撞上同一堵墙docker run敲下去容器状态是 Up日志干干净净可宿主机浏览器就是打不开那个页面或者反过来容器里ping得通 IP 却死活解析不了域名。这时候翻文档看到 bridge、host、none、overlay 一长串名词越看越糊。Docker 网络配置本身并不复杂复杂的是它把 Linux 网络命名空间、虚拟网桥、iptables 转发这几层平时藏在系统底层的东西一次性摊到了你面前。这篇笔记就是把我自己在配置 Docker 网络过程中反复踩过的坑、验证过的命令、以及那些文档里一笔带过但实际会卡住人的细节按顺序整理出来。内容偏向入门与实战排错适合刚接触容器、或者已经能跑起容器但一涉及容器互访和端口映射就发怵的朋友。读完你应该能自己判断这个场景该用哪种网络模式、端口为什么没通、容器之间为什么找不到对方。1. Docker 网络到底在解决什么问题1.1 从一个容器起来了却访问不到的现象说起最常见的场景是这样你照着教程跑了一个 Nginx 容器docker ps显示端口那一栏写着80/tcp但后面没有0.0.0.0:8080-80/tcp这样的映射信息你在宿主机浏览器输入localhost什么都没有。很多人第一反应是容器坏了于是docker logs看一遍、docker restart来一遍问题依旧。真正的原因是容器默认跑在自己的网络命名空间里它有自己的网卡、自己的 IP、自己的路由表和宿主机是隔离的。你没有做端口映射就等于这个容器只在 Docker 内部那张虚拟网络里露了个头外部的世界根本看不见它。80/tcp那行只是镜像构建时EXPOSE声明的元信息它不代表任何一个真实监听在宿主机上的端口。理解这一点后面所有的配置动作就都有了统一的解释框架要么你把外部流量接进容器端口映射要么你把容器抬到宿主机的网络栈上host 模式要么你干脆建一张容器们自己能互相认识的网自定义网络。1.2 容器网络和虚拟机网络的根本差别习惯用虚拟机的人容易把两者混为一谈。虚拟机是完整的操作系统它通过虚拟网卡接到 VMware 或 VirtualBox 提供的虚拟交换机上通常有独立的 IP宿主机和虚拟机之间是两台机器的关系。容器不是容器本质上是宿主机上的一个进程只不过被塞进了一个独立的网络命名空间里。这个差别带来了两个直接后果。第一容器不需要完整网络栈的启动开销所以 Docker 能在毫秒级给容器配好网络。第二容器的网络配置全部由 Docker 守护进程代劳你看到的docker0网桥、veth虚拟网卡对、iptables 里的DOCKER链都是它自动生成的。这也意味着当你不理解 Docker 帮你做了什么时你手工改的配置很容易被它覆盖或冲突——这是后面讲防火墙时最容易踩的坑。1.3 一次 run 背后 Docker 做了哪些网络动作当你在默认模式下docker run一个容器Docker 大致做了四件事创建一个新的网络命名空间容器进程在里面运行。创建一对veth虚拟网卡一端放进容器命名空间通常叫eth0另一端挂在宿主机的docker0网桥上。从docker0所在的子网默认172.17.0.0/16里给容器分配一个 IP并设置默认网关指向docker0的地址默认172.17.0.1。在宿主机 iptables 的nat表里写入 MASQUERADE 规则让容器发出的包在离开宿主机时被改写成宿主机 IP从而实现容器访问外网。知道这四步之后很多玄学问题就不玄学了。容器访问不了外网先看第 4 步的转发规则在不在。容器之间互相 ping 不通先看它们是不是挂在同一张桥上。2. 五种出厂网络模式的适用边界docker network ls一执行你会看到 bridge、host、none 三个默认网络。加上container模式和overlay日常够用的就是这五种。选哪种不是看哪个高级而是看你要解决的具体问题。2.1 bridge默认模式的加成与代价bridge 是默认值也是绝大多数场景的起点。它给每个容器分配独立 IP通过 NAT 与外部通信。好处是隔离清晰、可以精细控制端口、容器之间不会因为端口冲突打架——两个容器都监听 80 端口完全没问题因为它们在不同的网络命名空间里。代价也很明确多了一层地址转换外部要访问容器必须显式做端口映射容器的 IP 在重建后会变所以不要在任何地方硬编码容器 IP这是新手最容易犯的错之一。我见过有人在配置文件里写死172.17.0.3容器一重建整个服务就失联了。2.2 host去掉一层转换的代价host 模式下容器直接使用宿主机的网络命名空间不再有独立 IP-p参数直接失效容器监听 8080 就是宿主机监听 8080。它的优势是网络性能损失最小、不用做端口映射适合对网络延迟敏感或者需要绑定大量端口的场景。但它的代价往往被低估容器不再有端口隔离。如果宿主机上已经有一个进程占了 80容器里的服务就起不来。而且容器直接暴露在宿主机网络里安全性上少了一层缓冲。有一个细节必须说清楚在 Linux 上 host 模式名副其实而在 Mac 和 Windows 上Docker Desktop 是跑在一个轻量虚拟机里的此时 host 模式指的是那个虚拟机的网络不是你的笔记本本身。这是很多人困惑为什么 host 模式还是不通的根源。2.3 none 与 container两个极端none模式给容器一个完全空白的网络栈只有 lo 回环。它适合两类场景一是纯计算任务压根不需要网络二是你想手工接管网络配置用它当一张白纸。我第一次用它是在测试一个只做本地文件解析的批处理容器配上 none 之后再也不用担心它偷偷往外发请求。container模式则是让新容器复用另一个已存在容器的网络命名空间。两个容器共享 IP、端口和网卡通信走localhost。典型用途是排错新起一个带tcpdump或curl的容器--network container:目标容器直接站在目标的网络视角里看问题不需要改动目标容器一个字节。2.4 把默认网络的结构看穿入门阶段最值得养成的习惯是遇到网络问题先执行这两条命令docker network ls docker network inspect bridge第二条会输出Containers字段里面列着当前挂在 bridge 上的容器、各自的 IPv4 地址和 MAC。这一步的价值在于它把看不见的网络变成了可读的清单。你还在猜容器 IP 是多少不如直接 inspect 出来。再往下挖一层在 Linux 宿主机上执行ip addr show docker0你会看到网桥本身的地址通常就是172.17.0.1/16。docker network inspect里的Gateway字段应该和它一致。如果不一致说明有人在daemon.json里改过bip或default-address-pools这种环境往往是公司内网统一规划过的配置时要顺着既有规划走不要随手改。3. 自定义网络容器互访的正确姿势3.1 为什么默认 bridge 上的容器不能靠名字互访这是新手最大的疑惑来源明明两个容器都在 bridge 上为什么 A 里ping db不通只能ping 172.17.0.x原因是默认 bridge 网络不提供自动的容器名 DNS 解析。那个年代大家用--link参数硬塞 hosts 记录而--link早就被标记为遗留特性官方建议用自定义网络替代。自定义网络则内置了一个 DNS 解析器监听在127.0.0.11。你在同一个自定义网络里创建容器时给的名字会自动成为可解析的主机名。这个差别是能用和好用的分水岭。3.2 建网、接网、断网的完整命令链日常用得最多的几条命令我按使用顺序列一下# 建一张自定义 bridge 网络指定子网和网关 docker network create \ --driver bridge \ --subnet 172.20.0.0/16 \ --gateway 172.20.0.1 \ mynet # 启动时就接入 docker run -d --name web --network mynet nginx # 运行中的容器临时接进来 docker network connect mynet 已有容器名 # 把容器从网络里摘出去 docker network disconnect mynet web # 清理没有容器引用的网络 docker network prune这里有一个实用经验显式指定--subnet比让 Docker 自动分配更省心。Docker 默认会从172.17.0.0/16往后找空闲网段容器一多网段可能和你公司内网、或者家里路由器的网段撞上撞上之后会出现访问某个内网地址失败的诡异现象。提前规划成172.20.x.x这类不常见网段能省掉很多排查时间。3.3 解析机制与容器重启后的行为自定义网络里的 DNS 解析是按容器名和网络别名走的。容器重启后 IP 可能变化但名字不变所以只要你的配置文件里写的是服务名而不是 IP重建容器不会破坏依赖关系。这正是 Compose 编排能工作的基础。有一点需要留意别名只在同一张网络内有效。如果 A 接在net1B 接在net2即使两个网络都能上外网A 也解析不到 B 的名字。一个容器可以同时接多张网络接几张就能被几张网络里的容器用名字找到。docker network connect net2 web这条命令执行后web会多出一张网卡和另一个 IP它在两张网络里都能被解析到。这个技巧在多服务协同的场景里非常好用比如让一个监控容器同时接入业务网络和存储网络。3.4 用别名做多名字与平滑切换--network-alias允许一个容器在同一张网络里拥有多个名字。这看似是个小功能实际价值不小。比如数据库迁移期间你把新库起名为db-new同时挂上别名db应用配置完全不用改确认没问题后把旧容器停掉别名自然由新容器承接。docker run -d --name db-new \ --network mynet \ --network-alias db \ mysql:8.0需要注意的是别名的唯一性如果同一张网络里有两个容器都声明了同一个别名解析结果会在这两个之间轮询行为类似负载均衡。这个特性可以被有意利用也可能成为为什么请求时好时坏的元凶。排查这类问题时先 hint 自己检查一下别名的重复情况。4. 端口映射背后的三方博弈4.1 EXPOSE 只是声明-p 才是真绑定这两个概念经常被混淆值得单独拆开讲。Dockerfile 里的EXPOSE 80只是往镜像元数据里写一行备注告诉别人这个镜像预期在 80 端口提供服务它不会在宿主机上开任何端口。真正把端口暴露出去的是运行时的-p。-p的完整语法是-p 宿主IP:宿主端口:容器端口/协议省略宿主 IP 就默认监听所有地址省略宿主端口就随机分配一个。举几个实际写法写法含义适用场景-p 8080:80宿主机所有网卡的 8080 转发到容器 80常规对外服务-p 127.0.0.1:8080:80仅本机回环可访问内部管理界面、调试接口-p 80:80/udp指定 UDP 协议DNS、部分游戏服务-P按镜像 EXPOSE 随机映射临时验证方便但不适合长期4.2 绑定地址的选择直接关系安全-p 8080:80和-p 127.0.0.1:8080:80看起来只差几个字符风险差别却很大。前者等价于监听0.0.0.0同一个局域网里的任何机器都能访问后者只有宿主机自己能访问。对于数据库、缓存、管理后台这类不该对外露面的服务我强烈建议只绑回环地址业务服务通过自定义网络直连容器端口根本不需要走宿主机端口映射。这样既减少了暴露面也少了一次 NAT 转发。能不通宿主机端口就不通这是我配置容器网络时的一条基本原则。4.3 端口冲突与防火墙的两层拦截端口映射失败原因通常分两类。第一类是端口被占。表现是容器启动时报bind: address already in use。用ss -lntp | grep 8080就能定位是谁占的。注意一个反直觉的点占端口的可能不是别的进程而是你自己之前启动后忘了删的容器。docker ps -a看一眼比什么排查都快。第二类更隐蔽端口映射规则写对了宿主机本机curl localhost:8080也通但局域网里另一台机器访问不了。这通常是宿主机防火墙在拦。在 CentOS、Rocky 这类默认启用 firewalld 的系统上Docker 会往 iptables 里插入自己的规则链和 firewalld 管理的规则可能互相覆盖。遇到这种情况不建议粗暴地关掉防火墙比较稳妥的做法是把需要放行的端口用firewall-cmd --add-port显式加入放行列表再--reload。如果希望精细控制容器对外访问可以在DOCKER-USER链里加规则。这个链是 Docker 专门留给用户自定义的不会被 Docker 自己的规则覆盖是长期可维护的做法。5. 容器访问外网与宿主机的地址问题5.1 域名解析失败的排查顺序容器里出现Could not resolve host时按这个顺序查效率最高进容器看/etc/resolv.conf确认 nameserver 是不是你期望的值。默认情况下 Docker 会把宿主机的 DNS 配置复制进去但宿主机用的是systemd-resolved时这个复制过程可能拿到一个只在宿主机回环上有效的地址127.0.0.53容器里根本访问不到。用docker run --rm --dns 223.5.5.5 alpine nslookup 你的域名临时验证。如果能解析说明就是默认 DNS 传递有问题。确认没问题后把 DNS 固化到全局配置里避免每次 run 都加参数。{ dns: [223.5.5.5, 119.29.29.29] }改完daemon.json必须重启守护进程才生效systemctl restart docker。这一步经常被忘然后有人抱怨配置没反应。5.2 容器里怎么访问宿主机上的服务这是高频需求数据库装在宿主机上应用跑在容器里容器要连宿主机的 3306。Linux 上从容器访问宿主机的地址不是127.0.0.1——那是容器自己的回环访问到的是容器本身。正确做法是走docker0的网关地址也就是172.17.0.1或者你自定义网络的网关。但这个 IP 同样存在硬编码的问题。更通用的做法是加一条 hosts 映射docker run -d --name app \ --add-hosthost.docker.internal:host-gateway \ myapp:latest之后在容器里用host.docker.internal这个主机名就能指向宿主机。在 Docker DesktopMac / Windows上这个名字是内置的Linux 上需要上面这条参数显式加上。用主机名而不是 IP跨环境迁移时会省事很多。别忘了宿主机上的服务得监听在能被外部访问的地址上。如果 MySQL 只绑了127.0.0.1容器走网关地址照样连不上这不是 Docker 的问题是数据库配置的问题。5.3 转发开关与 MTU 这两个隐藏变量有两个内核层面的设置平时不用管一旦出问题就很难想到。第一个是net.ipv4.ip_forward。容器访问外网依赖 IP 转发正常情况下 Docker 会帮你打开。但如果宿主机上有其他软件或安全策略把它关掉了容器就会表现为能 ping 通网关出不去外网。用sysctl net.ipv4.ip_forward确认值为 1 才正常。第二个是 MTU。当容器与外部通信出现小包能过、大包卡死典型表现是 ping 通但网页打不开、SSH 连上后卡在某个操作时八成是 MTU 不匹配。宿主机网络 MTU 是 1500但某些虚拟化环境或隧道网络下实际可用 MTU 更小容器沿用了 1500 就会分片失败。这时可以在daemon.json里指定mtu: 1400试试。症状对得上再调不要一上来就乱改。6. 排错手册把问题收敛到具体一层6.1 先读状态再动手网络出问题时的第一反应不该是重启而是读状态。三条命令基本能定性# 看容器状态和端口映射是否如预期 docker ps --format table {{.Names}}\t{{.Ports}} # 看容器的网络详情、IP、网关、接入的网络 docker inspect -f {{json .NetworkSettings.Networks}} 容器名 | python3 -m json.tool # 看某张网络上有哪些容器 docker network inspect mynet这三条输出读完绝大多数问题就能归类是没做端口映射、还是接错了网络、还是 IP 分配异常。6.2 分场景对照表我把经常遇到的几类问题整理成了对照表排查时可以直接照方抓药现象大概率原因验证方式宿主机访问不到容器服务没做-p映射docker ps看 Ports 列本机能访问外部机器不能绑定了127.0.0.1或防火墙拦截看-p的宿主 IP 段容器间用名字访问失败不在同一自定义网络docker network inspect对比容器访问不了外网DNS 配置或转发被关sysctl net.ipv4.ip_forward大流量卡死、小请求正常MTU 不匹配ip link对比宿主机 MTU重启后依赖全断配置里写死了容器 IP全局搜索172.开头的地址表格里最后一条值得多说一句。写死 IP 是新手最普遍的习惯也是容器化环境里最脆的假设。容器 IP 是当前这次运行的产物不是配置的一部分。把服务名当作寻址依据才是能穿越重建、扩缩容的做法。6.3 用一次性容器做边界验证有一个技巧我用了很多年效果很好起一个带网络工具的一次性容器接入目标网络直接站在网络内部测试。docker run --rm -it --network mynet nicolaka/netshoot进去之后dig、curl、tcpdump随手可用。它的价值在于把变量拆开了如果这个容器能解析并访问db那问题就不在网络上而在你那个业务容器自己的配置里反之问题就在网络层。一次测试就能把排查范围砍掉一半。比起在业务容器里翻箱倒柜找有没有装ping这种借一个工具箱进来的方式干净得多也不会污染业务镜像。7. 实战一套三层服务的网络落地7.1 规划先行网段、命名与暴露策略假设要跑一套典型服务一个 Web 应用、一个 MySQL、一个 Redis。规划顺序应该是先想清楚三件事哪些服务需要被外部访问哪些只该在内部互相看见网段怎么避开内网冲突我的划分是Web 对外用端口映射MySQL 和 Redis 不对宿主机暴露任何端口只在内网被 Web 访问网段统一用172.21.0.0/16。这样做的直接收益是即使宿主机防火墙配置有疏漏数据库也不会因为映射到0.0.0.0而意外暴露。7.2 网络与服务的声明式定义用 Compose 描述会更清晰因为网络关系是显式写在文件里的不会散落在几十条docker run中services: web: image: myapp:latest ports: - 127.0.0.1:8080:8080 networks: - backend environment: DB_HOST: mysql REDIS_HOST: redis depends_on: - mysql - redis mysql: image: mysql:8.0 networks: backend: aliases: - mysql environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root redis: image: redis:7-alpine networks: - backend networks: backend: driver: bridge ipam: config: - subnet: 172.21.0.0/16这份配置里有两个刻意的设计。第一应用配置里写的是mysql和redis这样的服务名不是 IP重建后依然有效。第二Web 端口只绑到127.0.0.1如果确实需要对局域网开放再明确改成具体的宿主 IP把暴露决定显式化。7.3 上线后的验证清单容器起起来之后别急着打开浏览器。按顺序过一遍这几项能在早期发现大部分隐患docker compose ps确认所有容器状态正常没有反复重启。docker network inspect 项目名_backend确认三个容器都在同一张网络上。进 Web 容器curl -v mysql:3306确认能连通端口探测报协议错是正常的。从宿主机curl 127.0.0.1:8080确认映射生效。从另一台机器尝试访问宿主机的 8080确认防火墙策略符合预期。这里面第三项最容易被跳过但它的价值最高。很多应用启动失败的根因是数据库还没就绪而不是网络不通。depends_on只保证启动顺序不保证服务可用应用自身得有重试逻辑。这一点和网络配置无关却是上线时最常和网络问题混在一起的现象排查时别搞错了方向。最后再分享一个我自己摸索出来的小习惯给每张自定义网络写一句备注。可以是项目 README 里的一行也可以是网络名本身带上前缀比如projA_backend。当宿主机上同时跑着七八套环境时docker network ls输出一长串bridge、myapp_default没有备注根本分不清谁是谁清理网络时手一抖就可能删掉正在用的那张。这个习惯在单机多项目的开发机上尤其救命。