Docker Compose 网络配置与服务名 DNS 怎么用
了解 Compose 默认项目网络、服务名 DNS、host 网络适用场景、跨项目共享网络,以及绕过 UFW 的已发布端口,含常见连接错误。
Compose 在应用启动前构建的内容
Docker Compose 网络遵循一条基本规则:docker compose up 会为项目创建专用网络,将每个服务连接到该网络,并允许这些服务通过服务名称相互访问。无需手写任何 networks: 行即可实现这一点。许多 Compose 网络问题都源于不了解默认网络本身已经存在。
下面是一个小型文件。将其保存为目录 shop 中的 compose.yaml。
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example启动项目并查看 Docker 创建的内容:
docker compose up -d
docker network ls列表中现在会有一个名为 shop_default 的网络。Compose 将其命名为 <project>_default,项目名称默认使用目录名的小写形式。可以通过 docker compose -p myproject up -d 覆盖项目名称,也可以在文件顶层使用 name: myproject。该网络的驱动是 bridge,它相当于主机内部的虚拟交换机。每个容器都会在专用子网上获得一个地址,出站流量离开主机时会转换为主机的地址。
docker compose down 会再次删除该网络。这也解释了为什么旧项目中的过时容器可能会使网络保持存在:Docker 会返回 error while removing network: network shop_default has active endpoints,解决方法是停止或删除仍连接到该网络的容器。
如果你刚开始使用 Compose,建议先阅读 Compose 文件结构和生命周期命令,因为下面的内容都假设你已经能够启动和停止项目。
按服务名解析 DNS 是初学者最容易忽略的部分
在任何用户定义的网络上,Docker 都会运行一个内置 DNS 服务器,每个容器都可通过 127.0.0.11 访问该服务器。它会将服务名解析为当前容器的地址。因此,web 无需任何配置,就能通过主机名 db 和端口 5432 连接数据库。
docker compose exec web getent hosts db该命令会输出类似 172.18.0.2 db 的行。如果没有任何输出,说明这两个服务不在同一个网络上。
几乎所有人都会犯一次的错误,是在应用配置中使用 localhost。在容器内,localhost 指向该容器自身,而不是宿主机,也不是其他服务。Postgres 客户端会明确报告这一点:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?连接字符串应为 postgresql://postgres:example@db:5432/postgres。其中的主机部分应使用服务名。
还有两个细节可以避免后续排查浪费时间。名称解析结果对应当前正在运行的容器,因此 docker compose up -d --scale web=3 会为同一个名称返回三个地址;如果客户端永久缓存 DNS,就会固定连接到已经失效的容器。另一个细节是,普通 docker run 在不使用 --network 时采用的旧式 bridge 网络,完全不能解析容器名称:该网络中的容器只能通过 IP 地址互相访问,并通过宿主机 /etc/resolv.conf 的副本解析外部主机名。这就是为什么 2016 年关于容器链接的建议与你现在看到的行为不一致。
连接两个服务不需要 ports:
ports: 会将容器端口发布到主机。它用于接收来自 Docker 外部的流量。它与服务之间的流量无关,因为同一项目网络中的服务已经可以使用整个端口范围相互通信。
因此,许多人为数据库服务添加的 ports: - "5432:5432" 不仅没有作用,还会造成实际危害:它会将 Postgres 暴露在服务器的公网接口上。请删除它。如果需要在迁移期间从笔记本电脑访问数据库,请使用 "127.0.0.1:5432:5432" 将其绑定到回环接口,然后通过 SSH 隧道访问。监听套接字、已发布端口和防火墙规则之间的区别,请参阅Linux 中端口和监听服务的工作方式。
在 Compose 中,expose: 仅用于文档说明。它不会打开任何端口,因为同一网络中的容器之间本来就没有端口被关闭。
何时适合使用 network_mode host,以及它的代价
Host 模式会移除容器自己的网络命名空间,使进程可以直接使用宿主机的网络接口。
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinity使用该模式有一些实际需求。例如,需要查看本地网络广播或组播流量的进程(如媒体服务器或家庭自动化中枢的设备发现功能),无法在 bridge 后面看到这些流量,因为 bridge 不会将其转发到容器。读取宿主机网络接口计数器的监控代理也需要访问宿主机接口。此外,该模式会跳过地址转换这一跳,在高数据包速率下可能很重要。
它的代价也很明确。
ports: 将不再生效。Docker 会警告,使用 host 网络模式时会丢弃已发布的端口,容器会直接绑定其进程实际绑定的端口。两个 host 模式容器如果都要使用端口 8080,就会发生冲突,后启动的容器会因 bind: address already in use 退出。
两个方向上的服务名解析都会失效。容器不在项目网络中,因此无法解析 db,其他服务也无法解析它。容器仍可通过宿主机上发布的端口访问这些服务,通常使用 127.0.0.1。在 Linux 上,host 模式容器还可以通过容器 IP 地址,直接访问 bridge 容器的任意监听端口。这是因为宿主机可以访问其 bridge 网络上的容器,而 host 模式容器共享宿主机的网络栈。这些地址会在容器重新创建后变化,因此,将端口发布到 loopback 是更稳定的访问路径。
隔离也会消失。在 host 模式容器内绑定 0.0.0.0 的进程会监听服务器的所有网络接口,包括公网接口,其行为与通过 apt 安装的软件包完全相同。这样做有一个优点:该流量会经过正常的入站路径,因此 UFW 规则会对其生效;发布的端口则不是这样。
Host 模式是 Linux Docker Engine 的功能。Docker Desktop 仅从版本 4.34 开始支持该模式,而且必须先启用它;此外还存在以下限制:容器不能绑定宿主机 IP 地址,并且只处理 TCP 和 UDP。如果团队一部分成员使用 Linux 服务器,另一部分成员使用 Docker Desktop,应预期同一个文件会表现出不同的行为。
只有在需要使用宿主机网络接口时才应选择 host 模式。不要用它来修复连接问题,因为它通常会将一个问题替换为更难排查的问题。
连接两个使用 Compose 的项目,并使用外部网络
一个项目创建的网络对另一个项目不可见。因此,即使反向代理和应用位于同一台服务器上,proxy/compose.yaml 中的反向代理也无法发现 app/compose.yaml 中的应用。解决方法是创建一个不归任何项目所有的网络。
手动创建一次:
docker network create edge然后在两个项目中都将其声明为外部网络。代理端:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: true应用端:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:external: true 告诉 Compose 挂载现有网络,而不是创建网络,并在 docker compose down 上保留该网络。单独的 name: 键比看起来更重要:没有它,Compose 会查找名称必须完全为 edge 的网络;使用它后,可以在配置文件中使用一个名称,在主机上使用另一个名称。
如果网络不存在,Compose 会拒绝启动,并报告该网络已声明为外部网络但无法找到。请先创建网络。
注意应用配置文件如何处理 internal。数据库只连接到该项目的本地网络,因此代理无法访问它,只有 app 可以访问。将 internal: true 添加到网络配置下后,还会完全移除数据库容器访问外部网络的路由。对于数据库,这是一个合理的默认设置,但在启用前需要了解一个代价:连接到内部网络的容器无法下载任何内容,因此启动时运行 apt-get update 或 pip install 的 entrypoint 会先卡住,随后因超时失败。
如需查看包含路由规则和证书的完整配置示例,请参阅在同一个 Traefik 实例后运行多个应用。
发布的端口会绕过 UFW
这是 Compose 网络配置可能演变为安全事件的部分。您发布了一个端口,确认 UFW 已启用并拒绝除 SSH 之外的所有连接,但该服务仍可从互联网访问。
sudo ufw status
curl http://203.0.113.10:8080UFW 显示该端口已被阻止,但 curl 仍会返回页面。这并不是故障。Docker 会直接在 iptables 中写入自己的地址转换和转发规则。访问已发布的容器端口的流量会被转发到容器,而不是交付给主机,因此不会经过 UFW 管理的、处理发往本机流量的链。Docker 的规则也会在 UFW 的规则之前进行匹配。
简便的修复方法是仅在需要的位置发布端口:
ports:
- "127.0.0.1:8080:80"这样会将主机端绑定到 loopback,因此该端口可从服务器本身和 SSH 隧道访问,其他位置都无法访问。将公网入口放在反向代理之后,并由反向代理明确发布 80 和 443 端口。完整说明(包括必须过滤已发布端口时使用的 DOCKER-USER 链)请参阅为什么 Docker 会直接绕过 UFW 发布端口,以及如何修复。
用 4 条命令进行排查
首先确认每个容器实际连接到了哪个网络:
docker network inspect shop_defaultContainers 代码块会列出所有已连接容器及其地址。未出现在列表中的服务可能位于其他网络、处于 host 模式,或尚未运行。
从连接到同一网络的一次性容器中测试名称解析。这样无需在自有镜像中安装任何工具:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432nslookup 失败表示名称解析或网络成员关系存在问题。nslookup 成功而 nc 失败,表示名称和网络都正常,问题出在连接本身。如果 nc 报告 Connection refused,通常是因为服务没有监听该端口,或者服务在自己的容器内监听 127.0.0.1,而不是 0.0.0.0。开发服务器经常出现后一种情况。修复位置是应用的绑定地址,而不是 Docker。如果 nc 一直等待并最终超时,应检查是否有规则在传输路径中丢弃流量,例如防火墙规则,或者容器已暂停或没有响应。
还有一种看起来像 Docker 缺陷的问题。如果容器之间可以通信,但无法访问办公室或 VPN 网络中的计算机,Docker 子网很可能与该网络发生了重叠。Docker 默认从 172.17.0.0/16 开始分配地址池。可在 /etc/docker/daemon.json 中移动该地址池:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}然后运行 sudo systemctl restart docker 并重新创建受影响的网络,因为现有网络会保留创建时使用的子网。
FAQ
为什么容器无法通过服务名相互访问?
因为它们不在同一个网络中。Compose 会自动将每个服务连接到 <project>_default,但一旦为某个服务添加 networks: 列表,该列表就会成为该服务的完整网络集合,不再隐含包含默认网络。运行 docker network inspect <network>,检查两个容器是否都出现在 Containers 块中。还要检查两个服务是否都未使用 network_mode: host,因为 host 模式容器不连接任何 Docker 网络,也无法解析服务名。
一个服务访问另一个服务时,是否需要发布端口?
不需要。在 Compose 网络中,该网络上的其他容器可以访问每个容器的所有端口。ports: 仅用于将容器暴露给 Docker 外部的网络流量,expose: 仅用于提供文档说明。发布数据库端口是一种常见且代价高昂的做法,因为这会将数据库暴露在服务器的公网接口上。
bridge 网络和 host 网络有什么区别?
bridge 会为容器提供独立的网络命名空间,并在虚拟交换机上分配地址,同时在容器之间自动解析名称,并转换出站流量。host 会直接使用主机的网络协议栈:没有独立地址,无法通过服务名解析,不需要发布端口,也不与主机上的其他监听端口隔离。bridge 是默认选项,除非进程需要使用主机的网络接口,否则应使用 bridge。
如何连接来自两个不同 Compose 文件的容器?
使用 docker network create edge 创建共享网络,然后在两个文件中通过 external: true 声明该网络,并将需要通信的服务连接到该网络。Compose 不会创建或删除这个网络。如果跳过创建步骤,Compose 将拒绝启动,并报告该网络已声明为 external,但未找到。
UFW 阻止端口后,为什么容器仍可从互联网访问?
因为发布端口由 Docker 添加到 iptables 的转发规则处理。这些规则会先于 UFW 规则匹配,而且转发流量本身也不会经过 UFW 过滤的链。使用 "127.0.0.1:8080:80" 将主机侧绑定到 loopback,并通过 80 和 443 端口上的反向代理发布公网服务。