Docker 容器透過 VPN 路由:連接埠消失怎麼辦
使用 Gluetun sidecar 搭配 network_mode: service:gluetun 時,連接埠與服務名稱會消失。了解共享 network namespace 的原因,以及可正常運作的 Compose 設定。
透過 VPN 路由 Docker 容器時,連接埠為何會消失
若要透過 VPN 路由 Docker 容器,請先讓其中一個容器建立 VPN 通道,再使用 network_mode: "service:gluetun" 將其他容器連接到它的 network namespace。這個連接方式最容易讓人困惑。連接後的容器不再擁有自己的網路,因此原本發布的連接埠和 Docker 服務名稱也會一併消失。請改在 VPN 容器上發布連接埠,其他容器則透過 VPN 容器的名稱連線到該應用程式。
若在已連接的容器上保留 ports: 區塊,Docker 會直接拒絕建立該容器:
Error response from daemon: conflicting options: port publishing and the container type network mode這裡使用的工具是 Gluetun。它是一個可透過 WireGuard 或 OpenVPN 連線至商用 VPN(虛擬私人網路)提供者的容器,並內建防火牆。截至 August 2026,目前版本為 v3.41.3。範例使用 WireGuard 搭配 Mullvad,因此需要向提供者取得帳戶和金鑰。若您希望在自有硬體上終止 VPN 通道,可參考 在 VPS 上執行自有的 WireGuard 伺服器來建立通道的另一端;Docker 中的 wg-easy則會以 Web 介面包裝這項設定。
network_mode: "service:gluetun" 的實際作用
每個 Docker 容器通常都有自己的 network namespace,包括自己的網路介面、路由表、防火牆規則與監聽中的 socket。service: 模式會略過這個步驟,改為在 gluetun 的 namespace 中啟動容器。使用同一個 namespace 就代表共用同一個 IP address,這會造成 6 項變化。
- 應用程式沒有自己的 address,而是使用 gluetun 的 address。
- 應用程式不會連接至任何 Docker network,因此不會註冊服務名稱,也無法解析服務名稱。其他容器必須使用
gluetun。 - namespace 內的容器會透過
localhost彼此連線。 - 同一個 namespace 中的 2 個容器無法監聽相同的 port。Gluetun 文件對此說明得很直接:沒有替代解法。
- capabilities 屬於容器,而不是 namespace。Gluetun 會持有
NET_ADMIN與/dev/net/tun,因為它負責建立 tunnel interface。附加的容器不會繼承這些 capabilities。 - 如果某個 service 同時設定
network_mode與networks,Compose 會拒絕該檔案。請將 gluetun 連接至各個 network,應用程式就會一併使用這些 network。
重新啟動 gluetun 會中斷所有附加至它的容器。這是文件記載的行為,也是 gluetun 在連線失敗時,選擇在容器內重新啟動 VPN process,而不是直接結束的原因。若自行重新啟動或重新建立 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 分支的最後一次 commit,也就是開發中的 edge 版本。因此,若不想在週二進行除錯,請在該機器上固定使用 :v3。
WEBUI_PORT=8080 必須與已發布的連接埠一致,因為 qBittorrent 會在 gluetun 的 namespace 內繫結,而 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。接著從該 namespace 內確認出口位址;這項檢查會決定其餘所有結果:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"該 JSON 中的 ip 欄位應為 VPN provider 的位址。若顯示的是伺服器本身的位址,表示應用程式未位於 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 說明更強的選項。
容器如何與通道外的容器互通
兩個方向都可運作,但使用的名稱不同。這兩個容器需要共用 Docker network,也就是 gluetun 的 network,因為附加的容器本身沒有自己的 network。Docker Compose 如何連接 network 說明了預設設定。
從外部連入內部時,使用 gluetun 的名稱,以及應用程式監聽的埠。反向代理容器可透過 gluetun:8080 存取 qBittorrent Web 介面。這不需要 ports: 設定,因為容器之間的流量會留在 Docker network 上,不會經過主機埠。
從內部連到外部時,使用另一個容器的 service name,例如 postgres:5432。自 v3.41 起,Gluetun 已能在自己的 namespace 中解析其他容器名稱。如果名稱無法解析,請固定使用該版本或更新版本。
Gluetun 的防火牆會決定哪些來源可以向它建立連線。來自 gluetun 自己 Docker network 的流量會獲准通過。不同子網路上的用戶端、LAN 上的筆電,或位於不同 bridge network 的容器,都會遭到丟棄,除非你指定該子網路:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文件所述的意義很明確:以逗號分隔的子網路清單,列出允許 Gluetun 及共用其 network stack 的容器存取的來源。
來自網際網路的入站連線是另一個問題。torrent client 的對等端會從 VPN 端連入,因此在主機上發布 6881 埠對它們沒有作用。你需要向 VPN provider 取得 forwarded port,並將該埠列在 FIREWALL_VPN_INPUT_PORTS 中;這會允許來自 VPN server 端的連接埠。這是大多數 使用 Docker Compose 建立的媒體 stack 都未處理好的部分。
Tunnel 中斷時的 kill switch 行為
這種架構的複雜度在故障時才真正發揮作用。連接的容器沒有第二條路由。它離開主機的唯一途徑是所共用的 namespace,因此 tunnel 中斷時沒有其他路徑可供切換。Gluetun 的防火牆從另一端強制套用相同規則:對外流量必須經由 tunnel,或前往 VPN server endpoint;其他流量一律丟棄。client 重新連線期間,不會出現封包從一般介面洩漏出去的空窗。
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 會持續維持中斷。
Ordering:在 tunnel 建立前避免啟動 stack
映像檔提供 Docker healthcheck:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck該指令會執行另一個短暫存活的 gluetun 副本,查詢執行中 gluetun 在 http://127.0.0.1:9999/ 的 health server。正常運作的 tunnel 會回應 200 OK。發生問題時會回應 500 Internal server error 及錯誤字串;單次失敗後,容器就會被標記為 unhealthy。
condition: service_healthy 會等待該條件。單純使用 depends_on: [gluetun] 時,只會等待容器啟動。容器通常會在 handshake 完成前數秒啟動,因此應用程式會在網路尚未正常運作時啟動,並經常在第一次連線嘗試時直接放棄。Docker Compose 中的 healthcheck 說明語法與 timing 欄位。
有一項限制容易造成誤解。Compose 只會在建立容器時評估該條件。之後如果 gluetun 變成 unhealthy,Compose 不會停止或重新啟動應用程式。這種情況由 gluetun 內部的 auto-healing 處理,因此它會重新啟動 VPN process,而不是重新啟動容器。
在信任設定前檢查 DNS 洩漏
DNS(網域名稱系統)是即使通道設定正確仍可能存在的洩漏。Gluetun 會在 namespace 內執行自己的解析器,並預設透過 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,接著將瀏覽器指向該 proxy,再載入 DNS 洩漏測試。結果應顯示您的網際網路服務供應商或 Cloudflare,不應顯示家用路由器。Gluetun 文件也提醒,部分洩漏測試可能顯示異常結果,因為 namespace 內的解析器是本機快取中介,而不是最後提供回應的伺服器。請將錯誤的國家/地區或自家 ISP 的解析器視為真正的警訊。
在 VPN sidecar 旁加入 Tailscale,以及哪一個會優先
Tailscale 是建構在 WireGuard 上的 overlay network,用來連線到自己的機器。許多人會讓它與 provider VPN 並行,以便保留通往整個堆疊的管理路徑。兩者很少互相衝突,原因值得了解。Tailscale 的文件說明了預設行為:它會作為 overlay network,僅在執行 Tailscale 的裝置之間路由流量,不會處理公開網際網路流量。
因此,答案取決於一項設定。
- Tailscale 使用獨立容器,採用預設設定:它完全看不到應用程式的對外流量。所有流量都由 Gluetun 傳送。Tailscale 可透過
gluetun:8080存取應用程式,方式與任何其他外部容器相同。 - Tailscale 透過
network_mode: "service:gluetun"附加至 gluetun 的 namespace:它需要自己的cap_add、net_admin和net_raw,因為 namespace 不會一併提供 capabilities。在預設的 userspace networking 模式中,TS_USERSPACE已啟用,tailscaled 完全不會建立介面,而是作為 SOCKS5 或 HTTP proxy 運作,因此無法變更路由。所有流量仍由 Gluetun 傳送。 - 使用相同設定,但加入
TS_USERSPACE=false:tailscaled 會建立 tunnel device 並安裝路由,但只會涵蓋 tailnet 範圍100.64.0.0/10,以及透過TS_ROUTES宣告的任何 subnet routes。公開流量仍會經由 gluetun 傳出。 - 上述任一情況若選取 exit node,即使用
sudo tailscale set --exit-node=<exit-node-ip>:Tailscale 會取得預設路由並優先處理。不要將此設定與 gluetun 結合。只能有一條預設路由,也只能由一個元件負責。
如果這些宣告的路由才是你的目的,而且你希望能連線到 box 後方的整個私有網路,而不只是該 box,請參閱 在 VPS 上執行 Tailscale subnet router。其中涵蓋路由核准、IP forwarding,以及 TS_ROUTES 單獨執行時未完成的用戶端旗標設定。
如果使用 Tailscale 是為了提供管理 URL,而不是建立路由,tailscale serve 和 tailscale funnel 會在你的 tailnet 前方為 gluetun:8080 提供 HTTPS,只有 funnel 會將其開放至公開網際網路。
Tailscale 在 tunnel 內執行時,會產生一項可觀察的副作用。其對等端會看到 VPN provider 的位址,因此預期它更常退回使用 relay。發生此情況時,tailscale status 會在對等端旁顯示 relay "...",而不是 direct。連線仍可運作,但速度較慢。如果你實際需要的只有 overlay,plain WireGuard 與 Tailscale 的差異會是更合適的起點。
發生的問題與會看到的訊息
Docker 拒絕建立 app 容器。 Error response from daemon: conflicting options: port publishing and the container type network mode 表示附加的服務上仍有一個 ports: 區塊。將它移至 gluetun。
Compose 拒絕整個檔案。 服務不能同時設定 network_mode 和 networks。將網路設定放在 gluetun 上。
其他容器無法解析 app。 curl: (6) Could not resolve host: qbittorrent 是正常行為,因為附加的容器未加入任何網路,也未註冊名稱。使用 gluetun 和連接埠。
第二個附加的容器無法啟動。 同一個 namespace 中的兩個程序不能繫結相同的連接埠。無法繫結的程序會回報位址已在使用中。修改 app 的內部連接埠,或再執行一個 gluetun。
修改 gluetun 後,app 沒有網路。 重新啟動或重新建立 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 位址和一組監聽中的連接埠。應用程式仍在監聽,但發布規則必須設定在擁有該 namespace 的容器上。請將 ports: 清單移至 gluetun 服務。如果仍將它設定在附加的服務上,Docker 甚至不會建立該規則:Error response from daemon: conflicting options: port publishing and the container type network mode。
如何從 VPN tunnel 外部的容器連線到 VPN tunnel 內部的容器?
請使用 gluetun 的服務名稱,以及應用程式監聽的連接埠,例如 gluetun:8080。附加的容器不屬於任何 Docker network,因此無法解析自己的名稱。容器對容器的流量不需要發布連接埠。反過來,namespace 內部的容器可以透過服務名稱連線到外部容器,例如在 Gluetun v3.41 和更新版本中使用 postgres:5432。不同子網路上的用戶端(例如 LAN 上的筆記型電腦)會被 gluetun 的防火牆丟棄,直到您將該子網路加入 FIREWALL_OUTBOUND_SUBNETS。
Gluetun 在 VPN 中斷時能否作為 kill switch?
可以,原因有兩個。附加的容器除了共用 namespace 中的路由之外沒有其他路由,因此 tunnel 中斷後便沒有離開主機的路徑。Gluetun 的防火牆也只允許流量透過 tunnel 或前往 VPN 伺服器端點。Gluetun 會在內部重新啟動 VPN,並記錄 WARN [vpn] restarting VPN because it failed to pass the healthcheck,而不是直接結束程序,因為 gluetun 本身重新啟動時,所有附加的容器都會失去網路連線。
在同一個 stack 中同時使用 Tailscale 和 Gluetun:哪一個會承載對外流量?
除了以下一種情況外,都是 Gluetun。Tailscale 預設只會在 tailnet 中的裝置之間路由流量,不會處理公開網路流量。在容器映像檔的預設 userspace 模式中,Tailscale 完全不會建立介面,因此無法影響路由。使用 TS_USERSPACE=false 時,它只會為 100.64.0.0/10 和您公布的子網路安裝路由。例外是 exit node:sudo tailscale set --exit-node=<exit-node-ip> 會讓 Tailscale 成為預設路由,之後便由 Tailscale 優先處理。請選擇一個產品管理預設路由,不要同時堆疊兩者。