Docker网络配置详解:从命名空间、网桥到端口映射与排查
1. 先把 Docker 网络到底在管什么捋清楚
很多人第一次碰 Docker 网络,基本都栽在同一个场景里:容器起来了,docker ps看着也正常,可就是连不上、解析不了、外面访问不进来。我在最开始用的时候也一样,以为是“容器自带网络”就万事大吉,结果一遇到多容器协作就得翻文档。写 Docker 网络配置 network 详解这件事,其实最该先讲清楚的不是命令,而是它到底在你机器上动了几层东西。
一句话概括:Docker 网络本质是在宿主机上帮你批量管理网络命名空间和转发规则。容器不是“虚拟小机器”,它只是共享宿主机内核的一组进程,被塞进了一个独立的 network namespace 里,再通过一对虚拟网卡接到宿主机上的网桥上。你敲下的docker network create,做的事情就是:建一个网桥、分一个网段、配一套 DNS、写几条转发规则。
这篇文章我打算把我会的那些东西按顺序摊开讲:基础三件套(命名空间、veth、网桥)是怎么回事、默认 bridge 为什么坑、自定义网络怎么规划、端口映射到底走了哪几条链路、host/none/container 三种模式什么时候该用、跨主机和 MTU 这类进阶问题、最后给一份排查速查表。
适合谁看?如果你已经能跑起单个容器,但一碰到“容器之间怎么通信”“为什么宿主机 curl 不通”“为什么改了端口不生效”就卡壳,那这篇基本能覆盖八成的日常场景。不需要你事先精通网络,但要能看懂ip addr和iptables -L这类输出。
我先把所有实操的前提定死:下面涉及的所有命令,默认在 Linux 环境的 Docker Engine 上执行,版本 20.10 以上。桌面版(Docker Desktop)在网络实现上有额外一层转发,路径不太一样,遇到差异我会单独标出来。
2. 三件套:命名空间、veth、网桥
2.1 容器的网络隔离靠的是 network namespace
Linux 的 namespace 机制是容器的地基,network namespace 负责隔离网络栈——每个 netns 里都有自己的网卡列表、路由表、ARP 表、iptables 规则和端口空间。这是为什么你可以在同一台机器上跑十个 nginx 容器都监听 80 端口:它们的 netns 是独立的,各自的 80 端口互不冲突。
你可以直接把容器的 netns 当作宿主机上的一个“目录”来看:
ls /var/run/docker/netns/这里列出来的每个带随机串的文件,就是一个运行中容器的网络命名空间句柄。认不出谁是谁很正常,可以通过docker inspect拿到 PID,再做 PID 到 netns 的映射。
切换到某个容器的 netns 里看它的网卡,我现在还是习惯用nsenter,因为它不用在容器里装任何工具:
PID=$(docker inspect -f '{{.State.Pid}}' web) nsenter -t $PID -n ip addr nsenter -t $PID -n ip route这比docker exec强的地方在于:很多精简镜像里连ip、ping、ss都没有,docker exec进去什么都干不了,而nsenter用的是宿主机上的工具集。这个习惯我强烈建议尽早养成。
2.2 veth pair 就是那根“两头通的网线”
容器有了 netns 还不够,它得跟外界连起来。Docker 用的手段是veth pair——一对虚拟网卡,像一根网线,一头插在容器的 netns 里(容器内看到的名字通常是eth0),另一头插在宿主机的 netns 里(名字形如vetha1b2c3)。
在宿主机上执行ip link show type veth,能看到一串veth开头的接口,每个后面带着master docker0之类的标记。这一头不配 IP 地址,只是作为网桥的一个“端口”存在。如果自定义网络下有多张网卡,容器内的名称会变成eth0、eth1按顺序分配。
这层理解的价值在于:当 veth 这一头被误删、或者网桥被卸载,容器里的eth0就变成一根断了的线,表现为network is unreachable。我曾遇到过宿主机上有人清 iptables 顺手把docker0相关规则删干净的情况,症状就是容器内网卡还在、IP 还在,但什么都出不去——因为问题不在网卡,在转发。
2.3 网桥是宿主机侧的“交换机”
docker0就是最典型的那个网桥,默认存在,地址一般是172.17.0.1/16。你可以把它理解成一台二层交换机:所有接入它的 veth 端口处在同一个广播域里,IP 层再通过内核的转发能力接入宿主机路由表。
用下面的命令看它的真实状态:
ip -d link show docker0 brctl show docker0 # 老系统用这个 bridge link show # 新系统推荐 ip route | grep docker0能看到的关键信息有三个:网桥自己的 MAC 和 IP、挂在上面的 veth 列表、以及宿主机路由表里指向docker0的那条路由。默认 bridge 下,宿主机路由表会有一条172.17.0.0/16 dev docker0 proto kernel scope link src 172.17.0.1,这条决定了宿主机怎么把包丢进容器的网段。
注意:网桥模式下,同一网段的容器之间流量其实是二层转发,不经过宿主机 iptables 的 FORWARD 链吗?不是的——桥接流量默认要过
FORWARD链,早期版本要开net.bridge.bridge-nf-call-iptables,现在 Docker 安装时会自己写好。如果你手动关掉了这个参数,容器间通信会出现“ping 得通 ARP 但上层不通”的怪现象。
2.4 一次完整的外网访问,包是怎么走的
把这条路径讲清楚,后面的排查就有地图了。以容器访问一个公网 IP 为例:
| 步骤 | 发生了什么 | 对应检查点 |
|---|---|---|
| 1 | 容器内路由表有default via 172.17.0.1,包发往网关 | 容器内ip route |
| 2 | 包经 veth 对端进入宿主机,命中FORWARD链 | iptables -L FORWARD -v |
| 3 | 源地址在POSTROUTING被 MASQUERADE 成宿主机 IP | iptables -t nat -L POSTROUTING |
| 4 | 宿主机按路由表发出,走物理网卡 | ip route get 8.8.8.8 |
| 5 | 回包按 conntrack 表反向还原 | conntrack -L |
这里第 3 步是新手最容易忽略的:容器出网用的是 SNAT,靠的是宿主机的 IP。所以你在容器里执行curl ifconfig.me看到的是宿主机的外网地址,不是容器自己的。工程上这意味着:任何基于源 IP 做白名单的服务,看到的都是宿主机 IP,而不是容器 IP——这一点在做网关对接时经常被人踩。
顺便提一个 2024 年之后大家频繁用到的场景:容器里需要访问宿主机上跑的服务。Linux 上不能直接写localhost,得显式加一条 hosts 记录:
docker run -d --add-host=host.docker.internal:host-gateway --name app myapp:v1host-gateway是 Docker 20.10 引入的魔法值,它会自动解析成docker0的网关地址(或者 host 网络下的默认网关)。Windows/macOS 的 Docker Desktop 上这个域名默认就存在,属于平台差异。
3. 默认 bridge 的坑与自定义网络的必要性
3.1 默认 bridge 为什么不能靠容器名互通
这是被问得最多的问题。你在默认 bridge 上启动两个容器,ping web直接报bad address。原因很干脆:默认 bridge 网络没有内嵌 DNS 服务,只有自定义网络才有。
Docker 在内嵌 DNS 上做得很克制:每个自定义网络里,Docker 会在容器侧注入一个 DNS 服务器地址127.0.0.11,这个地址在每个容器的 netns 内部单独存在(监听在容器的 lo 上),所有发往它的查询会被守护进程接管,按容器名、网络别名、--network-alias来应答。
验证方式很直接:
docker run -d --name db --network mynet postgres:16 docker run -it --rm --network mynet alpine sh # 容器内 cat /etc/resolv.conf # nameserver 127.0.0.11 nslookup db ping db而在默认 bridge 里,/etc/resolv.conf指向的是宿主机的 DNS,db这个名字在大海捞针里查不到,自然失败。历史上有个--link的机制能补这件事,但它只支持单向依赖、维护成本高、官方早就标记为 legacy,新项目别用了。
提示:自定义网络里,容器名解析只在同一个网络中生效。
mynet里的容器访问othernet里的容器,得用docker network connect把两边拉进同一个网络,或者用别名双挂。这是隔离带来的代价,不是什么 bug。
3.2 建一张自定义网络的实操顺序
我的习惯是:先规划网段,再建网络,最后挂容器,不反过来。因为一旦网络建好、容器挂上去,再改 subnet 就得删网重建。
docker network create \ --driver bridge \ --subnet 172.20.0.0/24 \ --gateway 172.20.0.1 \ --opt com.docker.network.bridge.name=br-app \ app_net几个参数的意义值得逐条说:
--subnet:给这张网络划的地址段。不写的话 Docker 会自动从172.17之后的空间里挑一段(通常是172.18、172.19递增),容易和你自己的内网冲突,所以我基本都手动指定。--gateway:网桥的 IP,必须是 subnet 里可用的地址,默认取第一个。--opt com.docker.network.bridge.name:给宿主机上的网桥起个人能看懂的名字。默认是br-加一段随机十六进制,排查时看到br-9f3a2c1d你根本不知道对应哪个业务。
--opt com.docker.network.bridge.name这个参数在我看来是必选项,不是可选项。它带来的排查效率提升非常直接:ip link一眼就能对上业务。
再补一个容易被忽略但很实用的参数--ip-range。当你不想让一张网络占用整个大段时:
docker network create \ --subnet 172.20.0.0/16 \ --ip-range 172.20.10.0/24 \ --gateway 172.20.10.1 \ big_net这样对外声称是 /16,实际分配只在172.20.10.0/24里,避免和真实内网的172.20.0.0/24抢地址。这种场景在混合云、多网卡宿主机上经常遇到,尤其是宿主机还接了公司办公网的时候。
3.3 网段规划我踩过的那些坑
分享几条纯经验,文档里一般不会写:
- 避开
172.17.0.0/16。这是docker0的地盘,你手动建网络写172.17.1.0/24会直接冲突甚至建不起来。我一般从172.20开始往后用,或者干脆换到10.88.0.0/16这种私网段。 - 别用
192.168.0.0/24、192.168.1.0/24这种家庭路由器的默认段。开发机上用没事,但一批机器下发 compose 配置时就等着冲突吧。 - 一台机器上网络数量控制在十几张以内。每张自定义 bridge 都是宿主机上一块网桥,会占路由表条目,太多之后
ip route的输出会很长,排查很痛苦。 - 容器 IP 不要写死在应用配置里。除非你用了
--ip显式指定静态地址,否则容器重启后 IP 可能变,靠 IP 通信迟早出事。用容器名或网络别名。
4. 端口映射的完整链路
4.1-p的几种写法,语义差别很大
-p这块我觉得值得单独拉出来讲,因为它的写法和含义不完全直观:
| 写法 | 含义 | 典型用途 |
|---|---|---|
-p 8080:80 | 宿主机所有网卡的 8080 → 容器 80 | 内网服务对外开放 |
-p 127.0.0.1:8080:80 | 只绑宿主机回环 | 本机调试、避免误暴露 |
-p 8080:80/udp | UDP 协议映射 | DNS、游戏服 |
-p 8000-8010:8000-8010 | 按区间映射 | 批量服务 |
-p 8080:80 -p 8443:443 | 多端口并列 | 常规 Web |
-p 80 | 宿主随机端口 → 容器 80 | 避免端口冲突 |
最有价值的是第 2 条。默认-p 8080:80是监听0.0.0.0的,只要宿主机有公网 IP,这个端口就直接对公网开放。我在做内部服务的时候一律写127.0.0.1:前缀,需要外部访问时前面挂一个反向代理,让代理去处理 TLS 和鉴权。这个习惯让我少踩了很多“数据库端口暴露到公网”的坑。
注意:写
-p 8080:80的时候,容器内部必须真的监听0.0.0.0:80。如果你的应用只绑了127.0.0.1:80,端口映射会“看起来生效但连不上”,外面curl直接 connection reset。这个问题出在应用自身的监听地址上,不在 Docker 网络。我排查过至少三次,全都是应用配置写死了localhost作为监听地址。
4.2 DNAT 和 docker-proxy 到底谁在干活
端口映射的实际实现有两条路径同时存在:
路径一:iptables DNAT。在nat表的DOCKER链里,能看到类似这样的规则:
iptables -t nat -L DOCKER -n # DNAT tcp dpt:8080 to:172.20.0.2:80外来的包到达宿主机后命中 DNAT,目的地址被改写为容器 IP,再经路由转发进网桥。
路径二:docker-proxy 用户态进程。每映射一个端口,宿主机上就会有一个docker-proxy进程在监听那个端口,把流量转发给容器。默认userland-proxy=true。
这两者同时存在有历史原因:早期 DNAT 方案在宿主机本地访问容器 IP 时会走不通(环回场景的路由问题),用户态代理能兜住这类边界情况。代价是多一层进程拷贝。
能不能关掉?可以,在守护进程配置里"userland-proxy": false。但我建议不要轻易关,除非你在压测中明确测出它是瓶颈。关了之后某些场景(尤其是宿主机访问映射端口 + 容器内部再反向访问)会出诡异问题,排查成本远高于那点性能收益。
查看当前映射关系,最直接的办法是:
docker port web # 80/tcp -> 0.0.0.0:8080 # 80/tcp -> [::]:8080另外,docker-proxy在 IPv6 上也有监听,这是为什么docker port会输出两行。如果宿主机关了 IPv6,第二行可能仍在,但不影响使用。
4.3 用 DOCKER-USER 链做真正的访问控制
如果你需要限制“只有某几个 IP 能访问这个映射端口”,不要改DOCKER链——那几条规则会被 Docker 在重启时重写。正确的位置是DOCKER-USER链,它稳定存在且优先级在 Docker 自动规则之前。
# 先允许内网段,再拒绝其余 iptables -I DOCKER-USER -p tcp --dport 8080 -s 10.0.0.0/8 -j ACCEPT iptables -I DOCKER-USER -p tcp --dport 8080 -j DROP顺序很关键:-I插入到链首,后插入的规则会排在前面,所以上面这两条实际执行后,DROP 反而在上、ACCEPT 在下,结果就是全拒。要写成先-A再-I,或者按反逻辑顺序插入。这个顺序坑我吃过一次,排查了半小时才发现是规则顺序问题。
提示:
DOCKER-USER链的规则在宿主机重启、Docker 服务重启后是否保留,取决于你有没有把规则落盘(iptables-save)。纯手工加的规则丢了就是丢了,生产环境建议用配置管理工具统一下发。
5. host、none、container:三种特殊模式
5.1 host 模式:最简单也最容易踩坑
--network host让容器完全共享宿主机的 netns,容器内看到的网卡、路由、端口空间跟宿主机一模一样。容器里监听 80 就是宿主机监听 80,没有 NAT,没有端口映射,性能最好。
docker run -d --name nginx-host --network host nginx:alpine # 直接访问宿主机的 80 curl -I http://127.0.0.1代价很明确:
- 端口会冲突。两个 host 模式的容器都想监听 80,第二个必然起不来。
-p参数彻底失效。写了也不报错,但一点用没有,我第一次用的时候还傻傻等了半天。- 隔离性下降。容器能看到宿主机所有网卡和路由,恶意容器可以做的破坏更多。
什么场景该用?我总结成三条:宿主机网络代理类服务(需要接管全机流量)、对网络延迟极其敏感的高频交易或游戏服、以及需要绑定特定物理网卡做抓包/监控的工具。除此之外,我基本不用 host 模式。
5.2 none 模式:只有 lo 的“真空环境”
--network none只给容器一个lo环回网卡,其他什么都没有。看起来像没用,实际上有明确的用途:纯计算任务、敏感数据处理、以及需要自己接管网络配置的场景。
docker run --rm -it --network none alpine sh # 里面只有 lo ip addr我在做某些离线的批处理任务时会用这个模式,保证任务绝对不可能有外联行为——某些合规要求下的“断网执行”就是这个思路,比在容器里塞 iptables 规则更可靠,因为连网卡都没有。
5.3 container 模式:共享另一个容器的网络栈
--network container:<容器名或ID>让新容器复用已存在容器的 netns。两个容器共享 IP、端口空间、路由表。
这个模式最经典的用法是sidecar 排查容器:
# 生产容器是精简镜像,没有 curl、tcpdump docker run -it --rm --network container:web nicolaka/netshoot # 在这个容器里直接 curl localhost:80,看到的就是 web 的网络视图另一个用法是 Kubernetes 的 Pod 模型——一个 Pod 里的所有容器共享同一个 netns,就是这么实现的。
注意:用 container 模式时,两个容器的生命周期强绑定。被共享的那个容器停了,共享它的容器网络也跟着完蛋。另外这个模式和
-p互斥,因为端口归属权在被共享的那个容器上。
5.4 一张表把四种模式对比清楚
| 模式 | 独立 netns | 有独立 IP | 支持-p | 支持名称解析 | 适用场景 |
|---|---|---|---|---|---|
| bridge(自定义) | 是 | 是 | 是 | 是 | 绝大多数业务容器 |
| bridge(默认 docker0) | 是 | 是 | 是 | 否 | 单容器临时调试 |
| host | 否 | 否(用宿主机的) | 否 | 否 | 代理、性能敏感 |
| none | 是 | 否 | 否 | 否 | 纯计算、离线任务 |
| container | 否(共享) | 否(共享) | 否 | 否 | sidecar 排查、Pod 模型 |
这张表我贴在自己团队的文档里很久了,新同事进来先看这张表,能省掉一半的提问。
6. 跨主机通信与 MTU 这两个深水区
6.1 overlay 网络解决的是什么问题
bridge 网络只在单机有效。两台宿主机上的容器要互访,靠 bridge 是做不到的——网段是各自独立的,路由也不通。overlay 网络就是给这个场景准备的:它用 VXLAN 把二层帧封装进 UDP 包,让跨主机的容器看起来像在同一个二层网络里。
docker network create \ --driver overlay \ --subnet 10.0.9.0/24 \ --attachable \ cross_net--attachable这个参数很关键:默认 overlay 网络只允许 Swarm service 接入,普通docker run的容器挂不上。加上它之后,独立容器也能连进来。
overlay 的代价是每个包多一次封装解封装,吞吐会掉一截,延迟也会增加。我的经验是:如果只是两个服务在两台机器上通话,用宿主机的物理网络 + 明确的服务地址往往比 overlay 更省事;overlay 的价值在于容器数量多、需要动态扩缩、且希望保留“名字即地址”这种便利的时候。
6.2 macvlan:让容器直接拿物理网段的 IP
还有一种需求是“容器必须有一个和宿主机同网段的 IP,看起来像局域网里的一台独立设备”。这种时候 macvlan 登场:
docker network create -d macvlan \ --subnet=192.168.10.0/24 \ --gateway=192.168.10.1 \ -o parent=eth0 \ mac_net每个容器会拿到一个192.168.10.x的地址,外部设备可以直接访问它,不需要端口映射。这在迁移老系统、需要固定 IP 注册到某台设备上的场景非常好用。
但 macvlan 有一个广为人知的坑:宿主机默认无法直接访问自己 macvlan 容器。因为 macvlan 的流量被设计成“只能出去、不能折回宿主机”,需要额外在宿主机上建一个 macvlan 子接口才能打通。我第一次遇到时以为是防火墙问题,查了很久。
另一个前提条件是物理网卡或交换机必须允许混杂模式,很多云厂商的宿主机不满足,创建成功但容器完全不通。所以用之前先确认环境。
如果 macvlan 不可用,ipvlan是备选,它只共享 IP 层不共享 MAC,对交换机的要求低一些。
6.3 MTU:最隐蔽又最常见的问题
MTU 导致的问题特别恶心,因为它的症状是“小请求正常,大请求卡死”。典型表现:容器里curl一个首页正常,但拉一个大文件卡住不动;ping通,ssh能连但一传数据就挂。
原因在于封装层增加了开销:
| 网络类型 | 建议 MTU | 说明 |
|---|---|---|
| 物理网卡默认 | 1500 | 以太网标准 |
| 默认 bridge | 1500 | 无封装 |
| overlay(VXLAN) | 1450 | VXLAN 头 50 字节 |
| 跨公网隧道 | 1400 或更低 | 再留余量 |
在自定义网络里显式指定:
docker network create \ --opt com.docker.network.driver.mtu=1400 \ mtu_net改完之后一定要验证:
# 在容器里执行,探测路径 MTU ping -M do -s 1372 8.8.8.81372 + 28(IP 头 20 + ICMP 头 8)刚好 1400。如果这个能通、-s 1400不通,说明 MTU 设置生效了。
我个人的经验值:只要是跨主机、跨云、或者中间有隧道,一律先设 1400 试。宁可损失一点吞吐,也别让应用出现“偶发卡死”这种最难排查的故障。
7. 排查实录与常见问题速查
7.1 我的标准排查顺序
遇到网络问题,我固定按这个顺序走,基本不会绕远路:
- 看容器状态和内网 IP:
docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' web - 进 netns 看网卡和路由:
nsenter -t $PID -n ip addr && ip route - 测网关连通:容器内
ping 172.20.0.1,不通就是二层或网桥问题 - 测名称解析:
nslookup db,失败先看/etc/resolv.conf是不是127.0.0.11 - 测出网:
curl -v到一个已知 IP,排除 DNS 干扰 - 查宿主机转发:
iptables -L FORWARD -v有没有 DROP、ip_forward是不是 1 - 查端口映射:
iptables -t nat -L DOCKER -n、ss -lntp | grep 8080
第 6 步的ip_forward值得单独说:
sysctl net.ipv4.ip_forward # 必须是 1有些加固过的系统默认关掉转发,Docker 启动时通常会自己开,但如果被安全策略覆盖过,就会出现“容器内能 ping 通网关、ping 不通外网”的经典症状。
7.2 常见问题速查表
| 现象 | 大概率原因 | 处理方式 |
|---|---|---|
| 容器间 ping 容器名失败 | 用了默认 bridge,无内嵌 DNS | 改挂自定义网络 |
| 容器能出网,宿主机访问映射端口不通 | -p绑了127.0.0.1而访问走了别的 IP | 检查访问地址与绑定地址是否一致 |
| 宿主机本地 curl 通,外部不通 | 防火墙/安全组拦了 | 查宿主机防火墙与云安全组 |
| 大请求卡死,小请求正常 | MTU 不匹配 | 把网络 MTU 降到 1400 |
| 两个容器端口冲突 | 都用了 host 模式 | 改 bridge + 端口映射 |
| 容器内解析正常,宿主机解析不了容器名 | 内嵌 DNS 只在容器内可用 | 宿主机走映射端口或/etc/hosts |
| 重启后容器 IP 变了 | 未固定 IP | 用容器名通信,或--ip指定 |
docker network connect后仍不通 | 目标容器只连了一个网络 | 确认双方在同一网络或有路由 |
7.3 几条不太写在文档里的实操心得
关于清理。docker network prune会删掉所有没被容器使用的自定义网络。这个命令在有 CI 频繁建网的机器上很好用,但要注意它不会动默认的bridge、host、none。我有一次在共享开发机上跑它,把同事正在用的测试网络删了——所以还是先docker network ls看一眼再说。
关于“一服务一网络”。我在稍微复杂一点的项目里会按业务域分网络:front_net给接入层和 API 层,back_net给 API 层和数据库。API 容器双挂两张网,前端只能看到front_net。这样即使前端容器被攻破,也摸不到数据库。这种隔离在 compose 里写起来很清爽:
services: api: networks: [front_net, back_net] db: networks: [back_net] web: networks: [front_net] networks: front_net: back_net:关于别名。--network-alias是我用得最多的一个参数,比容器名好用。因为容器名带环境后缀(api-prod-1),而别名可以固定成api,应用配置里永远写api:8080,换环境不用改代码。
docker run -d --name api-prod-1 --network app_net --network-alias api myapi:v1关于容器内工具的缺失。排查网络问题最痛苦的是进容器发现没有curl、没有ping、没有ss。我的做法是永远不往业务镜像里加调试工具,而是用--network container:<目标>起一个 netshoot 容器共享网络栈。这样业务镜像保持干净,排查工具随用随起。
关于 Docker Desktop 的差异。如果你在 macOS/Windows 上跑,容器其实运行在一层轻量虚拟机里,docker0不在你的宿主系统上,nsenter、iptables那些命令查不到东西。那个环境下排查网络问题应该用docker run --rm --net=host alpine ip addr之类的命令,把视角放进虚拟机里。这个认知差异坑过不少人。
关于 IPv6。如果宿主环境需要 IPv6,建网络时加--ipv6 --subnet fd00::/64。但要留意端口映射默认只处理 IPv4,需要在守护进程里打开"ipv6": true并配置fixed-cidr-v6。这块我建议只在确有需求时开启,双栈配置出问题的排查成本比纯 IPv4 高不少。
8. 一套可以直接抄的落地配置
把前面散落的东西拼成一个能用的方案。假设场景是:一个 Web 服务、一个 API 服务、一个数据库,跑在一台宿主机上,只允许 Web 对外开放。
# 1. 建两张网络,明确网段和网桥名,避免默认 docker0 冲突 docker network create --driver bridge \ --subnet 172.20.0.0/24 --gateway 172.20.0.1 \ --opt com.docker.network.bridge.name=br-front front_net docker network create --driver bridge \ --subnet 172.21.0.0/24 --gateway 172.21.0.1 \ --opt com.docker.network.bridge.name=br-back back_net # 2. 数据库只挂 back_net,不暴露任何端口 docker run -d --name db --network back_net \ --network-alias db \ -v /data/pg:/var/lib/postgresql/data \ postgres:16 # 3. API 双挂,只绑回环对外暴露 docker run -d --name api --network front_net \ --network-alias api \ -p 127.0.0.1:8080:8080 \ myapi:v1 docker network connect back_net api # 4. Web 只挂 front_net,走 80 对外 docker run -d --name web --network front_net \ -p 80:80 \ -e BACKEND=http://api:8080 \ myweb:v1这套配置里有几个设计意图值得说明:
- 数据库完全不映射端口,连宿主机的
127.0.0.1都不绑。需要连库的时候用docker exec -it db psql,或者临时起一个带端口的调试容器。 - API 的
-p前缀明确写了127.0.0.1,因为它不需要公网直连,所有请求都从 Web 转发过来。这样即使误配了安全组,API 也不会暴露。 - API 用
docker network connect做双挂,而不是在一开始就--network两次。这个顺序有个好处:先建好 front 上的别名,再接入 back,避免接入过程中别名解析出现短暂异常。 - 所有服务间通信全靠网络别名,没有任何一处写死 IP。
验证的话,从 Web 容器里打:
docker exec -it web sh -c "nslookup api && wget -qO- http://api:8080/health"从宿主机打:
curl -I http://127.0.0.1:8080/health # 应该通 curl -I http://<宿主机公网IP>:8080 # 应该不通这两条一正一反,是我每次上线前的固定动作。第二条不通才算配置正确,很多人只测第一条,测完就以为稳了。
最后再分享一个我用了很久的小技巧:把网络的元信息直接写进 compose 的labels里,比如负责人、业务域名、网段用途。等到半年后回来看这张网络的br-a1b2c3d4时,docker network inspect一打开就知道它是干嘛的,不用再靠猜。这个习惯的成本几乎为零,但省下来的时间是真金白银。