Docker Compose 跨 Stack 复用 Bridge 网络

太阳作者太阳
原创内容采用 CC-4.0 协议发布,转载请注明出处

我有两个部署在同一台主机上的 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:
      - server

Compose 默认会给资源名加上项目名前缀,最终创建出来的网络通常叫:

server_server

第一个 server 是项目名,第二个 server 是 Compose 文件里的网络键名。不要凭感觉拼这个名字,部署后直接查看:

docker network ls

如果配置使用的是隐式 default 网络,对应名称通常是 <项目名>_default。例如项目名为 server,网络就是 server_defaultserver_defaultserver_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 misbehaving

127.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

这个请求成功后,才说明容器名称解析和应用端口都已打通。只看到容器处于 runninghealthy,不能证明 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 显示 UPLOWER_UP,并且挂在当前网桥下,通常就是运行中容器的正常接口。不要手动执行 ip link delete veth...,否则对应容器会立即断网。容器删除后,Docker 会处理它的 veth。

docker0 显示 NO-CARRIERstate DOWN 也不需要清理。这通常只是内置 bridge 当前没有连接运行中的容器。

自定义子网要避开宿主机路由

如果没有固定网段的需要,最省事的做法是不给网络写 ipam,让 Docker 选择可用网段:

networks:
  server:
    driver: bridge

确实需要固定子网时,先检查宿主机已有路由:

ip route

例如宿主机地址为 172.16.0.168/20,那么它所在网段覆盖 172.16.0.0172.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 的结果。

参考资料