Docker 容器通过 VPN 路由后端口消失怎么办
使用 Gluetun sidecar 和 network_mode: service:gluetun 时,容器为何没有独立端口与服务名?了解端口发布位置、Compose 冲突及可用配置。
将 Docker 容器的流量通过 VPN 路由后端口为何会消失
要将 Docker 容器的流量通过 VPN 路由,先让一个容器建立隧道,再使用 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(虚拟专用网络)提供商,并自带防火墙。截至 2026 年 8 月,当前版本为 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 网络中的容器,会被丢弃,除非明确指定该子网:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文档中的含义很明确:使用逗号分隔的子网列表,允许 Gluetun 以及共享其网络栈的容器访问这些子网。
来自互联网的入站连接是另一个问题。Torrent 客户端的对端会从 VPN 侧连接,因此在主机上发布 6881 端口对这些连接不起作用。你需要从 VPN 提供商处获取转发端口,并将该端口列入 FIREWALL_VPN_INPUT_PORTS;该配置允许来自 VPN 服务器侧的端口。这是大多数使用 Docker Compose 构建的媒体栈都会遗漏的部分。
紧急断网开关:隧道断开时会发生什么
这种模式的复杂性在故障时才有价值。附加的容器没有第二条路由。它离开此计算机的唯一通道是所共享的网络命名空间,因此隧道断开时没有备用路径可用。Gluetun 的防火墙从另一侧强制执行相同规则:出站流量必须经过隧道,或发送到 VPN 服务器端点,其他流量全部丢弃。客户端重新连接期间,不会出现数据包通过普通接口泄漏的窗口。
Gluetun 会监控自身的连接。它每分钟向 HEALTH_ICMP_TARGET_IPS 中的地址发送 ICMP 回显请求(ping),这些地址默认设置为 1.1.1.1,8.8.8.8。它每 5 分钟向 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 是默认设置,应保持启用。只有在调试某个特定故障时才暂时关闭它,因为关闭后,失效的隧道会一直保持失效。
排序:在隧道建立后再启动服务栈
该镜像内置了 Docker 健康检查:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck该命令会启动 gluetun 的另一个短生命周期副本,并查询运行中实例在 http://127.0.0.1:9999/ 提供的健康检查服务。隧道正常时会返回 200 OK。隧道故障时会返回带错误字符串的 500 Internal server error;只要检查失败一次,容器就会被标记为不健康。
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 文档明确说明了代价:所有 DNS 流量都不会经过 VPN 隧道,而会从隧道外泄漏。网络流量仍保持私密,但主机名列表不会。WireGuard 隧道中的同类错误见WireGuard 隧道中 DNS 停止解析。
要进行测试,请在 gluetun 上设置 HTTPPROXY=on 并发布 8888:8888/tcp,然后让浏览器访问该代理并加载 DNS 泄漏测试页面。结果应显示您的网络服务提供商或 Cloudflare,不能显示家庭路由器。Gluetun 文档还提醒,某些泄漏测试可能显示异常结果,因为网络命名空间内的解析器是本地缓存中间层,而不是最终提供响应的服务器。应将显示错误国家或您自己的 ISP 解析器视为实际泄漏信号。
在 VPN sidecar 旁添加 Tailscale,以及哪个配置优先
Tailscale 是基于 WireGuard 构建的覆盖网络,用于访问您自己的设备。很多人会将它与提供商 VPN 并行运行,以便保留进入整个服务栈的管理通道。两者很少发生冲突,原因值得了解。Tailscale 文档说明了默认行为:它作为覆盖网络运行,只在运行 Tailscale 的设备之间路由流量,不会处理您的公网流量。
因此,答案取决于一项设置。
- Tailscale 运行在独立容器中,使用默认配置:它不会看到应用的出站流量。所有流量都由 Gluetun 承载。Tailscale 可通过
gluetun:8080访问应用,其方式与其他外部容器相同。 - Tailscale 加入 gluetun 的网络命名空间,并使用
network_mode: "service:gluetun":它仍需要自己的cap_add、net_admin和net_raw,因为网络命名空间不会自动提供 capabilities。在默认的 userspace networking 模式下,TS_USERSPACE已启用,tailscaled 不会创建任何接口,而是作为 SOCKS5 或 HTTP 代理运行,因此无法更改路由。所有流量仍由 Gluetun 承载。 - 使用相同配置,但启用
TS_USERSPACE=false:tailscaled 会创建隧道设备并安装路由,但只针对 tailnet 网段100.64.0.0/10,以及您通过TS_ROUTES公告的任何子网路由。公网流量仍通过 gluetun 发送。 - 上述任一配置选择了出口节点
sudo tailscale set --exit-node=<exit-node-ip>:Tailscale 会接管默认路由并获得优先权。不要将此配置与 gluetun 组合使用。默认路由只能有一个所有者。
当 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,尝试 1400,然后尝试 1320。
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 能否充当 kill switch?
可以,原因有两个。附加容器除了共享命名空间中的路由外没有其他路由,因此隧道断开后,它无法离开本机。Gluetun 的防火墙还只允许通过隧道和 VPN 服务器端点发送出站流量。随后 Gluetun 会在内部重启 VPN,并记录 WARN [vpn] restarting VPN because it failed to pass the healthcheck,而不是退出,因为 gluetun 自身重启时,所有附加容器都会失去网络连接。
在同一个堆栈中同时使用 Tailscale 和 Gluetun:哪个负责出站流量?
除一种情况外,始终由 Gluetun 负责。Tailscale 默认只在 tailnet 中的设备之间路由流量,不处理公网流量。在容器镜像的默认 userspace 模式下,它完全不会创建网络接口,因此无法影响路由。使用 TS_USERSPACE=false 时,它只会为 100.64.0.0/10 和已发布的子网安装路由。例外情况是 exit node:sudo tailscale set --exit-node=<exit-node-ip> 会使 Tailscale 成为默认路由,此时由 Tailscale 接管。应选择一个产品负责默认路由,不要同时叠加两者。