Docker Compose 跨 Stack 复用 Bridge 网络
我有两个部署在同一台主机上的 Compose Stack:一个运行 Caddy,另一个运行博客。博客只需要由 Caddy 反向代理,不应该再把应用端口暴露到宿主机。
最初想到的是把博客端口绑定到 127.0.0.1:
ports:
- "127.0.0.1:12001:3000"这只适合宿主机上的程序。Caddy 运行在另一个容器里,它看到的 127.0.0.1 是自己,不是宿主机。两个容器要直接通信,还是得进入同一个 Docker 网络。
这件事不需要给每个 Stack 再创建一套网络。直接复用其中一个 Stack 已经创建的用户自定义 bridge 即可。
Compose 中的网络名是怎么来的
假设 Caddy 所在 Stack 的项目名是 server,配置里定义了一个叫 server 的网络:
networks:
server:
driver: bridge
services:
caddy:
image: caddy:alpine
container_name: server_caddy
networks:
- serverCompose 默认会给资源名加上项目名前缀,最终创建出来的网络通常叫:
server_server第一个 server 是项目名,第二个 server 是 Compose 文件里的网络键名。不要凭感觉拼这个名字,部署后直接查看:
docker network ls如果配置使用的是隐式 default 网络,对应名称通常是 <项目名>_default。例如项目名为 server,网络就是 server_default。server_default 和 server_server 是两个独立网络,名字相似也不会互通。
让另一个 Stack 加入现有网络
应用 Stack 不再创建自己的网络,而是把 default 指向已经存在的 server_server:
services:
blog:
image: example/blog:latest
container_name: private_blog
restart: unless-stopped
networks:
default:
aliases:
- private_blog
environment:
- NODE_OPTIONS=--max-old-space-size=384
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3000/api/health >/dev/null || exit 1"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
networks:
default:
external: true
name: server_server这里的 external: true 有两个含义:
- Compose 不创建这个网络,只查找名为
server_server的现有网络。 - 应用 Stack 不负责这个网络的生命周期。
博客没有配置 ports。端口 3000 仍然可以被同一网络里的 Caddy 访问,只是没有映射到宿主机。
Caddyfile 可以直接使用容器的网络别名:
example.com {
reverse_proxy private_blog:3000
}容器重建后 IP 可能变化,但 private_blog 会由 Docker 内置 DNS 重新解析,因此不需要固定容器 IP。
lookup private_blog on 127.0.0.11:53 是怎么回事
网络没有配对时,Caddy 会返回 502,并留下类似错误:
dial tcp: lookup private_blog on 127.0.0.11:53: server misbehaving127.0.0.11 是容器内使用的 Docker DNS。这个错误不代表公网 DNS 坏了,而是 Caddy 所在网络中没有名为 private_blog 的端点。
一次实际排查中,两个容器分别连接到了:
Caddy: server_server
Blog: server_default把博客配置中的外部网络名改为 server_server,重新部署后即可解析。Compose 不会因为两个服务选错网络而主动警告,配置本身能正常启动,问题通常要到反向代理收到请求时才暴露。
先确认网络中的容器:
docker network inspect server_server \
--format '{{range .Containers}}{{println .Name .IPv4Address}}{{end}}'正确结果里应同时出现代理和应用容器:
server_caddy 172.30.2.2/24
private_blog 172.30.2.3/24然后从 Caddy 容器直接请求应用:
docker exec server_caddy \
wget -qO- http://private_blog:3000/api/health这个请求成功后,才说明容器名称解析和应用端口都已打通。只看到容器处于 running 或 healthy,不能证明 Caddy 所在容器能够访问它。
为什么不用 Docker 自带的 bridge
Docker Engine 安装后会自动创建一个名为 bridge 的内置网络,对应宿主机上的 docker0。没有显式指定网络的 docker run 容器通常会进入这里。
它当然能传输数据,但不适合这个场景。内置 bridge 没有用户自定义 bridge 那样的自动名称解析能力,跨容器访问往往只能依赖会变化的 IP 或旧式 link。所有加入它的容器也挤在同一个网络里,边界不清楚。
Compose 创建的 server_server 也是 bridge,区别在于它是用户自定义网络,支持按容器名或网络别名解析,并且只连接明确加入它的服务。
一个网络为什么有很多 veth
清理完其他 Docker 网络后,docker network ls 可能只剩下这些:
bridge
host
none
server_server此时运行 ip addr,仍然会看到许多 veth:
veth2c25236@if13567 ... master br-5a7771da6c3a
vetha958539@if13886 ... master br-5a7771da6c3a
veth1529a95@if13892 ... master br-5a7771da6c3a它们不是多出来的 Docker 网络。Linux bridge 更像一台虚拟交换机,br-5a7771da6c3a 是交换机本身,每个 veth 是一个容器接入交换机的虚拟端口。一个网络连接 30 个容器,宿主机上就会出现大约 30 个 veth 接口。
名称中的 5a7771da6c3a 通常对应 Docker 网络 ID 的前 12 位,可以这样对照:
docker network ls查看网络端点数量:
docker network inspect server_server \
--format '{{len .Containers}}'查看网桥下的宿主机侧接口:
ip -o link show master br-5a7771da6c3a只要这些 veth 显示 UP、LOWER_UP,并且挂在当前网桥下,通常就是运行中容器的正常接口。不要手动执行 ip link delete veth...,否则对应容器会立即断网。容器删除后,Docker 会处理它的 veth。
docker0 显示 NO-CARRIER 或 state DOWN 也不需要清理。这通常只是内置 bridge 当前没有连接运行中的容器。
自定义子网要避开宿主机路由
如果没有固定网段的需要,最省事的做法是不给网络写 ipam,让 Docker 选择可用网段:
networks:
server:
driver: bridge确实需要固定子网时,先检查宿主机已有路由:
ip route例如宿主机地址为 172.16.0.168/20,那么它所在网段覆盖 172.16.0.0 到 172.16.15.255。此时再给 Docker 配置 172.16.2.0/24 就发生了重叠。服务暂时正常,也可能在访问局域网地址、连接 VPN 或路由变化时出现难以解释的问题。
文章示例使用 172.30.2.0/24 只是为了便于阅读,不代表它一定适合另一台主机。仍要以 ip route、VPN 网段和其他 Docker 网络为准:
networks:
server:
driver: bridge
ipam:
config:
- subnet: "172.30.2.0/24"修改正在使用的 Docker 子网需要重建网络,会中断相关容器连接,应该安排维护窗口处理。
这种复用方式的边界
复用某个 Stack 创建的网络,配置少,适合同一台主机上长期一起运行的固定服务。它不是访问控制系统:任何被接入 server_server 的容器都能访问网络中的其他容器。
网络仍由最初创建它的 Stack 管理。如果多个 Stack 需要独立部署、频繁拆除,单独创建一个长期存在的 external 网络会更稳妥:
docker network create shared_proxy然后在相关 Stack 中统一引用:
networks:
shared_proxy:
external: true
name: shared_proxy无论采用哪种方式,判断容器能否通信只看一件事:它们是否连接到了同一个实际 Docker 网络。Compose 文件里的键名、Stack 名和容器名只是帮助我们找到这个网络,不能替代 docker network inspect 的结果。