Gluetun 如何連線主機與其他容器?
使用 gluetun network namespace 的容器沒有自己的介面。請在 gluetun 上發布連接埠,並只放行隧道外必須連線的子網路。
容器加入 gluetun 的網路後會發生什麼事
設定 network_mode: service:gluetun 的容器沒有自己的網路介面。它會加入 gluetun 的 network namespace,因此連接埠發布與防火牆規則不再屬於該容器,而是屬於 gluetun 服務。以下所有答案都源自這項事實。
network namespace 是核心中的私有網路堆疊副本,包含自己的介面、路由表、防火牆規則與 listening socket。Docker 預設會為每個容器建立一個 network namespace。指定 network_mode: service:gluetun 時,Docker 會略過這個步驟,並將新容器放入 gluetun 已擁有的 network namespace。該容器仍保有自己的檔案系統與自己的 /etc/hosts 檔案,後者稍後會發揮作用。
你可以直接查看。
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent這會輸出 container:,後面接著 gluetun 容器 ID;一般容器則會輸出 bridge。本指南接續 使用 gluetun 將 Docker 流量透過 VPN 路由 的內容:tunnel 已正常運作,但現在沒有任何連線能到達該容器。
在 gluetun 上發布連接埠,不要在應用程式上發布
在設定 network_mode 的服務上保留 ports: 區塊,Docker 會拒絕建立容器:
Error response from daemon: conflicting options: port publishing and the container type network mode原因很直接。發布連接埠會新增 NAT(network address translation)規則,將主機連接埠轉送到容器自身的 network namespace;但這個容器沒有自己的 network namespace。請將對應設定移至 gluetun 服務。連接埠號碼不變,因為應用程式仍在共用的 namespace 內監聽該連接埠。
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block here在相依服務上設定 expose: 區塊同樣沒有作用,而設定 networks: 區塊則會直接停止載入:Compose 會回報該服務同時宣告互斥的 network_mode 與 networks,並拒絕載入整個檔案。
另一個問題可能稍後才出現。namespace 中的所有容器共用同一個連接埠空間,因此兩個預設使用 8080 的應用程式會發生衝突,後啟動的應用程式會因 address already in use 錯誤而啟動失敗。請在其中一個應用程式自己的設定中變更連接埠,例如修改 LinuxServer qBittorrent image 的 WEBUI_PORT 變數,然後在 gluetun 上發布新的連接埠號碼。
容器如何在 gluetun 後方互相連線?
在該 network namespace 內,容器已共用 loopback 介面。位於 gluetun 後方的容器可透過 127.0.0.1:<port> 連線到同一組 namespace 中的兄弟容器,過程不涉及 Docker network。
從 namespace 外部看,該容器沒有名稱。Docker 內建 DNS 會將服務名稱解析為該服務在 user-defined network 上的位址,但此容器在任何 network 上都沒有位址。因此,Sonarr 這類一般容器無法透過 http://qbittorrent:8080 連線到 torrent client,而是透過 http://gluetun:8080 連線,因為 socket 正在 gluetun 的 namespace 中監聽,使用的是 gluetun 的位址。熟悉 Docker Compose network 與服務名稱運作方式 的使用者,通常會預期套用一般的命名方式,因此會對此感到意外。這也不需要將任何項目發布到 host,因為兩個容器位於同一個 Compose network。
在進行其他除錯前,先檢查 DNS。Gluetun 會執行自己的 resolver,並在自己的容器中改寫 /etc/resolv.conf;但 /etc/resolv.conf 是每個容器各自擁有的檔案,因此 gluetun 寫入的檔案並不是應用程式讀取的檔案。
docker exec qbittorrent cat /etc/resolv.conf如何連線至 Docker host 上執行的服務?
使用 host.docker.internal。這需要在兩個不同位置設定,因為有兩個不同問題需要修正。
先設定名稱。/etc/hosts 是每個容器各自的設定,因此 extra_hosts 項目應放在應用程式容器上,而不是 gluetun 上。
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway 是特殊值,Docker 會將它替換為 host 本身的內部位址。在純 Linux Docker 安裝中,這是 docker0 bridge 的位址,通常為 172.17.0.1。在 VPS 上執行 ip -4 addr show docker0 確認實際位址。Docker Desktop 會自行解析這個名稱,因此在筆記型電腦上撰寫的教學通常省略 extra_hosts 這一行,而同一份檔案放到伺服器上就會失效。
接著設定路由。只加入名稱,只能告訴容器要使用哪個位址。封包仍會經由 gluetun 的預設路由離開,而該路由是 tunnel;gluetun 的防火牆會丟棄封包。其症狀是連線持續等待,最後逾時,而不是立即遭到拒絕。拒絕表示封包已抵達,且有服務回應拒絕。逾時則表示封包從未抵達。
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32接著確認 host 服務確實在該位址上監聽。若 PostgreSQL 只綁定至 127.0.0.1,則無論是否使用 tunnel,任何容器都無法連線,因為 namespace 內的 127.0.0.1 是該 namespace 自己的 loopback。改為綁定至 172.17.0.1:這樣可接受來自容器的連線,同時不會暴露在公開介面上。在 host 上使用 ss -lntp | grep 5432 驗證設定。
FIREWALL_OUTBOUND_SUBNETS 實際變更的內容
gluetun 文件將其描述為以逗號分隔的子網路清單。這些子網路是 gluetun 及共用其 network stack 的容器可存取的目標。文件也指出,這會涉及防火牆與路由變更。這兩部分都很重要。gluetun 會為每個列出的子網路,透過 Docker bridge gateway 新增路由,因此前往這些位址的封包會經由 eth0 離開,而不是通過 tunnel。它也會為這些子網路開放防火牆,因為 gluetun 預設會丟棄不是前往 VPN server 的 outbound traffic。
設定值中的逗號後不要加空格。
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32有兩項特性容易被忽略。這是 namespace 層級的設定,因此會套用到 gluetun 後方的每個容器,而不只是你原本指定的容器。此外,這項設定只適用於 outbound traffic,控制的是由容器發起的連線。傳入 published port 的連線會經過不同路徑,因此不需要在此列出。
從 Tailscale 對等節點存取 Web UI
Tailscale 會為每台機器配置一個位於 100.64.0.0/10 的位址。這是保留給 carrier-grade NAT 的範圍。兩個方向需要不同的處理方式。
入站流量較簡單。在 gluetun 上發布 8080:8080,會將該連接埠繫結至主機的所有位址,其中也包括主機的 tailscale0 介面。因此,對等節點開啟 http://<machine-name>:8080 後即可連線至容器。這條路徑不會經過 gluetun,因為 Docker 的 NAT 規則位於主機上,不在該 network namespace 內。
若只允許透過 tailnet 存取 UI,請將發布的連接埠繫結至主機的 Tailscale 位址,而不是所有位址。
ports:
- "100.101.102.103:8080:8080/tcp"在主機上使用 tailscale ip -4 找出該位址。在此情況下,繫結比防火牆規則更能有效控制存取,因為該連接埠根本不會在公開介面上開放。這也能避開 Docker 直接繞過 ufw 發布連接埠 的問題。
出站流量則會回到 FIREWALL_OUTBOUND_SUBNETS。如果容器必須呼叫對等節點,請加入該對等節點的位址。對每個對等節點使用 /32,不要涵蓋整個 /10。MagicDNS 名稱不會在容器內解析,因為容器不使用主機的 resolver。因此,請使用數值形式的 100.x 位址,或以 extra_hosts 行固定該位址。使用 Headscale 執行自己的 Tailscale control server 時也一樣。
常見架構的完整 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 stack 採用相同分層方式,只是將資料庫移到 Docker 外部。
請勿將 WireGuard 私密金鑰放入 compose 檔案。${WIREGUARD_PRIVATE_KEY} 會從旁邊的 .env 檔案讀取金鑰,這就是 Docker Compose 的 env 檔案與 secrets 所介紹的做法。condition: service_healthy 子句會使用 gluetun image 已內建的 healthcheck,因此在 tunnel 回報已啟動前,不會啟動任何服務。Compose healthcheck 說明了一般寫法。
在所有位址上發佈,而不只是在 tailnet 上
移除位址前綴,以及 0.0.0.0 上的連接埠繫結;這會包含 VPS 的公開 IP。只有在你能控管防火牆的情況下才可這麼做,並請先閱讀上方的 ufw 注意事項。
ports:
- "8080:8080/tcp"確認通道仍在傳輸網路流量
在 namespace 內與主機上各執行一次相同的請求,然後比較結果。
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 gateway。任何經由 eth0 離開的其他路由,都是略過 VPN 的流量。
Gluetun 的 control server 會在 port 8000 的 /v1/publicip/ip 回報相同的 public IP。近期版本要求你為 control server routes 設定 authentication,因此請先完成設定,再依賴這項功能。
單一錯誤子網路造成的洩漏
FIREWALL_OUTBOUND_SUBNETS 是刻意在防火牆上開出的缺口,因此缺口大小就是風險範圍。以下有 4 種方式會讓缺口過大:
0.0.0.0/0會將所有流量送出 tunnel。上方的 2 次 IP 檢查會在第一次執行時發現這個問題,因為兩者會回傳相同的位址。- 範圍大於目標。為了連線至位於
10.0.1.7的一台機器而開放10.0.0.0/8,也會開放 torrent peer 可能在該範圍內公布的所有位址。請改用10.0.1.7/32。 - 範圍與 tunnel 自身的位址重疊。gluetun 文件警告,這會使 gluetun 改為透過 bridge 傳送 VPN 流量,導致 port forwarding 失效。開放任何 private range 前,請先檢查
WIREGUARD_ADDRESSES值。 - Tailscale 使用
100.64.0.0/10。這大約會開放 4 million 個位址,只為了讓一個 peer 可連線。請將需要連線的 peer 列為/32項目。
請記住,這項設定會套用至整個 namespace。開放一個子網路,讓 indexer 連線至 host service,也會為共用該 namespace 的 torrent client 開放相同子網路。每次變更此變數後,都要重新執行 public IP 檢查,因為只有這項測試能確認變更是否達到預期效果。
重新啟動 gluetun 時會發生什麼問題
Gluetun 擁有該 network namespace,因此 gluetun 的生命週期就是該 namespace 的生命週期。gluetun 停止時,立即啟動相依容器會失敗:
Error response from daemon: cannot join network of a non running container直接重新啟動 gluetun 會造成較不明顯的失效。相依容器會繼續執行,但它們所連接的 namespace 已在底層重建,因此 docker ps 會回報所有項目正常,實際上卻沒有任何服務回應。對 gluetun service 進行任何變更後,請重建整個群組,不要只重新啟動其中一個部分。
docker compose up -d --force-recreate映像檔更新也一樣。拉取新的 gluetun image 並只重建該 service,會讓其他容器繼續指向已不存在的 namespace。
FAQ
為什麼 Docker 會顯示「port publishing and the container type network mode」?
因為服務上仍有 ports: 區塊,同時也設定了 network_mode: service:gluetun。發布連接埠會新增 NAT 規則,將主機連接埠轉送至容器自己的 network namespace;但使用此模式的容器沒有自己的 network namespace。請從該服務刪除 ports: 區塊,並將完全相同的對映新增至 gluetun 服務。連接埠號碼維持不變,因為應用程式仍在共用的 namespace 中監聽該連接埠。
其他容器如何連線到位於 gluetun 後方的服務?
位於相同 namespace 的容器可透過 127.0.0.1 互相連線。位於 namespace 外部的容器則使用 gluetun 服務名稱,因此 http://gluetun:8080 可運作,而 http://qbittorrent:8080 無法運作。應用程式容器在任何 Docker network 上都沒有位址,因此內建 DNS 伺服器沒有可解析的名稱。只要兩個容器共用 Compose network,就不需要發布連接埠。
FIREWALL_OUTBOUND_SUBNETS 應該填入哪些內容?
只填入 gluetun 後方的容器必須主動建立連線的位址,並盡可能縮小範圍。單一主機應寫成 /32。最常見的兩個項目是 Docker 主機的 172.17.0.1/32,以及每個要連線之 Tailscale peer 各一個 /32。不要加入 0.0.0.0/0,也不要加入與 VPN 自己的 tunnel 位址重疊的範圍。對已發布連接埠的入站連線不需要在此加入項目。
為什麼容器無法解析我的 Tailscale MagicDNS 名稱?
MagicDNS 會將主機的 resolver 指向 Tailscale 的 DNS 伺服器,但容器不會使用主機的 resolver。容器會使用自己的 /etc/resolv.conf 所指定的設定;在 gluetun 後方時,使用的是 gluetun 的 DNS 設定。請使用 docker exec <container> cat /etc/resolv.conf 確認。請使用 peer 的數值 100.x 位址,或在該容器上透過 extra_hosts 項目固定名稱。
如何確認流量仍然經過 VPN?
在 namespace 內執行一次請求,再從主機執行相同請求,然後比較回應。docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org 應回傳 VPN provider 的出口位址,而 VPS 上的 curl -s https://api.ipify.org 應回傳 VPS 位址。兩個回應相同表示 tunnel 未承載容器的流量。每次變更 FIREWALL_OUTBOUND_SUBNETS 後,都要再次執行這項檢查。