Gluetun経由のコンテナからホストや他のコンテナへ接続する方法
Gluetunのネットワーク名前空間では独自のポート公開ができません。ポートはGluetun側に移し、VPN外へ出す必要があるサブネットだけを許可する方法を説明します。
コンテナが gluetun のネットワークに参加するとどうなるか
network_mode: service:gluetunを指定したコンテナには、独自のネットワークインターフェースがありません。gluetun のネットワーク名前空間に参加するため、ポート公開とファイアウォールルールはそのコンテナの設定ではなく、gluetun サービスの設定になります。以下の説明は、すべてこの事実に基づきます。
ネットワーク名前空間は、カーネルが持つネットワークスタックの専用コピーです。独自のインターフェース、ルーティングテーブル、ファイアウォールルール、待ち受けソケットを備えます。Docker は通常、各コンテナに 1 つずつネットワーク名前空間を割り当てます。network_mode: service:gluetunと記述すると、Docker はこの処理を省略し、新しいコンテナを gluetun がすでに所有している名前空間内に配置します。コンテナは独自のファイルシステムと独自の /etc/hosts ファイルを保持します。後者が、後で重要になります。
これを直接確認できます。
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: ブロックを記述しても意味がありません。また、そこに networks: ブロックを記述すると処理が停止します。Compose は、そのサービスが相互排他的な network_mode と networks を宣言していると報告し、ファイル自体の読み込みを拒否します。
後になって問題になる影響もあります。名前空間内のすべてのコンテナは 1 つのポート空間を共有するため、どちらもデフォルトで 8080 を使用する 2 つのアプリケーションは競合します。後から起動した方は、アドレスがすでに使用されているというエラーで失敗します。LinuxServer qBittorrent イメージの WEBUI_PORT 変数などを、いずれかのアプリケーション自身の設定で変更してください。そのうえで、新しい番号を gluetun で公開します。
gluetun 配下のコンテナはどのように相互接続しますか?
namespace 内では、コンテナはすでに loopback インターフェースを共有しています。gluetun 配下のコンテナは、Docker ネットワークを経由せず、127.0.0.1:<port> で兄弟コンテナに接続します。
namespace の外部から見ると、このコンテナには名前がありません。Docker の組み込み DNS は、ユーザー定義ネットワーク上のサービス名をそのサービスのアドレスに解決します。しかし、このコンテナにはどのネットワーク上にもアドレスがありません。そのため、Sonarr などの通常のコンテナは、http://qbittorrent:8080 で torrent クライアントに接続できません。gluetun の namespace 内でソケットが gluetun のアドレスを使って待ち受けているため、http://gluetun:8080 で接続します。Docker Compose のネットワークとサービス名の仕組みを理解していて、通常の名前解決が適用されると考える人は、ここで戸惑います。両方のコンテナが同じ Compose ネットワーク上にあるため、ホストに何も公開しなくても接続できます。
ほかの調査を始める前に、DNS を確認してください。Gluetun は独自の resolver を実行し、自身のコンテナ内で /etc/resolv.conf を書き換えます。しかし、/etc/resolv.conf はコンテナごとのファイルです。そのため、gluetun が書き込んだファイルは、アプリケーションが読み取るファイルと同じではありません。
docker exec qbittorrent cat /etc/resolv.confDocker ホスト上で実行しているサービスに接続するにはどうすればよいですか?
host.docker.internalを使用します。異なる2つの問題を解決するため、異なる2か所で2つの設定が必要です。
最初に名前を設定します。/etc/hostsはコンテナ単位の設定なので、extra_hostsの項目はgluetunではなくアプリケーションコンテナに記述します。
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gatewayは、Dockerがホスト自体の内部アドレスに置き換える特殊な値です。通常のLinux Dockerインストールでは、これはdocker0ブリッジのアドレスであり、一般的には172.17.0.1です。VPS上でip -4 addr show docker0を実行して、自分の環境の値を確認してください。Docker Desktopではこの名前が自動的に解決されます。そのため、ノートPC向けに書かれたガイドではextra_hostsの行を省略しており、同じファイルをサーバーで使うと失敗します。
次にルートを設定します。名前を追加するだけでは、コンテナが使用するアドレスを指定するだけです。パケットは引き続きgluetunのデフォルトルートを通ります。このルートはトンネルなので、gluetunのファイアウォールによって破棄されます。症状としては、接続拒否ではなく、接続がしばらく停止してタイムアウトします。拒否の場合は、パケットが到達し、何らかの応答が返されています。タイムアウトの場合は、パケットが到達していません。
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32続いて、ホストのサービスが実際にそのアドレスで待ち受けていることを確認します。127.0.0.1だけにバインドされたPostgreSQLサーバーには、コンテナから接続できません。トンネルを使用しているかどうかは関係ありません。名前空間内の127.0.0.1は、その名前空間自身のループバックだからです。代わりに172.17.0.1へバインドしてください。これにより、パブリックインターフェイスでは公開せず、コンテナからの接続を受け付けられます。ホスト上でss -lntp | grep 5432を実行して確認します。
FIREWALL_OUTBOUND_SUBNETS が実際に変更する内容
gluetun のドキュメントでは、gluetun とそのネットワークスタックを共有するコンテナがアクセスを許可される、カンマ区切りのサブネットとして説明されています。また、ファイアウォールとルーティングの変更を伴うと記載されています。どちらの変更も重要です。gluetun は、指定された各サブネットへのルートを Docker ブリッジのゲートウェイ経由で追加します。そのため、これらのアドレス宛てのパケットはトンネルではなく eth0 から送信されます。同時に、これらのサブネットへの通信をファイアウォールで許可します。gluetun は通常、VPN サーバー宛てではない送信トラフィックを破棄するためです。
値は、カンマの後にスペースを入れずに記述します。
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32見落としやすい点が 2 つあります。これはネットワーク名前空間単位の設定です。そのため、意図したコンテナだけでなく、gluetun の背後にあるすべてのコンテナに適用されます。また、対象は送信のみです。コンテナが開始する接続を制御します。公開ポートに到着する接続は別の経路を通るため、ここに指定する必要はありません。
Tailscale ピアから Web UI にアクセスする
Tailscale はすべてのマシンに 100.64.0.0/10 のアドレスを割り当てます。これはキャリアグレード NAT 用に予約された範囲です。2 方向の通信では、それぞれ異なる設定が必要です。
インバウンド通信は単純です。gluetun で 8080:8080 を公開すると、そのポートはホストのすべてのアドレスで待ち受けます。ホストの tailscale0 インターフェースもその 1 つなので、ピアは http://<machine-name>:8080 を開いてコンテナにアクセスできます。この経路では gluetun は関与しません。Docker の NAT ルールは namespace の外側にあるホスト上に配置されるためです。
UI へのアクセスを tailnet 経由だけに制限するには、公開ポートをすべてのアドレスではなく、ホストの Tailscale アドレスにバインドします。
ports:
- "100.101.102.103:8080:8080/tcp"ホスト上で tailscale ip -4 を実行すると、そのアドレスを確認できます。ここでは、ファイアウォールルールよりバインドのほうが強力な制御になります。公開インターフェースではポートがまったく開かれないためです。また、Docker が ufw を迂回して直接ポートを公開する問題も回避できます。
アウトバウンド通信では FIREWALL_OUTBOUND_SUBNETS が関係します。コンテナからピアへ接続する必要がある場合は、そのピアのアドレスを追加し、tailnet 全体に対する /10 よりも、ピアごとの /32 を優先します。コンテナはホストのリゾルバーを使用しないため、MagicDNS 名はコンテナ内で解決されません。そのため、数値形式の 100.x アドレスを使用するか、extra_hosts 行で固定します。Headscale で独自の Tailscale コントロールサーバーを運用する場合も同様です。
一般的な構成の完全な 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
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製品名ではなく、構成パターンを確認してください。2 つの UI はどちらも gluetun 上で公開し、ホストの tailnet アドレスにバインドしているため、Tailscale 上でのみ応答します。extra_hosts 行があるのは Prowlarr だけです。host.docker.internal を解決するコンテナが Prowlarr だからです。FIREWALL_OUTBOUND_SUBNETS には 2 つの単一アドレスを指定しています。1 つは Prowlarr がデータベースへ接続するための Docker ブリッジ上のホストアドレスで、もう 1 つは tailnet のピアです。
PostgreSQL サーバーは意図的にこのファイルから除外しています。VPS 上で通常のシステムサービスとして実行し、172.17.0.1:5432 で待ち受けます。これは Docker Compose 上の arr スタック と同じ層構成で、データベースだけを Docker の外部に移したものです。
WireGuard の秘密鍵は compose ファイルに保存しないでください。${WIREGUARD_PRIVATE_KEY} は隣接する .env ファイルから読み込みます。このパターンについては、Docker Compose の env ファイルと Secretで説明しています。condition: service_healthy 句では、gluetun イメージにあらかじめ含まれている healthcheck を使用します。そのため、トンネルが起動したと報告するまで何も起動しません。一般的な形式については Compose の healthcheckを参照してください。
すべてのアドレスで公開する場合(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.org1 回目には VPN プロバイダーの出口アドレスが表示されます。2 回目には VPS のアドレスが表示されます。両者が一致する場合、コンテナのトラフィックはトンネルを経由していません。その場合、これを修正するまで、このガイドの他の対策を行っても意味がありません。
ルーティングテーブルには、トンネルを経由するトラフィックと、経由しないトラフィックが表示されます。
docker run --rm --network=container:gluetun alpine:3.22 ip route showデフォルトルートはトンネルインターフェース tun0 を指している必要があります。その下には、FIREWALL_OUTBOUND_SUBNETS の各エントリに対応するルートが 1 つずつ表示され、Docker ブリッジのゲートウェイを指しているはずです。eth0 経由で外部へ出るその他のルートは、VPN を迂回するトラフィックです。
Gluetun の control server でも、ポート 8000 の /v1/publicip/ip で同じパブリック IP を確認できます。最近のバージョンでは、control server のルートに対する認証設定が必要です。依存する前に設定してください。
1 つの誤ったサブネットで発生する漏えい
FIREWALL_OUTBOUND_SUBNETS はファイアウォールに意図的に開ける穴です。したがって、穴の大きさがそのままリスクの大きさになります。大きくしすぎる方法は 4 つあります。
0.0.0.0/0は、トンネル外へすべての通信を送信します。上記の 2 つの IP チェックでは、初回実行時にこれを検出できます。どちらも同じアドレスを返すためです。- 宛先より広い範囲を指定することです。
10.0.1.7の 1 台に到達するために10.0.0.0/8を開くと、その範囲内で torrent のピアが通知する可能性のあるすべてのアドレスも開かれます。10.0.1.7/32と記述してください。 - トンネル自身のアドレスと重なる範囲を指定することです。gluetun のドキュメントでは、この設定により gluetun が VPN トラフィックをブリッジ経由で送信するようになり、ポートフォワーディングが機能しなくなると警告しています。プライベート範囲を開く前に、
WIREGUARD_ADDRESSESの値を確認してください。 - Tailscale で
100.64.0.0/10を指定することです。これは、1 つのピアに到達するために約 4000000 個のアドレスを開くことになります。必要なピアを/32エントリとして列挙してください。
この設定は、名前空間全体に適用されます。indexer がホストサービスに到達できるようにサブネットを開くと、その名前空間を共有する torrent クライアントにも同じサブネットが開かれます。この変数を変更するたびに、パブリック IP チェックを再実行してください。変更が意図どおりに機能したかを示すテストは、これだけです。
gluetun を再起動すると何が壊れるか
Gluetun がネットワーク名前空間を所有するため、gluetun のライフサイクルが名前空間のライフサイクルになります。gluetun が停止している状態で依存コンテナを起動すると、すぐに失敗します。
Error response from daemon: cannot join network of a non running containergluetun をその場で再起動する場合は、問題が表面化しにくくなります。依存コンテナは動作し続けますが、接続先の名前空間がその下で再構築されるため、docker ps はすべて正常と報告する一方で、どのサービスにも応答がありません。gluetun サービスを変更した後は、1 つの部分だけを再起動せず、グループ全体を再作成してください。
docker compose up -d --force-recreateイメージを更新する場合も同じです。新しい gluetun イメージを pull してそのサービスだけを再作成すると、他のコンテナは存在しなくなった名前空間を参照し続けます。
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 はホストのリゾルバーを Tailscale の DNS サーバーに向けて動作しますが、コンテナはホストのリゾルバーを使用しません。コンテナ自身の /etc/resolv.conf に従います。gluetun の背後では、gluetun の DNS 設定が使用されます。docker exec <container> cat /etc/resolv.conf で確認してください。peer の数値 100.x アドレスを使用するか、そのコンテナに extra_hosts エントリを設定して名前を固定してください。
トラフィックが引き続き 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 のアドレスを返すはずです。結果が一致する場合、トンネルはコンテナのトラフィックを運んでいません。FIREWALL_OUTBOUND_SUBNETS を変更するたびに、この確認を再実行してください。