Docker 容器如何通过 Gluetun VPN 路由
使用 Gluetun sidecar 共享网络命名空间后,容器端口和服务名会消失。本文解释原因,并提供可用的 Docker Compose 配置及端口发布方式。
通过 VPN 路由 Docker 容器时端口为何会消失
要通过 VPN 路由 Docker 容器,需要让一个容器建立隧道,再使用 network_mode: "service:gluetun" 将其他容器连接到它的网络命名空间。连接到该命名空间后,容器不再拥有独立网络,因此其发布端口和 Docker 服务名称也会随之消失。应改为在 VPN 容器上发布端口,其他容器通过 VPN 容器的名称访问应用。
如果在已连接的容器上保留 ports: 块,Docker 会直接拒绝创建该容器:
Error response from daemon: conflicting options: port publishing and the container type network mode这里使用的工具是 Gluetun。它是一个通过 WireGuard 或 OpenVPN 连接商业 VPN(虚拟专用网络)提供商,并自带防火墙的容器。截至 October 2026,当前版本为 v3.41.3。示例使用 WireGuard 连接 Mullvad,因此需要从提供商处获取账户和密钥。如果希望在自有硬件上终止隧道,可以通过 在 VPS 上运行自己的 WireGuard 服务器 构建隧道的另一端,再使用 Docker 中的 wg-easy 为其提供 Web 界面。
network_mode: "service:gluetun" 的实际作用
通常,每个 Docker 容器都会获得自己的网络命名空间:自己的网络接口、路由表、防火墙规则和监听套接字。service: 模式会跳过这一步,并在 gluetun 的命名空间中启动容器。一个命名空间只有一个 IP 地址,这会带来以下六点变化。
- 应用没有自己的地址。它使用 gluetun 的地址。
- 应用未连接到任何 Docker 网络,因此其服务名称不会注册,也无法解析。其他容器必须使用
gluetun。 - 命名空间中的容器通过
localhost互相访问。 - 同一命名空间中的两个容器不能监听同一个端口。Gluetun 文档对此说明得很明确:没有可行的解决方法。
- 能力属于容器,而不属于命名空间。由于 Gluetun 会创建隧道接口,因此它持有
NET_ADMIN和/dev/net/tun。附加的容器不会继承这些能力。 - 如果某个服务同时设置
network_mode和networks,Compose 会拒绝该文件。将 gluetun 连接到你的网络,应用即可随之使用这些网络。
重启 gluetun 会断开所有附加到它的容器。这是文档明确说明的行为,也是 gluetun 在连接失败时于容器内重启 VPN 进程、而不是退出的原因。自行重启或重新创建 gluetun 后,请重启附加到它的容器。
可用的 compose 文件
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- SERVER_CITIES=Amsterdam
- TZ=Europe/Amsterdam
env_file:
- ./gluetun.env
volumes:
- ./gluetun:/gluetun
ports:
- 127.0.0.1:8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- ./downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped:v3标签是 v3 系列的最新稳定版本。:latest标签指向 master 分支的最后一次提交,也就是开发中的前沿版本。因此,在不希望某个周二还要排查问题的机器上,应固定使用 :v3。
WEBUI_PORT=8080必须与发布端口一致,因为 qBittorrent 在 gluetun 的网络命名空间中绑定端口,而发布规则会将主机流量转发到该命名空间中的 8080 端口。只修改其中一个数字会导致另一个不匹配,端口将不会响应。127.0.0.1:8080:8080会将 Web 界面限制在主机的 loopback 地址上。直接使用 8080:8080 会在所有网络接口上发布端口,并自行写入防火墙规则;这正是 Docker 发布的端口直接绕过 ufw 的原因。
启动服务,然后按以下顺序检查:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps应显示 gluetun 的状态为 healthy,qbittorrent 的状态为 running。然后从该命名空间内部确认出口地址。这项检查的结果决定其他所有检查:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"该 JSON 中的 ip 字段应为 VPN 提供商分配的地址。如果显示的是服务器自身的地址,说明应用未通过隧道运行,后续内容都不会按预期工作。
不要将密钥写入 compose 文件
gluetun.env 保存凭据,并且不会提交到 git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32这两个值都来自您在服务商账户区域生成的 WireGuard 配置文件。将文件权限设为 600。请明确这一做法的实际作用:密钥不会出现在代码仓库中,但任何能够访问 Docker socket 的人仍可通过 docker inspect gluetun 输出所有环境变量。Docker Compose 中的环境文件和机密介绍了更强的安全选项。
容器如何与隧道内的容器通信
两个方向都可行,但使用的名称不同。两个容器需要共享 Docker 网络,也就是 gluetun 的网络,因为附加的容器没有自己的网络。Docker Compose 网络的连接方式介绍了默认配置。
从外部访问内部容器时,使用 gluetun 的名称和应用监听的端口。反向代理容器可通过 gluetun:8080 访问 qBittorrent Web 界面。此时不需要 ports: 条目,因为容器之间的流量留在 Docker 网络中,不会经过主机端口。
从内部访问外部容器时,使用另一个容器的服务名称,例如 postgres:5432。从 v3.41 开始,Gluetun 可在其网络命名空间内解析其他容器的名称。因此,如果名称无法解析,请固定使用该版本或更高版本。
Gluetun 的防火墙决定哪些来源可以向其发起连接。来自 gluetun 自身 Docker 网络的流量是允许的。来自其他子网的客户端、LAN 上的笔记本电脑或独立 bridge 网络中的容器,需要 Gluetun 的防火墙和路由允许返回路径:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文档中的含义是明确的:以逗号分隔的子网,这些子网是 Gluetun 及与其共享网络栈的容器允许访问的目标。
来自互联网的入站连接是另一个问题。Torrent 客户端的对等端从 VPN 侧连接,因此在主机上发布 6881 端口对它们不起作用。你需要服务提供商转发的端口,并将该端口列在 FIREWALL_VPN_INPUT_PORTS 中;该配置允许来自 VPN 服务器一侧的端口。此示例使用的 Mullvad 不支持端口转发,因此必须使用支持该功能的服务提供商。这是大多数通过 Docker Compose 构建的媒体栈都会遗漏的问题。
隧道断开时会发生什么:终止开关
这种方案的复杂性体现在故障处理上。附加容器会共享 Gluetun 的网络接口和路由表。Gluetun 的防火墙会阻止隧道外的普通互联网流量,同时允许访问 VPN 服务器端点、本地 Docker 流量和已配置的例外。正是这个防火墙阻止流量在隧道断开时回退到普通网络接口。客户端重新连接期间,不会出现数据包从普通网络接口泄漏的时间窗口。
Gluetun 会监控自身的连接。它每分钟向 HEALTH_ICMP_TARGET_IPS 中的地址发送 ICMP 回显请求(即 ping),这些地址默认是 1.1.1.1,8.8.8.8。如果不允许 ICMP,该检查会改用明文 UDP DNS 查询。它每五分钟会对 HEALTH_TARGET_ADDRESSES 执行一次完整的 TCP 和 TLS(传输层安全)连接,默认目标为 cloudflare.com:443,github.com:443。这些检查失败时,它会在容器内重启 VPN,并记录以下日志:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout请结合这一顺序阅读附加容器的日志。应用内出现 connection refused、operation not permitted 和 i/o timeout 等行,是隧道已断开的结果,而不是原因。Gluetun 文档明确说明了这一点,因为人们经常报告这些结果,然后花数小时追查错误方向。
HEALTH_RESTART_VPN=on 默认处于启用状态,应保持开启。只有在调试某个特定故障时才关闭它,因为关闭后,断开的隧道会一直保持断开。
Ordering:在隧道建立前停止启动堆栈
该镜像包含一个 Docker 健康检查:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=3 CMD /gluetun-entrypoint healthcheck该命令会运行一个短生命周期的 gluetun 副本,并查询运行中实例在 http://127.0.0.1:9999/ 提供的健康检查服务器。隧道正常时会返回 200 OK。隧道故障时会返回 500 Internal server error 和错误字符串;连续三次检查失败后,Docker 会将容器标记为不健康。
condition: service_healthy 会等待该条件满足。普通的 depends_on: [gluetun] 只等待容器启动,而容器启动通常比握手完成早几秒。因此,应用会在网络不可用时启动,并且首次连接通常会失败。Docker Compose 中的健康检查详细介绍了语法和计时字段。
有一个限制容易被忽略。Compose 只会在启动堆栈时评估该条件。如果 gluetun 后续变为不健康,Compose 不会停止或重启应用。此时由 gluetun 的内部自动修复机制处理,因此它重启的是 VPN 进程,而不是容器。
检查 DNS 泄漏,再信任此配置
DNS(域名系统)是即使隧道配置正确也可能存在的泄漏。Gluetun 会在命名空间内运行自己的解析器,并默认通过 DoT(DNS over TLS)将查询转发到 Cloudflare:DNS_UPSTREAM_RESOLVER_TYPE=dot 和 DNS_UPSTREAM_RESOLVERS=cloudflare。不要修改这两项,DNS 查询就会加密,并通过隧道传输。
将本地解析器填入 DNS_UPSTREAM_PLAIN_ADDRESSES 会破坏这一点。有人会在域名无法解析时使用此设置,希望改由路由器或网络服务提供商的解析器响应。Gluetun 文档警告,将查询发送到 Docker bridge 或 LAN 上的解析器,会使查询泄漏到 VPN 隧道之外。公共明文解析器仍会通过隧道接收查询,但 VPN 提供商可以读取和修改这些查询。WireGuard 中的同类错误见WireGuard 隧道中的 DNS 停止解析。
要进行测试,请在 gluetun 上设置 SHADOWSOCKS=on 和 SHADOWSOCKS_PASSWORD,发布 8388:8388/tcp 和 8388:8388/udp,然后使用配置为通过隧道传输 UDP DNS 的 Shadowsocks 客户端连接,并加载 DNS 泄漏测试页面。结果应显示您的 VPN 提供商或 Cloudflare,绝不能显示家庭路由器。Gluetun 文档还警告,某些泄漏测试会报告异常结果,因为命名空间内的解析器是本地缓存中间层,而不是最终提供响应的服务器。应将显示错误国家或您自己的 ISP 解析器视为真正的泄漏信号。
在 VPN sidecar 旁添加 Tailscale,以及谁拥有路由优先权
Tailscale 是基于 WireGuard 构建的覆盖网络,用于访问您自己的设备。许多人会将它与供应商 VPN 并行运行,以保留进入整个服务栈的管理路径。共享网络命名空间也意味着共享防火墙规则,因此两者必须使用兼容的防火墙和路由设置。Tailscale 文档说明了默认行为:它作为覆盖网络运行,只在运行 Tailscale 的设备之间路由流量,不会处理您的公网流量。
因此,答案取决于一个设置。
- Tailscale 使用独立容器和默认配置:它完全看不到应用的出站流量。所有流量都由 Gluetun 承载。Tailscale 容器中的进程可通过共享 Docker 网络访问
gluetun:8080中的应用;要让 tailnet 客户端访问该连接,需要配置转发或代理。 - Tailscale 使用
network_mode: "service:gluetun"连接到 gluetun 的网络命名空间:在默认的 userspace networking 模式下,TS_USERSPACE已启用。tailscaled 完全不会创建网络接口,而是作为 SOCKS5 或 HTTP 代理运行,因此无法更改路由。除非应用显式使用该代理,否则普通出站流量仍由 Gluetun 承载。 - 使用相同配置并设置
TS_USERSPACE=false:Tailscale 需要自己的/dev/net/tun设备和相应能力,因为这些不会随网络命名空间自动提供。tailscaled 会创建隧道设备。在 Linux 上,它通常会为各个 IPv4 对等节点安装单独的 /32 路由,而不是为100.64.0.0/10安装一条聚合路由。仅设置TS_USERSPACE=false不会启用该聚合路由。只有通过TS_EXTRA_ARGS传递--accept-routes时,它才会安装其他节点的子网路由。TS_ROUTES的作用相反:它会向 tailnet 的其余设备通告此容器可访问的子网,作用等同于tailscale set --advertise-routes=,但不会在此处添加路由。公网流量仍会通过 gluetun 发出。 - 在 gluetun 的网络命名空间中使用内核网络,并选择退出节点
sudo tailscale set --exit-node=<exit-node-ip>:Tailscale 会在该命名空间中安装默认路由,但 Gluetun 的防火墙可能会阻止这条路径。在独立容器中选择退出节点不会更改应用的路由,而 userspace 模式只会承载显式发送到其代理的流量。不要在 gluetun 的网络命名空间中配置相互竞争的默认路由。只能有一条默认路由,也只能有一个管理者。
如果这些通告的路由才是目的,并且您希望访问整个盒子后方的私有网络,而不仅是该盒子本身,那么在 VPS 上运行 Tailscale 子网路由器可以处理路由批准、IP 转发,以及客户端侧需要设置的标志。仅设置 TS_ROUTES 无法完成后者。
如果使用 Tailscale 是为了提供管理 URL,而不是提供路由,那么tailscale serve 和 tailscale funnel可以在本地 HTTP 服务前置 HTTPS;只有 funnel 会将其开放到公网。在 Tailscale v1.104.5 中,当 Tailscale 容器可以通过共享 Docker 网络访问目标时,其 HTTP 反向代理可以直接接受 http://gluetun:8080。对于非 localhost 目标,请包含 http:// scheme。共享 gluetun 的网络命名空间并将目标设为 http://127.0.0.1:8080 也可以正常工作。
当 Tailscale 在隧道内部运行时,还会出现一个可观察的副作用。其对等节点看到的是 VPN 供应商的地址,因此它更容易回退到中继。发生这种情况时,tailscale status 会在对等节点旁显示 relay "...",而不是 direct。连接仍然可用,但速度会更慢。如果您实际只需要覆盖网络,那么普通 WireGuard 与 Tailscale 的区别是更合适的起点。
会出现哪些故障,以及对应的提示
Docker 拒绝创建应用容器。 Error response from daemon: conflicting options: port publishing and the container type network mode 表示附加到该服务的容器中仍有一个 ports: 块。将它移到 gluetun。
Compose 拒绝整个文件。 一个服务不能同时设置 network_mode 和 networks。将网络配置放在 gluetun 中。
其他容器无法解析应用。 curl: (6) Could not resolve host: qbittorrent 属于正常行为,因为附加容器未加入任何网络,也未注册名称。使用 gluetun 和端口。
第二个附加容器无法启动。 同一命名空间中的两个进程不能绑定同一个端口,后启动的进程会报告地址已在使用。更改应用的内部端口,或运行第二个 gluetun。
修改 gluetun 后,应用没有网络。 重启或重新创建 gluetun 会断开所有附加容器的网络连接。重启这些容器。
小页面可以加载,大页面却一直挂起。 这是 MTU(最大传输单元)问题。隧道会增加额外开销,路径中的某个环节会丢弃过大的数据包,且不会返回错误。检查 WIREGUARD_MTU。v3.41.3 中的默认值是 1320;如果路径要求使用更小的数据包,请调低该值。未设置此项时,开发镜像会使用路径 MTU 发现。
Gluetun 始终无法变为 healthy。 启动检查会列出最先应排查的对象:WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout。先检查密钥是否已过期,再检查服务器列表是否过时,最后确认主机防火墙是否阻止出站 UDP。
FAQ
我的容器通过 Gluetun 发布的端口为什么停止工作?
因为 network_mode: "service:gluetun" 会将容器放入 gluetun 的网络命名空间,而一个命名空间只有一个 IP 地址和一组监听端口。应用仍在监听,但端口发布规则必须配置在拥有该命名空间的容器上。将 ports: 列表移到 gluetun 服务中。如果该列表仍留在附加服务上,Docker 甚至不会创建它:Error response from daemon: conflicting options: port publishing and the container type network mode。
如何从 VPN 隧道外部的容器访问隧道内的容器?
使用 gluetun 的服务名和应用监听的端口,例如 gluetun:8080。附加容器不属于任何独立的 Docker 网络,因此其自身名称永远无法解析。容器之间的流量无需发布端口。反过来,在 Gluetun v3.41 及更高版本中,命名空间内的容器可使用外部容器的服务名访问该容器,例如 postgres:5432。如果客户端位于不同子网,例如 LAN 中的笔记本电脑,则需要允许 gluetun 防火墙和路由提供返回路径;将该子网添加到 FIREWALL_OUTBOUND_SUBNETS。
VPN 断开时,Gluetun 是否可以充当终止开关?
可以,但必须启用其防火墙。附加容器会共享 Gluetun 的网络接口和路由。防火墙会阻止隧道外的普通互联网流量,同时允许访问 VPN 服务器端点、本地 Docker 流量和已配置的例外。Gluetun 会在内部重新启动 VPN,并记录 WARN [vpn] restarting VPN because it failed to pass the healthcheck,而不是退出,因为 gluetun 自身重启时,所有附加容器都会失去网络连接。
Tailscale 和 Gluetun 位于同一堆栈中时,哪个负责出站流量?
除非明确通过 Tailscale 发送,否则 Gluetun 负责普通出站流量。默认情况下,Tailscale 只在 tailnet 中的设备之间路由流量,不处理公网流量。在容器镜像的默认用户态模式下,它根本不会创建网络接口,因此不会影响路由。使用 TS_USERSPACE=false 时,Linux 通常会为各个 IPv4 对等节点创建单独的 /32 路由,而不是聚合的 100.64.0.0/10 路由;只有通过 --accept-routes 和 TS_EXTRA_ARGS 添加后,才会包含其他节点的子网路由。TS_ROUTES 会将此容器的子网发布到 tailnet,但不会添加本地路由。使用 sudo tailscale set --exit-node=<exit-node-ip> 选择出口节点时,只有采用 Tailscale 内核网络的命名空间才会安装默认路由;独立容器不会改变应用的路由,而用户态模式只会承载通过其代理发送的流量。应选择一个产品负责默认路由,不要同时叠加两者。