SSD Nodes Learn 🎉 VPS $4.99/月起
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-07

Gluetun 网络下如何访问主机和其他容器

使用 network_mode: service:gluetun 后,应用没有独立接口和端口。将端口发布到 Gluetun,并仅放行隧道外必须访问的子网,避免路由和防火墙配置失效。

容器加入 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_modenetworks,并拒绝加载整个文件。

另一个问题会在之后出现。命名空间中的所有容器共享同一个端口空间,因此两个默认使用 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 访问,因为该套接字在 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 网桥网关的路由,因此发往这些地址的数据包会通过 eth0 离开,而不是进入隧道。它还会为这些子网放行防火墙流量,因为在默认情况下,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。两个方向的配置方式不同。

入站访问比较简单。在 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 查找该地址。在这里,绑定端口比防火墙规则更可靠,因为该端口根本不会在公网接口上开放。这样也可以避开 让 Docker 直接绕过 ufw 发布端口 的问题。

出站访问则需要处理 FIREWALL_OUTBOUND_SUBNETS。如果容器必须访问某个对端,请添加该对端的地址。相比整个 /10,更建议为每个对端配置一个 /32。容器不会使用主机的解析器,因此 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 行,因为 Prowlarr 是负责解析 host.docker.internal 的容器。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 健康检查介绍了这种配置的一般形式。

:::details在所有地址上发布,而不仅限于 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 改为通过 bridge 发送 VPN 流量,从而破坏端口转发。在开放任何私有地址范围前,先检查 WIREGUARD_ADDRESSES 的值。
  • Tailscale 使用 100.64.0.0/10。这相当于开放大约四百万个地址,只为访问一个对等端。请将所需的对等端列为 /32 条目。

请记住,该设置覆盖整个 namespace。为使 indexer 访问主机服务而开放某个子网后,使用同一 namespace 的 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 会提示“端口发布与容器类型网络模式冲突”?

因为同时设置了 network_mode: service:gluetun 的服务中仍保留着 ports: 块。发布端口会添加 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 后,都应重新执行此检查。