Docker 容器透過 VPN 路由,連接埠消失怎麼辦
使用 Gluetun sidecar 與 network_mode: service:gluetun 時,應用程式的連接埠與 service name 會消失。了解原因及可用的 compose 設定。
透過 VPN 路由 Docker 容器時,連接埠為何會消失
要透過 VPN 路由 Docker 容器,先讓其中一個容器建立 tunnel,再使用 network_mode: "service:gluetun" 將其他容器連接到它的 network namespace。這個連接方式最容易讓人困惑。被連接的容器不再擁有自己的網路,因此原本發佈的連接埠與 Docker service name 也會一併消失。請改在 VPN 容器上發佈連接埠,其他容器則透過 VPN 容器的名稱連線到該應用程式。
如果在被連接的容器上保留 ports: 區塊,Docker 會直接拒絕建立該容器:
Error response from daemon: conflicting options: port publishing and the container type network mode這裡使用的工具是 Gluetun。它會透過 WireGuard 或 OpenVPN 連接商用 VPN(virtual private network)provider,並自行執行防火牆。2026 年 8 月的目前版本為 v3.41.3。範例使用 Mullvad 搭配 WireGuard,因此需要向 provider 取得帳號與 key。如果想在自有硬體上終止 tunnel,在 VPS 上執行自有 WireGuard server 可建立另一端;Docker 中的 wg-easy 則會以 web interface 封裝這項設定。
What network_mode: "service:gluetun" actually does
Every Docker container normally gets its own network namespace: its own interfaces, routing table, firewall rules and listening sockets. service: mode skips that step and starts the container inside gluetun's namespace. One namespace means one IP address, and that changes six things.
- The app has no address of its own. Its address is gluetun's address.
- The app is attached to no Docker network, so its service name is never registered and never resolves. Other containers must use
gluetun. - Containers inside the namespace reach each other over
localhost. - Two containers in one namespace cannot listen on the same port. The Gluetun documentation is blunt about this: there is no workaround.
- Capabilities belong to a container, not to a namespace. Gluetun holds
NET_ADMINand/dev/net/tunbecause it creates the tunnel interface. The attached container does not inherit them. - Compose rejects any file where one service sets both
network_modeandnetworks. Attach gluetun to your networks, and the app rides along.
Restarting gluetun disconnects everything attached to it. That is documented behaviour, and it is the reason gluetun restarts the VPN process inside the container instead of exiting when the connection fails. After you restart or recreate gluetun yourself, restart the containers attached to it.
可正常運作的 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 分支的最後一次 commit,也就是開發中的 edge 版本。因此,若不想在星期二除錯,請在該主機上固定使用 :v3。
WEBUI_PORT=8080 必須與發布的連接埠一致,因為 qBittorrent 會在 gluetun 的 network namespace 內進行 bind,而 publish 規則會將主機流量轉送到其中的 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。接著從該 network namespace 內確認對外位址。這項檢查的結果會決定其他所有項目:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"該 JSON 中的 ip 欄位應該是 VPN 供應商提供的位址。如果顯示的是伺服器本身的位址,表示應用程式未通過 tunnel,後續內容都不會如本文所述運作。
將金鑰排除在 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 中的環境檔案與 secret 說明更安全的選項。
容器如何與 tunnel 外部的容器通訊
兩個方向都可運作,但使用的名稱不同。兩個容器需要共用 Docker network,也就是 gluetun 的 network,因為附加的容器本身沒有獨立的 network。Docker Compose network 的連線方式說明了預設設定。
從外部連入內部時,使用 gluetun 的名稱,以及應用程式監聽的埠。反向代理容器可透過 gluetun:8080 連線到 qBittorrent web 介面。這不需要 ports: 設定,因為容器之間的流量會留在 Docker network,不會經過 host port。
從內部連到外部時,使用另一個容器的 service name,例如 postgres:5432。自 v3.41 起,Gluetun 可在自己的 namespace 內解析其他容器名稱。如果名稱無法解析,請固定使用這個版本或更新版本。
Gluetun 的 firewall 會決定哪些來源可向它建立連線。來自 gluetun 自有 Docker network 的流量會獲准通過。來自不同 subnet 的 client,例如 LAN 上的 laptop 或位於獨立 bridge network 的容器,必須先指定該 subnet,否則會被丟棄:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文件定義的含義很明確:以逗號分隔的 subnet,指定允許 Gluetun 及共用其 network stack 的容器存取的網段。
來自 internet 的 inbound connection 是另一個問題。torrent client 的 peer 會從 VPN 端連入,因此在 host 上發布 port 6881 對這些連線沒有作用。你需要向 provider 取得 forwarded port,並將該 port 列在 FIREWALL_VPN_INPUT_PORTS 中;這會允許來自 VPN server 端的連接埠。這是大多數使用 Docker Compose 建置的 media stack都未正確處理的部分。
Tunnel 斷線時的 kill switch:系統會發生什麼事
這個架構的複雜度在發生故障時就能發揮作用。連接的容器沒有第二條路由。它離開主機的唯一通道是共用的 namespace,因此 tunnel 停止時沒有其他路徑可用。Gluetun 的防火牆也從另一端強制執行相同規則:對外流量只能經由 tunnel 或 VPN server endpoint 傳送,其他流量一律丟棄。用戶端重新連線期間,不會出現封包從一般介面洩漏出去的時間窗口。
Gluetun 會監控自己的連線。它每分鐘會向 HEALTH_ICMP_TARGET_IPS 中的位址傳送 ICMP echo(ping),預設為 1.1.1.1,8.8.8.8。它每 5 分鐘會對 HEALTH_TARGET_ADDRESSES 建立完整的 TCP 與 TLS(transport layer security)連線,預設為 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 這類訊息,是 tunnel 中斷的結果,不是原因。Gluetun 文件明確說明了這一點,因為人們常會回報結果,並花數小時追查錯誤方向。
HEALTH_RESTART_VPN=on 是預設設定,應保持啟用。只有在偵錯某個特定故障時才暫時關閉,因為關閉後,中斷的 tunnel 會持續維持中斷狀態。
排序:在 tunnel 建立前不要啟動 stack
映像檔內建 Docker healthcheck:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck這個命令會啟動另一個短暫執行的 gluetun 副本,查詢執行中 gluetun 的 health server,位置在 http://127.0.0.1:9999/。tunnel 正常時會回應 200 OK。tunnel 故障時會回應 500 Internal server error 並附帶錯誤字串;只要失敗一次,容器就會被標記為不健康。
condition: service_healthy 會等待這個條件。單純使用 depends_on: [gluetun] 時,只會等待容器啟動。容器通常會在 handshake 完成前數秒啟動,因此應用程式會在網路不可用的狀態下啟動,並經常在第一次連線嘗試時直接放棄。Docker Compose 中的 healthcheck 說明語法與計時欄位。
有一項限制容易被忽略。Compose 只會在建立容器時評估一次這個條件。之後如果 gluetun 變成不健康,Compose 不會停止或重新啟動應用程式。這種情況由 gluetun 內部的自動修復機制處理。因此它會重新啟動 VPN 程序,而不是重新啟動容器。
在信任設定前檢查 DNS 洩漏
DNS(網域名稱系統)是即使通道設定正確仍可能存在的洩漏。Gluetun 會在 namespace 內執行自己的 resolver,並依預設透過 DoT(DNS over TLS)將查詢轉送至 Cloudflare:DNS_UPSTREAM_RESOLVER_TYPE=dot 和 DNS_UPSTREAM_RESOLVERS=cloudflare。保留這兩項設定,查詢就會經過加密並透過通道傳送。
會造成問題的設定是 DNS_UPSTREAM_PLAIN_ADDRESSES。當名稱無法解析時,有些人會啟用這項設定,改由路由器或服務提供者的 resolver 回應。Gluetun 文件明確說明其代價:所有 DNS 流量都不會經過 VPN 通道,而會從通道外洩漏。你的流量仍受隱私保護,但主機名稱清單不會。WireGuard 通道中的相同錯誤,請參閱 WireGuard 通道上的 DNS 無法解析。
若要測試,請在 gluetun 設定 HTTPPROXY=on 並發布 8888:8888/tcp,然後將瀏覽器指向該 proxy,載入 DNS 洩漏測試頁面。結果應顯示你的服務提供者或 Cloudflare,絕不應顯示家用路由器。Gluetun 自己的文件提醒,部分洩漏測試可能會回報異常結果,因為 namespace 內的 resolver 是本機快取中介,而不是最後提供回應的伺服器。若顯示的國家錯誤,或出現你自己的 ISP resolver,才應視為真正的警訊。
在 VPN sidecar 旁加入 Tailscale,以及哪一個會生效
Tailscale 是建構在 WireGuard 上的 overlay network,用來連線到自己的機器。許多人會讓它與 provider VPN 並行,以保留進入整個 stack 的管理路徑。兩者很少互相衝突,原因值得了解。Tailscale 的文件說明了預設行為:它是 overlay network,只會在執行 Tailscale 的裝置之間路由流量,不會處理公開網際網路流量。
因此,答案取決於一項設定。
- Tailscale 使用自己的 container,採用預設設定:它完全看不到 app 的對外流量。所有流量都由 Gluetun 傳送。Tailscale 會透過
gluetun:8080連到 app,與其他外部 container 相同。 - Tailscale 連接到 gluetun 的 namespace,並使用
network_mode: "service:gluetun":它需要自行設定cap_add、net_admin與net_raw,因為 capabilities 不會隨 namespace 一起提供。在預設的 userspace networking mode 中,TS_USERSPACE已啟用,tailscaled 完全不會建立 interface,而是作為 SOCKS5 或 HTTP proxy 運作,因此無法變更路由。所有流量仍由 Gluetun 傳送。 - 使用相同設定,但加入
TS_USERSPACE=false:tailscaled 會建立 tunnel device 並安裝路由,但只會處理 tailnet 範圍100.64.0.0/10,以及透過TS_ROUTES宣告的任何 subnet route。公開流量仍會經由 gluetun 離開。 - 上述任一情況選取 exit node,使用
sudo tailscale set --exit-node=<exit-node-ip>:Tailscale 會接管 default route 並生效。不要將此設定與 gluetun 一起使用。只能有一條 default route,也只能有一個負責者。
Tailscale 在 tunnel 內執行時,還會產生一項可觀察的影響。它的 peer 會看到 VPN provider 的位址,因此更常退回使用 relay。發生這種情況時,tailscale status 會在 peer 旁顯示 relay "...",而不是 direct。連線仍可運作,但速度較慢。如果你實際需要的只有 overlay,plain 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 和連接埠。
第二個附加的容器無法啟動。 同一個 namespace 中的兩個程序不能繫結相同的連接埠,後啟動的程序會回報位址已在使用中。請變更應用程式的內部連接埠,或執行第二個 gluetun。
您修改 gluetun 後,應用程式無法連線網路。 重新啟動或重新建立 gluetun,會中斷所有附加至它的容器連線。請重新啟動這些容器。
小型頁面可以載入,大型頁面卻停滯。 這是 MTU(maximum transmission unit)問題。通道會增加額外負載,路徑中的某個元件會丟棄過大的封包,卻不回傳錯誤。請降低 WIREGUARD_MTU,嘗試 1400,然後嘗試 1320。
Gluetun 始終無法進入健康狀態。 啟動檢查列出最先應檢查的項目: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 的 network namespace,而一個 namespace 只有一個 IP 位址和一組 listening ports。應用程式仍會持續監聽,但 publish rule 必須設定在擁有該 namespace 的容器上。將 ports: 清單移至 gluetun service。如果仍將它保留在附加的 service 上,Docker 甚至不會建立該規則:Error response from daemon: conflicting options: port publishing and the container type network mode。
如何從 VPN tunnel 外部的容器連線到 VPN tunnel 內部的容器?
使用 gluetun 的 service name,以及應用程式監聽的 port,例如 gluetun:8080。附加的容器不屬於任何 Docker network,因此其自身名稱永遠無法解析。容器之間的 traffic 不需要發布 port。反過來,在 Gluetun v3.41 及更新版本中,namespace 內的容器可透過外部容器的 service name 連線,例如 postgres:5432。不同 subnet 上的 client,例如 LAN 上的 laptop,會被 gluetun 的 firewall 丟棄,直到將該 subnet 加入 FIREWALL_OUTBOUND_SUBNETS。
Gluetun 在 VPN 中斷時是否可作為 kill switch?
可以,原因有兩個。附加的容器除了 shared namespace 中的路由之外沒有其他路由,因此 tunnel 中斷後便沒有離開該機器的路徑。Gluetun 的 firewall 也只允許透過 tunnel,以及前往 VPN server endpoint 的 outbound traffic。Gluetun 會在內部重新啟動 VPN,並記錄 WARN [vpn] restarting VPN because it failed to pass the healthcheck,而不是退出,因為 gluetun 本身重新啟動時,所有附加的容器都會失去 network。
Tailscale 和 Gluetun 位於同一個 stack 時,哪一個會承載 outbound traffic?
除了一種設定外,都是 Gluetun。Tailscale 預設只會在 tailnet 中的裝置之間路由 traffic,不會處理 public traffic。在 container image 的預設 userspace mode 中,它完全不會建立 interface,因此無法影響 routing。使用 TS_USERSPACE=false 時,它只會為 100.64.0.0/10 和你宣告的 subnet 安裝 routes。例外是 exit node:sudo tailscale set --exit-node=<exit-node-ip> 會讓 Tailscale 成為 default route,接著由它取得路由控制權。請選擇一個產品負責 default route,不要同時疊加兩者。