Gluetun 如何連線主機與其他容器
使用 gluetun 網路的容器沒有獨立介面,請在 gluetun 發布連接埠,並只開放必須繞過 VPN 通道的子網路。
容器加入 gluetun 的網路後會發生什麼事
設定 network_mode: service:gluetun 的容器沒有自己的網路介面。它會加入 gluetun 的 network namespace,因此連接埠發佈與防火牆規則不再屬於該容器,而會成為 gluetun 服務的屬性。以下所有結論都源自這項事實。
network namespace 是核心的私人網路堆疊副本,包含自己的介面、路由表、防火牆規則及監聽中的 socket。Docker 預設會為每個容器建立一個 network namespace。指定 network_mode: service:gluetun 時,Docker 會略過這個步驟,直接將新容器放入 gluetun 已擁有的 network namespace。容器仍保有自己的檔案系統,但沒有自己的 /etc/hosts 或 /etc/resolv.conf。Docker 會將 gluetun 使用的相同檔案交給它,而這一點稍後會產生影響。
你可以直接查看。
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrent這會輸出 container:,接著輸出 gluetun 容器 ID;一般容器則會輸出 bridge。本指南承接透過 gluetun 將 Docker 流量路由至 VPN的內容:通道運作正常,但現在沒有任何項目能與該容器通訊。
在 gluetun 上發布連接埠,不要在應用程式上發布
在設定 network_mode 的服務上保留 ports: 區塊時,Docker 會拒絕建立容器:
Error response from daemon: conflicting options: port publishing and the container type network mode原因很直接。發布連接埠會加入 NAT(網路位址轉換)規則,將主機連接埠轉送至容器自己的 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: 區塊也會以相同方式失敗,並顯示 conflicting options: port exposing and the container type network mode;在該服務上加入 networks: 區塊也會直接停止處理:Compose 會回報該服務同時宣告互斥的 network_mode 與 networks,並完全拒絕載入檔案。
另一個問題會在稍後出現。namespace 中的所有容器都共用同一個連接埠空間,因此兩個預設使用 8080 的應用程式會發生衝突,後啟動的應用程式會因 address already in use 錯誤而啟動失敗。請在其中一個應用程式自己的設定中變更連接埠,例如修改 LinuxServer qBittorrent 映像中的 WEBUI_PORT 變數,然後在 gluetun 上發布新的連接埠號碼。
Gluetun 後方的容器如何彼此連線?
在該 namespace 內,這些容器已共用 loopback 介面。Gluetun 後方的容器可透過 127.0.0.1:<port> 連線到同層的容器,過程不涉及 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 與服務名稱運作方式,且預期一般命名規則仍適用的使用者,通常會對此感到意外。由於兩個容器位於同一個 Compose network,也不需要將任何連接埠發布到主機。
在進行其他疑難排解前,先檢查 DNS。Gluetun 會在 namespace 內執行自己的 DNS server,而 Docker 會讓 gluetun 後方的每個容器使用與 gluetun 相同的 /etc/resolv.conf 檔案,因此應用程式的查詢會經由 gluetun 的 resolver,而不是主機的 resolver。此 resolver 預設也會拒絕解析結果指向 private address 的回應,因此某個名稱可能能在主機上解析,卻在容器內解析失敗。gluetun 文件指出,這類 hostname 必須列在 DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES。
docker exec qbittorrent cat /etc/resolv.conf如何連線到 Docker host 上執行的服務?
使用 host.docker.internal。它需要設定 2 個項目,而且都要設定在 gluetun 服務上,因為有 2 個不同問題需要修正。
先設定名稱。位於 gluetun 後方的容器會讀取 gluetun 的 /etc/hosts;如果服務設定了 network_mode,Docker 會拒絕在該服務上加入 extra_hosts 項目,並顯示 conflicting options: custom host-to-IP mapping and the network mode。因此,該項目應設定在 gluetun 上,命名空間中的每個容器都能看見它。
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 服務確實在該位址上監聽。只繫結至 127.0.0.1 的 PostgreSQL server,無法從具有自身 network namespace 的任何容器連線;這包括位於 gluetun 後方的所有容器,無論是否使用 tunnel,因為命名空間內的 127.0.0.1 是該命名空間自己的 loopback。只有使用 host networking 的容器會共用 host 的 loopback,而 gluetun 後方的容器已將其 network_mode 用於 gluetun。請改將 PostgreSQL 綁定至 172.17.0.1:如此可接受來自容器的連線,同時不會暴露在 public interface 上。請在 host 上使用 ss -lntp | grep 5432 驗證。
FIREWALL_OUTBOUND_SUBNETS 實際變更的內容
gluetun 文件將其描述為以逗號分隔的子網路清單,指定 gluetun 及共用其網路堆疊的容器可以存取的網路,並指出這項設定會變更防火牆與路由。這兩部分都很重要。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 中的位址。這是保留給電信級 NAT 的位址範圍。在個人 tailnet 上,這些功能都不收費。不過,免費方案上限為 6 位使用者,其他人需要存取相同 UI 時,是否仍可免費使用就取決於此限制。所有方案的使用者裝置數量都不限。伺服器與其他已標記裝置則分開計算:Tailscale 的定價頁面列出 50 個已標記資源,超出的部分每個每月 $1。由於計費方式主要是依使用者而非機器計算,付費 tailnet 的實際費用主要取決於你邀請多少人,而不是向他們公開多少個容器。這兩個方向需要不同的處理方式。
入站連線較簡單。在 gluetun 上發布 8080:8080,會將該連接埠繫結到主機的所有位址。主機的 tailscale0 介面也是其中之一,因此對等端開啟 http://<machine-name>:8080 後即可連到容器。Gluetun 不參與這條路徑,因為 Docker 的 NAT 規則位於主機上,在該 network namespace 之外。
若要讓 UI 僅能透過 tailnet 存取,請將發布的連接埠繫結到主機的 Tailscale 位址,而不是所有位址。
ports:
- "100.101.102.103:8080:8080/tcp"在主機上使用 tailscale ip -4 找出該位址。在此情況下,繫結比防火牆規則更強,因為該連接埠根本不會在公開介面上開放。如果你希望透過 HTTPS 名稱而不是主機與連接埠存取 UI,tailscale serve 可以代理該已發布的連接埠。不過,serve 與 funnel 的差異在於最終哪些人能夠存取,而且只有其中一種方式會讓 UI 留在 tailnet 上。這也避開了Docker 直接繞過 ufw 發布連接埠的問題。Compose 的 Tailscale sidecar 模式可以更進一步,完全移除主機連接埠;但其運作方式是將應用程式放入 sidecar 的 network namespace,而位於 gluetun 後方的容器已經將唯一的 network_mode 用於 gluetun。
出站連線則是 FIREWALL_OUTBOUND_SUBNETS 重新發揮作用的地方。如果容器必須呼叫某個對等端,請加入該對等端的位址,並優先為每個對等端設定一個 /32,而不是涵蓋整個 /10。如果要呼叫的機器位於私人網路,而該網路是透過向 tailnet 宣告該子網路的 VPS連線,請列出已宣告的範圍,而不是路由器本身的 100.x 位址,並確認主機本身已接受這些路由。MagicDNS 名稱預設無法在容器內解析,因為容器使用 gluetun 的 DNS 伺服器,而不是主機的解析器。因此,請使用數值形式的 100.x 位址,或在 gluetun 服務中加入 extra_hosts 行來固定名稱。Gluetun 的 DNS_UPSTREAM_PLAIN_ADDRESSES 選項可以改為將查詢轉送到其他解析器,但所有查詢都會以明文傳送。Gluetun 文件不建議這樣做,因為傳送至私人位址的查詢會從 VPN 通道外洩。同樣的情況也適用於使用 Headscale 執行自己的 Tailscale 控制伺服器。
完整的常見結構 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
extra_hosts:
- "host.docker.internal:host-gateway"
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"
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 上回應,不會在其他介面回應。extra_hosts 行也位於 gluetun 上,因為 Prowlarr 解析 host.docker.internal 時會讀取 gluetun 的 /etc/hosts,而 Docker 會拒絕將這一行直接設定在 Prowlarr 上。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 的環境檔與 secret 所介紹的做法。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 回報相同的公開 IP,但預設必須先設定驗證。gluetun 文件指出,所有 control server 路由現在預設為 private,因此直接請求該路徑會失敗。使用 bind mount 至 /gluetun/auth/config.toml 的 auth file 設定驗證,可採用 basic 或 apikey 方法,然後重新啟動 gluetun。上方的 compose file 已將 ./gluetun 掛載至 /gluetun,因此主機上的檔案應放在 ./gluetun/auth/config.toml。HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE 變數是較快速的做法。將其設為 {"auth":"none"} 會完全關閉驗證,文件明確標示不建議這麼做。若版本早於 v3.39.1,server 不會以這種方式限制存取;文件也警告,任何能連線至該 server 的來源都應視為可信。
錯誤的子網路設定造成的資訊外洩
FIREWALL_OUTBOUND_SUBNETS 是刻意在防火牆上開出的通道,因此通道越大,風險也越高。以下是讓通道過大的 4 種方式:
0.0.0.0/0會將所有流量送出 VPN tunnel。前面的 2 次 IP 檢查會在首次執行時偵測到這個問題,因為它們會回傳相同的位址。- 範圍大於目標。開放
10.0.0.0/8以連線至位於10.0.1.7的單一機器,也會開放 torrent peer 可能在該範圍中公布的所有位址。請改為寫入10.0.1.7/32。 - 範圍與 tunnel 自身的位址重疊。gluetun 文件警告,這會使 gluetun 改為透過 bridge 送出 VPN 流量,導致埠轉送失效。開放任何私有範圍前,請先檢查
WIREGUARD_ADDRESSES值。 - Tailscale 使用
100.64.0.0/10。這大約會開放 4 million 個位址,只為了連線至一個 peer。請將需要連線的 peer 列為/32項目。
請記住,此設定涵蓋整個 namespace。開放某個子網路,讓 indexer 可以連線至主機服務時,也會讓共用該 namespace 的 torrent client 使用相同的子網路。每次變更此變數後,都要重新執行 public IP 檢查,因為這是唯一能確認變更是否達到預期的測試。
重新啟動 gluetun 時會發生什麼問題
Gluetun 負責管理該 network namespace,因此 gluetun 的生命週期也就是該 namespace 的生命週期。當 gluetun 停止時,啟動相依容器會立即失敗:
Error response from daemon: cannot join network namespace of a non running container: container <id> is exited直接重新啟動 gluetun 會造成較不明顯的故障。相依容器仍持續執行,但它們所連接的 namespace 已在底層重建,因此 docker ps 會回報所有項目正常,實際上卻沒有任何服務回應。每次變更 gluetun 服務後,都應重建整個群組,不要只重新啟動其中一個元件。
docker compose up -d --force-recreate映像檔更新也適用相同原則。拉取新的 gluetun 映像檔並只重建該服務,會讓其他服務仍指向已不存在的 namespace。
FAQ
為什麼 Docker 會顯示「port publishing 和 container type network mode」?
因為同時設定 network_mode: service:gluetun 的服務中仍保留 ports: 區塊。發布連接埠會加入 NAT 規則,將主機連接埠轉送至容器自己的網路命名空間;但採用此模式的容器沒有自己的網路命名空間。請從該服務刪除 ports: 區塊,並將完全相同的對映加入 gluetun 服務。連接埠號碼維持不變,因為應用程式仍在共用的命名空間中監聽該連接埠。
其他容器如何連線至位於 gluetun 後方的服務?
同一命名空間中的容器可透過 127.0.0.1 互相連線。位於命名空間外的容器則使用 gluetun 服務名稱,因此 http://gluetun:8080 可運作,但 http://qbittorrent:8080 不行。應用程式容器在任何 Docker 網路上都沒有自己的位址,因此內建 DNS 伺服器無法解析其名稱。只要兩個容器共用 Compose 網路,就不需要為此發布連接埠。
FIREWALL_OUTBOUND_SUBNETS 應填入哪些內容?
只填入 gluetun 後方的容器必須主動建立連線的位址,並盡可能縮小範圍。單一機器應使用 /32。常見的兩項設定是 Docker 主機的 172.17.0.1/32,以及每個要連線之 Tailscale 對等節點各一個 /32。切勿加入 0.0.0.0/0,也不要加入與 VPN 自有通道位址重疊的範圍。對發布連接埠的入站連線不需要在此處加入項目。
為什麼容器無法解析我的 Tailscale MagicDNS 名稱?
MagicDNS 會將主機的 resolver 指向 Tailscale 的 DNS 伺服器,但容器不會使用主機的 resolver。容器使用與 gluetun 共用的 /etc/resolv.conf,而該設定會指向 gluetun 自己的 DNS 伺服器。請使用 docker exec <container> cat /etc/resolv.conf 確認。您可以使用對等節點的數值 100.x 位址,或在 gluetun 服務上以 extra_hosts 項目固定該名稱。Gluetun 可透過 DNS_UPSTREAM_PLAIN_ADDRESSES 將查詢轉送至其他 resolver,但這會以明文傳送查詢,且其文件不建議這樣做。
如何確認流量仍經由 VPN 傳送?
先在該命名空間內執行一次請求,再從主機執行相同請求,然後比較回應。docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org 應回傳 VPN 供應商的出口位址,而 VPS 上的 curl -s https://api.ipify.org 應回傳 VPS 位址。若兩個回應相同,表示通道沒有承載容器的流量。每次變更 FIREWALL_OUTBOUND_SUBNETS 後,都要再次執行此檢查。