Gluetun経由のコンテナからホストや他のコンテナに接続する方法
Gluetunのネットワークを使うコンテナは独自のインターフェースを持ちません。ポートはGluetunで公開し、VPN外へ接続する必要最小限のサブネットだけを許可します。
コンテナが gluetun のネットワークに参加するとどうなるか
network_mode: service:gluetun を設定したコンテナには、独自のネットワークインターフェースがありません。gluetun のネットワーク名前空間に参加するため、ポート公開とファイアウォールルールはそのコンテナの設定ではなく、gluetun サービスの設定になります。以下の内容は、すべてこの事実に基づいています。
ネットワーク名前空間は、カーネルが持つネットワークスタックの専用コピーです。独自のインターフェース、ルーティングテーブル、ファイアウォールルール、待ち受けソケットを持ちます。Docker は通常、各コンテナにネットワーク名前空間を1つ割り当てます。network_mode: service:gluetun と記述すると、Docker はこの処理を省略し、新しいコンテナを gluetun がすでに所有している名前空間に配置します。コンテナは独自のファイルシステムを維持しますが、独自の /etc/hosts や /etc/resolv.conf は持ちません。Docker は gluetun が使用している同じファイルをコンテナに渡します。この点が後で重要になります。
これは直接確認できます。
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentこのコマンドは、通常のコンテナなら bridge と表示される箇所に、container: と gluetun コンテナ ID を表示します。このガイドでは、gluetun を使用して Docker トラフィックを VPN 経由でルーティングするの続きとして説明します。トンネルは動作していますが、現在は何もコンテナと通信できません。
gluetun でポートを公開し、アプリケーションでは公開しない
network_mode を設定する ports: ブロックをサービスに残すと、Docker はコンテナの作成を拒否します。
Error response from daemon: conflicting options: port publishing and the container type network mode理由は明確です。ポートを公開すると、ホストのポートからコンテナ独自のネットワーク名前空間へ転送する NAT(ネットワークアドレス変換)ルールが追加されます。しかし、このコンテナには独自のネットワーク名前空間がありません。マッピングを gluetun サービスへ移してください。アプリケーションは共有名前空間内で引き続き同じポートを待ち受けるため、ポート番号は変わりません。
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 を宣言していると報告し、ファイル全体の読み込みを拒否します。
この構成では、後から問題が発生することもあります。名前空間内のすべてのコンテナが単一のポート空間を共有するため、どちらもデフォルトで 8080 を使用するアプリケーションがあると競合します。後から起動したアプリケーションは、アドレスがすでに使用されているというエラーで失敗します。一方のアプリケーションの設定でポートを変更してください。たとえば、LinuxServer qBittorrent イメージの WEBUI_PORT 変数を変更し、その新しい番号を gluetun で公開します。
コンテナは gluetun の背後でどのように相互接続するか
namespace 内では、コンテナはすでに同じ loopback インターフェースを共有しています。gluetun の背後にあるコンテナは、Docker network を介さずに 127.0.0.1:<port> の sibling コンテナへ接続できます。
namespace の外部から見ると、そのコンテナには名前がありません。Docker の組み込み DNS は、user defined network 上の service name をそのサービスのアドレスに解決します。しかし、このコンテナにはどの network 上にもアドレスがありません。そのため、Sonarr のような通常のコンテナは http://qbittorrent:8080 で torrent client に接続できません。http://gluetun:8080 で接続します。socket は gluetun の namespace 内で、gluetun のアドレス上で listening しているためです。Docker Compose の network と service name の仕組みを知っていて、通常の名前解決が適用されると考える人は、ここで戸惑うことがあります。両方のコンテナが同じ Compose network 上にあるため、host に何も publish せずに接続できます。
ほかの調査を始める前に、DNS を確認してください。Gluetun は namespace 内で独自の DNS server を実行します。Docker は gluetun のものと同じ /etc/resolv.conf ファイルを、gluetun の背後にあるすべてのコンテナに渡します。そのため、アプリケーションの名前解決は host の resolver ではなく、gluetun の resolver を経由します。デフォルトでは、この resolver は private address を指す応答も拒否します。そのため、host 上では解決できる名前が、コンテナ内では失敗することがあります。gluetun のドキュメントでは、このような hostname を DNS_REBINDING_PROTECTION_EXEMPT_HOSTNAMES に列挙する必要があると説明しています。
docker exec qbittorrent cat /etc/resolv.confDocker ホスト上で実行されているサービスに接続するにはどうすればよいですか?
host.docker.internal を使用します。異なる 2 つの問題を解決するため、gluetun サービスに 2 つの設定が必要です。
最初に名前を設定します。gluetun 配下のコンテナは gluetun の /etc/hosts を読み取ります。また、network_mode を conflicting options: custom host-to-IP mapping and the network mode と設定したサービスでは、Docker は extra_hosts エントリを拒否します。そのため、このエントリは gluetun に設定します。同じネットワーク名前空間に属するすべてのコンテナから、この設定が見えるようになります。
gluetun:
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway は、Docker がホスト自体の内部アドレスに置き換える特殊な値です。通常の Linux Docker 環境では、これは docker0 bridge のアドレスであり、一般的には 172.17.0.1 です。VPS 上で ip -4 addr show docker0 を実行して、自分の環境の値を確認してください。Docker Desktop ではこの名前が自動的に解決されます。そのため、ラップトップ向けに書かれたガイドでは extra_hosts の行を省略しており、同じファイルをサーバーで使うと失敗します。
次に経路を設定します。名前を追加するだけでは、コンテナが使用するアドレスを指定したことにしかなりません。パケットは引き続き gluetun のデフォルト経路を通ります。この経路はトンネルであり、gluetun のファイアウォールによってパケットが破棄されます。症状としては、接続が拒否されるのではなく、しばらく待ってからタイムアウトします。拒否の場合は、パケットが到達し、何らかのサービスが拒否を応答しています。タイムアウトの場合は、パケットが到達していません。
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32続いて、ホスト上のサービスがそのアドレスで実際に待ち受けていることを確認します。127.0.0.1 のみにバインドされた PostgreSQL サーバーには、独自のネットワーク名前空間を持つコンテナから接続できません。これはトンネルの有無にかかわらず、gluetun 配下のすべてのコンテナに当てはまります。名前空間内の 127.0.0.1 は、その名前空間自身の loopback だからです。ホストの loopback を共有するのは host networking を使用するコンテナだけです。また、gluetun 配下のコンテナでは、すでに network_mode が gluetun に割り当てられています。代わりに PostgreSQL を 172.17.0.1 にバインドしてください。これにより、パブリックインターフェイスに公開せず、コンテナからの接続を受け入れられます。ホスト上で ss -lntp | grep 5432 を実行して確認します。
FIREWALL_OUTBOUND_SUBNETS が実際に変更する内容
gluetun のドキュメントでは、gluetun とそのネットワークスタックを共有するコンテナがアクセスを許可される、カンマ区切りのサブネットとして説明されています。また、ファイアウォールとルーティングの変更を伴うと記載されています。どちらも重要です。gluetun は指定された各サブネットについて、Docker bridge gateway 経由のルートを追加します。そのため、これらのアドレス宛てのパケットはトンネルではなく eth0 から送信されます。また、対象サブネットへのファイアウォール通信も許可します。gluetun は通常、VPN server 宛てではない outbound traffic を破棄するためです。
値は、カンマの後にスペースを入れずに記述してください。
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32見落としやすい点が 2 つあります。これは namespace 単位の設定です。そのため、想定していたコンテナだけでなく、gluetun の背後にあるすべてのコンテナに適用されます。また、outbound 専用です。コンテナが開始する接続を制御します。公開ポートに到達する接続は別の経路を通るため、ここに指定する必要はありません。
Tailscale peer から Web UI にアクセスする
Tailscale は、キャリアグレード NAT 用に予約された 100.64.0.0/10 の範囲から、各マシンにアドレスを割り当てます。個人用の tailnet では、これらの機能に料金はかかりません。ただし、同じ UI へのアクセスが他のユーザーにも必要になると、無料プランの 6 ユーザーという上限によって無料のまま利用できるかどうかが決まります。ユーザーデバイスは、すべてのプランで無制限です。サーバーなどのタグ付きデバイスは別に数えられます。Tailscale の料金ページでは、タグ付きリソース 50 個が含まれ、それを超える分は 1 個あたり月額 $1 とされています。その他の料金計算はマシン数ではなくユーザー数を基準とするため、有料の tailnet に実際にかかる費用は、公開するコンテナ数ではなく、招待するユーザー数によってほぼ決まります。この 2 つの方向では、必要な作業が異なります。
受信方向は単純です。gluetun で 8080:8080 を公開すると、そのポートはホストのすべてのアドレスにバインドされます。ホストの tailscale0 インターフェースもその1つなので、ピアが http://<machine-name>:8080 を開くとコンテナに到達します。この経路では gluetun は関与しません。Docker の NAT ルールは名前空間の外側にあるホスト上に存在するためです。
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 のネットワーク名前空間に配置して動作します。また、gluetun の背後にあるコンテナでは、1 つしかない network_mode をすでに gluetun が使用しています。
Outbound では FIREWALL_OUTBOUND_SUBNETS が返されます。コンテナからピアを呼び出す必要がある場合は、そのピアのアドレスを追加し、/10 全体よりもピアごとの /32 を使用してください。接続先のマシンが、そのサブネットを tailnet に広告する VPS 経由で到達するプライベートネットワーク上にある場合は、ルーター自身の 100.x アドレスではなく、広告された範囲を指定します。また、ホスト自体がそのルートを受け入れていることを確認してください。コンテナはデフォルトで MagicDNS 名を解決できません。コンテナはホストのリゾルバーではなく、gluetun の DNS サーバーを使用するためです。そのため、数値形式の 100.x アドレスを使用するか、gluetun サービスに extra_hosts 行を指定して名前を固定してください。Gluetun の DNS_UPSTREAM_PLAIN_ADDRESSES オプションを使用すると、クエリを別のリゾルバーに転送できます。ただし、すべてのクエリが平文で送信されます。gluetun のドキュメントでは、プライベートアドレスへのクエリが VPN トンネルの外部に漏れるため、この方法を推奨していません。Headscale を使用して独自の Tailscale control server を運用する場合も同様です。
一般的な構成に対応する完全な compose ファイル
VPN 経由で接続するダウンロードクライアント、tailnet 上でのみ応答する2つの Web UI、ホスト上で実行する PostgreSQL データベースを読み取るコンテナ1つという構成です。
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製品名ではなく、構成のパターンを確認してください。2つの UI は gluetun 上で公開し、ホストの tailnet アドレスにバインドしているため、Tailscale からのみ応答し、それ以外からは応答しません。extra_hosts の行も gluetun 上に配置しています。Prowlarr は host.docker.internal を解決するときに gluetun の /etc/hosts を読み取り、Prowlarr 自体にこの行を記述すると Docker が拒否するためです。FIREWALL_OUTBOUND_SUBNETS には2つの単一アドレスを指定しています。1つは Prowlarr がデータベース接続を開くための、ホストの Docker bridge アドレスです。もう1つは tailnet peer です。
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 を使用します。そのため、トンネルが起動したと報告するまで何も起動しません。一般的な形式については Compose の healthchecks を参照してください。
すべてのアドレスで公開し、tailnet のみに限定しない場合
0.0.0.0 のアドレスプレフィックスとポートバインドを削除します。これには VPS のパブリック IP も含まれます。自分で管理するファイアウォールの背後でのみ実施し、先に上記の ufw に関する注意を確認してください。
ports:
- "8080:8080/tcp"トンネルが引き続き通信を転送していることを確認する
同じリクエストを 2 回実行します。1 回は namespace 内から、もう 1 回はホストから実行して結果を比較します。
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 sh -c 'ip rule show; ip route show table 51820; ip route show table 199'このガイドの WireGuard 構成では、gluetun がトンネルインターフェースに wg0 という名前を付けます。tun0 は OpenVPN のデフォルト名にすぎません。gluetun は、トンネルのデフォルトルートを main routing table からも分離します。ip rule show には、WireGuard 自身がマークしていないすべての通信を table 51820 に送るルールが含まれているはずです。また、そのテーブルには wg0 上のデフォルトルートがあるはずです。table 199 には、FIREWALL_OUTBOUND_SUBNETS のエントリごとに 1 つのルートがあり、それぞれが Docker bridge のゲートウェイ eth0 を指します。このテーブルのルールは、トンネルのルールの直前に評価されます。通常の ip route show では、Docker 自身のデフォルトルートが eth0 上にあることも表示されます。これは想定された状態であり、リークではありません。gluetun の 2 つのルールは、main table より前に評価されるためです。table 199 に、一覧に含めていないルートがある場合、その通信は VPN を迂回しています。
gluetun の control server は、ポート 8000 の /v1/publicip/ip で同じパブリック IP を返せます。ただし、デフォルトでは認証を設定した後に限られます。gluetun のドキュメントによると、現在はすべての control server route がデフォルトで非公開になっているため、そのパスに通常のリクエストを送ると失敗します。basic または apikey の方法で、/gluetun/auth/config.toml に bind mount する認証ファイルを使って設定し、その後 gluetun を再起動します。上記の compose file では、すでに ./gluetun が /gluetun に mount されています。そのため、ホスト上では ./gluetun/auth/config.toml にファイルを配置します。HTTP_CONTROL_SERVER_AUTH_DEFAULT_ROLE 変数を使う方法がより簡単です。これを {"auth":"none"} に設定すると認証が完全に無効になり、ドキュメントでは強く非推奨とされています。v3.39.1 より前のリリースでは、この方法で server がロックダウンされていません。ドキュメントでは、server に到達できるすべての接続元を信頼することになるため注意するよう警告しています。
1 つの誤ったサブネットが作る漏えい
FIREWALL_OUTBOUND_SUBNETS は、意図的にファイアウォールへ開ける穴です。そのため、穴の大きさがリスクの大きさになります。大きくしすぎる方法は 4 つあります。
0.0.0.0/0は、すべての通信をトンネルの外へ送信します。上記の 2 つの IP チェックでは、初回実行時にこれを検出できます。どちらも同じアドレスを返すためです。- 宛先より広い範囲を指定することです。1 台のマシンへ
10.0.1.7で到達するために10.0.0.0/8を開くと、その範囲内で torrent peer が通知するすべてのアドレスも開かれます。10.0.1.7/32と記述してください。 - トンネル自身のアドレスと重複する範囲を指定することです。gluetun のドキュメントでは、この設定により gluetun が VPN トラフィックを bridge 経由で送信するようになり、ポートフォワーディングが機能しなくなると警告しています。プライベート範囲を開く前に、
WIREGUARD_ADDRESSESの値を確認してください。 - Tailscale に
100.64.0.0/10を指定することです。これは、1 台の peer に到達するために約 4000000 個のアドレスを開くことに相当します。必要な peer を/32エントリとして列挙してください。
この設定は namespace 全体に適用されます。indexer がホストサービスへ到達できるようにサブネットを開くと、同じ namespace を共有する torrent client に対しても同じサブネットが開かれます。この変数を変更するたびに、公開 IP チェックを再実行してください。変更が意図どおりに機能したかを示すテストは、これだけだからです。
gluetun を再起動すると何が壊れるか
gluetun が namespace を管理するため、gluetun のライフサイクルが namespace のライフサイクルになります。gluetun が停止している状態で依存コンテナを起動すると、直ちに失敗します。
Error response from daemon: cannot join network namespace of a non running container: container <id> is exitedgluetun をその場で再起動する場合は、より気付きにくい障害になります。依存コンテナは動作し続けますが、接続先の namespace がその下で再構築されます。そのため、docker ps はすべて正常と報告する一方で、どのサービスにも応答できなくなります。gluetun サービスを変更した後は、1 つの部分だけを再起動せず、グループ全体を再作成してください。
docker compose up -d --force-recreateイメージを更新する場合も同じです。新しい gluetun イメージを pull してそのサービスだけを再作成すると、他のコンテナは存在しなくなった namespace を参照し続けます。
FAQ
Docker が「ポート公開とコンテナ型のネットワークモード」と表示するのはなぜですか?
ports: ブロックが、network_mode: service:gluetun も設定しているサービスに残っているためです。ポートを公開すると、ホストのポートからコンテナ独自のネットワーク名前空間へ転送する NAT ルールが追加されます。しかし、このモードのコンテナには独自のネットワーク名前空間がありません。そのサービスから ports: ブロックを削除し、同じマッピングを gluetun サービスに追加してください。アプリケーションは共有名前空間内で引き続き同じポートを待ち受けるため、ポート番号は変わりません。
gluetun の背後にあるサービスへ、他のコンテナから接続するにはどうすればよいですか?
同じ名前空間内のコンテナ同士は 127.0.0.1 で接続します。名前空間の外部にあるコンテナは gluetun サービス名を使用するため、http://gluetun:8080 は機能しますが、http://qbittorrent:8080 は機能しません。アプリケーションコンテナはどの Docker ネットワークにもアドレスを持たないため、組み込み DNS サーバーはその名前を解決できません。両方のコンテナが Compose ネットワークを共有していれば、この接続にポート公開は必要ありません。
FIREWALL_OUTBOUND_SUBNETS には何を設定すればよいですか?
gluetun の背後にあるコンテナが接続を開始する必要のあるアドレスだけを、可能な限り狭く指定します。1 台のマシンは /32 です。一般的な指定は、Docker ホストの 172.17.0.1/32 と、接続する Tailscale peer ごとの /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 で確認してください。peer の数値形式の 100.x アドレスを使用するか、gluetun サービスに extra_hosts エントリを追加して名前を固定してください。gluetun は DNS_UPSTREAM_PLAIN_ADDRESSES により別の resolver へクエリを転送できますが、クエリが平文で送信されるため、ドキュメントでは使用しないよう案内されています。
トラフィックが引き続き VPN 経由になっていることを確認するにはどうすればよいですか?
名前空間内から 1 回リクエストを実行し、同じリクエストをホストからも実行して、結果を比較します。docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org は VPN プロバイダーの出口アドレスを返し、VPS 上の curl -s https://api.ipify.org は VPS のアドレスを返すはずです。結果が 2 つとも一致する場合、トンネルはコンテナのトラフィックを転送していません。FIREWALL_OUTBOUND_SUBNETS を変更するたびに、この確認を再度実行してください。