SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-28

Gluetun 后的容器如何访问主机和其他容器

加入 Gluetun 网络命名空间的容器没有独立接口。将端口发布到 Gluetun,并仅放行必须绕过 VPN 隧道访问的主机或容器子网。

容器加入 gluetun 的网络后会发生什么

设置了 network_mode: service:gluetun 的容器没有自己的网络接口。它会加入 gluetun 的网络命名空间,因此,端口发布和防火墙规则不再属于该容器,而是属于 gluetun 服务。下面的所有结论都源于这一事实。

网络命名空间是内核中网络协议栈的私有副本:包含自己的网络接口、路由表、防火墙规则和监听套接字。Docker 默认会为每个容器创建一个网络命名空间。使用 network_mode: service:gluetun 时,Docker 会跳过这一步,并将新容器放入 gluetun 已拥有的命名空间中。该容器仍保留自己的文件系统和自己的 /etc/hosts 文件,后者稍后会发挥作用。

可以直接查看这一点。

docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent

该命令会输出 container:,后面跟 gluetun 容器 ID;普通容器则会输出 bridge。本指南承接使用 gluetun 通过 VPN 路由 Docker 流量的内容:隧道已正常工作,但现在没有任何内容能够与该容器通信。

在 gluetun 上发布端口,而不是在应用上发布

如果在设置 network_mode 的服务中保留 ports: 块,Docker 会拒绝创建容器:

Error response from daemon: conflicting options: port publishing and the container type network mode

原因很直接。发布端口意味着添加一条 NAT(网络地址转换)规则,将主机端口转发到容器自己的网络命名空间中,但该容器没有独立的网络命名空间。将端口映射移到 gluetun 服务中。端口号不变,因为应用仍在共享的网络命名空间中监听该端口。

services:
  gluetun:
    ports:
      - "8080:8080/tcp"   # qBittorrent web UI

  qbittorrent:
    network_mode: "service:gluetun"
    # no ports: block here

在依赖服务中添加 expose: 块同样没有作用,而添加 networks: 块会直接导致失败:Compose 会报告该服务同时声明了互斥的 network_mode 和 networks,并拒绝加载整个文件。

这会在之后引发一个问题。命名空间中的所有容器共享同一个端口空间,因此两个默认使用 8080 的应用会发生冲突,后启动的应用会因地址已被占用而启动失败。在其中一个应用自己的配置中修改端口,例如修改 LinuxServer qBittorrent 镜像中的 WEBUI_PORT 变量,然后在 gluetun 上发布新的端口号。

Gluetun 后的容器如何相互访问?

在该网络命名空间内,它们已经共享 loopback 接口。Gluetun 后的容器无需经过 Docker 网络,即可通过 127.0.0.1:<port> 访问同一组中的容器。

从网络命名空间外部看,该容器没有名称。Docker 内置 DNS 会将服务名称解析为该服务在用户自定义网络上的地址,而此容器在任何网络上都没有地址。因此,Sonarr 之类的普通容器无法通过 http://qbittorrent:8080 访问 torrent 客户端,而是通过 http://gluetun:8080 访问它,因为该 socket 在 gluetun 的网络命名空间内监听,使用的是 gluetun 的地址。熟悉 Docker Compose 网络和服务名称工作方式 的用户通常会对此感到意外,因为他们预期这里也能使用通常的命名方式。由于两个容器位于同一个 Compose 网络中,即使不向主机发布任何端口,这种方式也能正常工作。

在排查其他问题之前,先检查 DNS。Gluetun 运行自己的解析器,并会在自身容器中重写 /etc/resolv.conf;但 /etc/resolv.conf 是每个容器独立的文件,因此 gluetun 写入的文件不是应用读取的文件。

docker exec qbittorrent cat /etc/resolv.conf

如何访问 Docker 主机上运行的服务?

使用 host.docker.internal。它需要在两个不同位置设置,因为有两个不同的问题需要修复。

先设置名称。/etc/hosts 按容器配置,因此 extra_hosts 条目应放在应用容器上,而不是 gluetun 上。

  prowlarr:
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"

host-gateway 是一个特殊值,Docker 会将其替换为主机自身的内部地址。在纯 Linux Docker 安装中,该地址是 docker0 网桥的地址,通常为 172.17.0.1。在 VPS 上使用 ip -4 addr show docker0 确认实际地址。Docker Desktop 会自行解析此名称,因此在笔记本电脑上编写的指南会省略 extra_hosts 行;同一个文件放到服务器上就会失败。

然后设置路由。仅添加名称只能告诉容器使用哪个地址。数据包仍会通过 gluetun 的默认路由离开,而默认路由是隧道;gluetun 的防火墙会丢弃该数据包。其表现是连接先挂起,随后超时,而不是立即被拒绝。拒绝表示数据包已到达,并且有服务返回了拒绝响应。超时表示数据包从未到达。

  gluetun:
    environment:
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32

然后确认主机服务确实在该地址上监听。仅绑定到 127.0.0.1 的 PostgreSQL 服务器无法从任何容器访问,无论是否经过隧道,因为命名空间中的 127.0.0.1 是该命名空间自己的回环地址。改为绑定到 172.17.0.1:这样既能接受来自容器的连接,又不会监听公网接口。在主机上使用 ss -lntp | grep 5432 验证。

FIREWALL_OUTBOUND_SUBNETS 实际会改变什么

Gluetun 文档将其描述为一个逗号分隔的子网列表,表示 gluetun 以及共享其网络栈的容器可以访问哪些子网,并说明该设置会同时改变防火墙和路由。这两部分都很重要。Gluetun 会为每个列出的子网添加一条经由 Docker bridge 网关的路由,因此,发往这些地址的数据包会通过 eth0 离开,而不是通过隧道发送。同时,Gluetun 也会为这些子网放行防火墙流量,因为 gluetun 默认会丢弃所有不是发往 VPN 服务器的出站流量。

逗号后不要留空格。

      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32

有两个特性容易被忽略。这是网络命名空间级别的设置,因此它会应用于 gluetun 后面的所有容器,而不只是您原本指定的那个容器。它还仅适用于出站流量:该设置控制由容器发起的连接。到达已发布端口的连接会经过不同的路径,因此不需要在此处添加。

通过 Tailscale 对等节点访问 Web UI

Tailscale 会为每台机器分配一个位于 100.64.0.0/10 的地址,该地址段专门用于运营商级 NAT。在个人 tailnet 上,这些功能均不收费;不过,当其他人需要访问相同的 UI 时,免费计划的用户数和设备数限制会决定是否仍可保持免费。由于计费按用户而不是按机器计算,因此付费 tailnet 的实际费用取决于您邀请多少人,而不是向他们开放多少个容器。两个方向需要采用不同的处理方式。

入站访问较为简单。在 gluetun 上发布 8080:8080 会将该端口绑定到主机的所有地址,而主机的 tailscale0 接口就是其中之一。因此,对等节点打开 http://<machine-name>:8080 即可访问容器。此路径不经过 gluetun,因为 Docker 的 NAT 规则位于主机上,在该网络命名空间之外。

若要仅通过 tailnet 访问 UI,请将发布端口绑定到主机的 Tailscale 地址,而不是绑定到所有地址。

    ports:
      - "100.101.102.103:8080:8080/tcp"

在主机上使用 tailscale ip -4 查找该地址。在这里,绑定比防火墙规则更可靠,因为该端口根本不会在公网接口上开放。如果希望通过 HTTPS 名称而不是主机和端口访问 UI,可以使用 tailscale serve 代理该发布端口;但 serve 和 funnel 对访问者范围的处理不同,只有其中一种方式会让 UI 保持在 tailnet 内。这样也可以避开Docker 绕过 ufw 直接发布端口的问题。

出站访问需要使用 FIREWALL_OUTBOUND_SUBNETS。如果容器必须访问某个对等节点,请添加该节点的地址;相比覆盖整个 /10,应优先为每个对等节点配置一个 /32。如果要访问的机器位于私有网络中,并且该网络通过向 tailnet 宣告该子网的 VPS可达,请列出宣告的地址范围,而不是路由器自身的 100.x 地址,并确认主机本身已接受这些路由。MagicDNS 名称不会在容器内解析,因为容器不使用主机的解析器。因此,请使用数字形式的 100.x 地址,或通过 extra_hosts 行固定该地址。运行使用 Headscale 自建 Tailscale 控制服务器时同样适用。

常见结构的完整 compose 文件

一个位于 VPN 后方的下载客户端、两个只在 tailnet 上提供服务的 Web UI,以及一个读取主机上 PostgreSQL 数据库的容器。

services:
  gluetun:
    image: qmcgaw/gluetun:latest
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    ports:
      - "127.0.0.1:8000:8000/tcp"        # gluetun control server, host only
      - "100.101.102.103:8080:8080/tcp"  # qBittorrent UI, tailnet only
      - "100.101.102.103:9696:9696/tcp"  # Prowlarr UI, tailnet only
    volumes:
      - ./gluetun:/gluetun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
      - WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
      - SERVER_CITIES=Amsterdam
      - FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
      - TZ=Europe/Amsterdam
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - /srv/downloads:/downloads
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    network_mode: "service:gluetun"
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - PROWLARR__POSTGRES__HOST=host.docker.internal
      - PROWLARR__POSTGRES__PORT=5432
      - PROWLARR__POSTGRES__USER=prowlarr
      - PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
      - PROWLARR__POSTGRES__MAINDB=prowlarr-main
      - PROWLARR__POSTGRES__LOGDB=prowlarr-log
    volumes:
      - ./prowlarr:/config
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

阅读此文件时应关注其模式,而不是产品名称。两个 UI 都发布在 gluetun 上,并绑定到主机的 tailnet 地址,因此只在 Tailscale 上提供服务,不会在其他网络上响应。只有 Prowlarr 包含 extra_hosts 行,因为负责解析 host.docker.internal 的容器是 Prowlarr。FIREWALL_OUTBOUND_SUBNETS 定义了两个单独的地址:一个是主机的 Docker bridge 地址,供 Prowlarr 建立数据库连接;另一个是 tailnet 对等节点地址。

PostgreSQL 服务器特意未写入此文件。它作为普通系统服务运行在 VPS 上,并监听 172.17.0.1:5432。这与Docker Compose 上的 arr 堆栈采用相同的分层方式,只是将数据库移到了 Docker 外部。

不要将 WireGuard 私钥写入 compose 文件。${WIREGUARD_PRIVATE_KEY} 从旁边的 .env 文件中读取私钥,这正是Docker Compose 的环境文件和密钥中介绍的模式。condition: service_healthy 子句使用 gluetun 镜像自带的健康检查,因此只有隧道报告已启动后,其他内容才会启动。Compose 健康检查介绍了这种通用形式。

在所有地址上发布,而不只是 tailnet

删除 0.0.0.0 上的地址前缀和端口绑定,其中包括 VPS 的公网 IP。只有在由您控制的防火墙后方时才这样做,并先阅读上面的 ufw 说明。

    ports:
      - "8080:8080/tcp"

确认隧道仍在传输网络流量

从命名空间内部和主机分别运行同一个请求,并比较结果。

docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.org

第一次请求应输出 VPN 提供商的出口地址。第二次请求应输出 VPS 地址。如果两者相同,说明容器的流量没有经过隧道。在此问题修复前,本指南中的其他修复都没有意义。

路由表可以显示哪些流量经过隧道,哪些没有。

docker run --rm --network=container:gluetun alpine:3.22 ip route show

默认路由应指向隧道接口 tun0。在其下方,应看到 FIREWALL_OUTBOUND_SUBNETS 中每个条目对应一条路由,并且这些路由都指向 Docker bridge 网关。任何经由 eth0 离开的其他路由,都是绕过 VPN 的流量。

Gluetun 的控制服务器会在端口 8000 的 /v1/publicip/ip 报告相同的公网 IP。较新版本要求为控制服务器路由配置身份验证,因此请先完成配置,再依赖此功能。

错误的子网范围会造成的泄漏

FIREWALL_OUTBOUND_SUBNETS 是您有意在防火墙上打开的缺口,因此缺口大小就是风险范围。以下四种情况会使范围过大:

  • 0.0.0.0/0 会将所有流量发送到隧道外。上面的两项 IP 检查会在首次运行时发现这一点,因为它们会返回同一个地址。
  • 范围大于目标范围。为访问 10.0.1.7 上的一台主机而开放 10.0.0.0/8,也会开放该范围内 torrent 对等端可能通告的所有地址。请填写 10.0.1.7/32。
  • 范围与隧道自身的地址重叠。gluetun 文档警告称,这会使 gluetun 改为通过网桥发送 VPN 流量,从而导致端口转发失效。在开放任何私有地址范围前,请检查您的 WIREGUARD_ADDRESSES 值。
  • Tailscale 使用 100.64.0.0/10。这大约会开放 4000000 个地址,只为访问一个对等端。请将所需的对等端列为 /32 条目。

请记住,此设置会覆盖整个命名空间。开放一个子网,使索引器能够访问主机服务,也会为共享该命名空间的 torrent 客户端开放同一个子网。每次修改此变量后,都应重新运行公网 IP 检查,因为只有这项测试能够显示修改是否达到了预期效果。

重启 gluetun 时哪些功能会失效

gluetun 管理该网络命名空间,因此 gluetun 的生命周期就是该命名空间的生命周期。gluetun 停止时,启动依赖它的容器会立即失败:

Error response from daemon: cannot join network of a non running container

在原位置重启 gluetun 会导致更隐蔽的故障。依赖它的容器会继续运行,但它们所连接的网络命名空间已在底层重建,因此 docker ps 显示一切正常,却没有任何服务响应。每次修改 gluetun 服务后,都应重建整个服务组,而不是只重启其中一个服务。

docker compose up -d --force-recreate

镜像更新也一样。拉取新的 gluetun 镜像并只重建该服务后,其他服务仍会指向已不存在的网络命名空间。

FAQ

为什么 Docker 会提示“端口发布与容器类型网络模式”?

因为某个 ports: 块仍配置在同时设置了 network_mode: service:gluetun 的服务上。发布端口会添加一条 NAT 规则,将主机端口转发到容器自己的网络命名空间;而使用此模式的容器没有独立的网络命名空间。请从该服务中删除 ports: 块,并将完全相同的映射添加到 gluetun 服务中。端口号保持不变,因为应用仍在共享的命名空间中监听该端口。

gluetun 后的服务如何被其他容器访问?

同一命名空间中的容器通过 127.0.0.1 互相访问。命名空间外的容器使用 gluetun 服务名,因此 http://gluetun:8080 可用,而 http://qbittorrent:8080 不可用。应用容器在任何 Docker 网络上都没有地址,因此内置 DNS 服务器无法解析其名称。只要两个容器共享一个 Compose 网络,就不需要为此发布端口。

FIREWALL_OUTBOUND_SUBNETS 应该填写什么?

只填写 gluetun 后的容器必须主动连接的地址,并尽可能缩小范围。单台主机应写成 /32。最常见的两个条目是 Docker 主机的 172.17.0.1/32,以及每个要访问的 Tailscale 对等端对应的一个 /32。不要添加 0.0.0.0/0,也不要添加与 VPN 自身隧道地址重叠的地址范围。访问已发布端口的入站连接不需要在此处添加条目。

为什么容器无法解析我的 Tailscale MagicDNS 名称?

MagicDNS 通过将主机的解析器指向 Tailscale 的 DNS 服务器来工作,而容器不使用主机的解析器。容器使用自身 /etc/resolv.conf 中配置的内容;在 gluetun 后时,使用的是 gluetun 的 DNS 配置。使用 docker exec <container> cat /etc/resolv.conf 进行确认。请使用对等端的数字 100.x 地址,或在该容器中通过 extra_hosts 条目固定名称。

如何确认流量仍通过 VPN 传输?

在该命名空间内发起一次请求,再从主机发起相同请求,然后比较返回结果。docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org 应返回 VPN 提供商的出口地址,而 VPS 上的 curl -s https://api.ipify.org 应返回 VPS 地址。两个结果相同,表示隧道未承载该容器的流量。每次修改 FIREWALL_OUTBOUND_SUBNETS 后,都应重新执行此检查。