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(network address translation)ルールが追加されます。しかし、このコンテナには固有のネットワーク名前空間がありません。マッピングを 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 network を使わずに 127.0.0.1:<port> で兄弟コンテナへ接続できます。
namespace の外部から見ると、そのコンテナには名前がありません。Docker の組み込み DNS は、ユーザー定義 network 上のサービス名を、そのサービスのアドレスに解決します。しかし、このコンテナにはどの network 上にもアドレスがありません。そのため、Sonarr などの通常のコンテナは、http://qbittorrent:8080 で torrent クライアントに接続できません。http://gluetun:8080 で接続します。ソケットは gluetun の namespace 内で、gluetun のアドレス上で待ち受けているためです。Docker Compose の network とサービス名の仕組みを理解していて、通常の名前解決が適用されると考える人は、この動作に驚くことがあります。両方のコンテナが同じ Compose network 上にあるため、host へ何も公開せずに接続できます。
他の調査を始める前に、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 から送信されます。同時に、これらのサブネットへの通信をファイアウォールで許可します。これは、VPN サーバー宛てではない送信トラフィックを gluetun が通常は破棄するためです。
値は、カンマの後に空白を入れずに記述してください。
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32見落としやすい点が2つあります。これは namespace 単位の設定であるため、意図したコンテナだけでなく、gluetun の背後にあるすべてのコンテナに適用されます。また、対象は outbound のみです。コンテナが開始する接続を制御します。公開ポートに到着する接続は別の経路を通るため、ここに指定する必要はありません。
Reaching the web UI from a Tailscale peer
Tailscale はすべてのマシンに、キャリアグレード NAT 用に予約された 100.64.0.0/10 の範囲内のアドレスを割り当てます。個人用 tailnet であれば、これは一切費用がかかりません。ただし、他のユーザーが同じ UI にアクセスする必要が生じた場合も無料のままにできるかどうかは、無料プランのユーザー数とデバイス数の上限によって決まります。料金はマシン数ではなくユーザー数に基づくため、有料 tailnet の実際の料金は、公開するコンテナ数ではなく招待するユーザー数で決まります。2 つの方向では、必要な作業が異なります。
Inbound is the simple one. Publishing 8080:8080 on gluetun binds that port on all of the host's addresses, and the host's tailscale0 interface is one of them, so a peer opens http://<machine-name>:8080 and reaches the container. Gluetun plays no part in that path, because Docker's NAT rule sits on the host, outside the namespace.
To make the UI reachable only over the tailnet, bind the published port to the host's Tailscale address rather than to every address.
ports:
- "100.101.102.103:8080:8080/tcp"Find that address with tailscale ip -4 on the host. Binding is a stronger control than a firewall rule here, because the port is never opened on the public interface at all. If you would rather reach the UI at an HTTPS name than at a host and port, tailscale serve can front that published port, though serve and funnel differ in who ends up able to reach it and only one of the two keeps the UI on the tailnet. It also sidesteps the problem in Docker publishing ports straight past ufw.
Outbound is where FIREWALL_OUTBOUND_SUBNETS returns. If a container has to call a peer, add that peer's address, and prefer a /32 per peer over the whole /10. If the machine you are calling sits on a private network reached through a VPS advertising that subnet to your tailnet, list the advertised range instead of the router's own 100.x address, and confirm the host itself has accepted those routes. MagicDNS names will not resolve inside the container, because the container does not use the host's resolver, so use the numeric 100.x address or pin it with an extra_hosts line. The same applies when you run your own Tailscale control server with Headscale.
一般的な構成の完全な 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 bridge アドレスで、もう 1 つは tailnet の peer です。
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"トンネルが引き続きトラフィックを転送していることを確認する
同じリクエストを、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 ip route showデフォルトルートはトンネルインターフェイス、tun0 を指している必要があります。その下には、FIREWALL_OUTBOUND_SUBNETS の各エントリに対応するルートが 1 つずつ表示され、Docker bridge のゲートウェイを指しているはずです。eth0 を経由して外部へ送信されるその他のルートは、VPN を迂回するトラフィックです。
Gluetun の control server は、port 8000 の /v1/publicip/ip で同じ public IP を返します。最近のバージョンでは、control server の route に対する認証設定が必要です。これに依存する前に設定してください。
1 つの誤ったサブネットが作る情報漏えい
FIREWALL_OUTBOUND_SUBNETS は、意図的にファイアウォールへ開ける穴です。そのため、穴の大きさがリスクの大きさになります。大きくしすぎる方法は 4 つあります。
0.0.0.0/0は、トンネル外へすべての通信を送ります。上記の 2 つの IP チェックでは、最初の実行時にこれを検出できます。どちらも同じアドレスを返すためです。- 対象より広い範囲を指定することです。
10.0.1.7の 1 台に到達するために10.0.0.0/8を開くと、その範囲内で torrent peer が広告するすべてのアドレスも開くことになります。10.0.1.7/32と記述してください。 - トンネル自体のアドレスと重なる範囲を指定することです。gluetun のドキュメントでは、この場合、gluetun が VPN トラフィックをブリッジ経由で送信するようになり、ポートフォワーディングが機能しなくなると警告しています。プライベート範囲を開く前に、
WIREGUARD_ADDRESSESの値を確認してください。 - Tailscale に
100.64.0.0/10を指定することです。これは、1 つの peer に到達するために約 400 万個のアドレスを開くことになります。必要な peer を/32エントリとして列挙してください。
この設定は namespace 全体に適用されます。indexer がホストサービスに到達できるようにサブネットを開くと、その namespace を共有する torrent client に対しても同じサブネットが開かれます。この変数を変更するたびに、パブリック IP チェックを再実行してください。変更が意図したとおりに機能したかを示せるテストは、これだけです。
gluetun を再起動すると何が壊れるか
Gluetun が network namespace を所有するため、gluetun のライフサイクルが network namespace のライフサイクルになります。gluetun が停止している状態で依存コンテナを起動すると、直ちに失敗します。
Error response from daemon: cannot join network of a non running containergluetun をその場で再起動する場合は、より気付きにくい障害になります。依存コンテナは実行を続けますが、接続先の network namespace はその下で再構築されます。そのため、docker ps はすべて正常と報告する一方で、どのサービスにも応答がありません。gluetun サービスを変更した後は、一部だけを再起動せず、グループ全体を再作成してください。
docker compose up -d --force-recreateイメージを更新する場合も同様です。新しい gluetun イメージを pull してそのサービスだけを再作成すると、他のコンテナは存在しなくなった network namespace を参照し続けます。
FAQ
Docker が「ポート公開とコンテナ型のネットワークモード」と表示するのはなぜですか?
ports: ブロックが、network_mode: service:gluetun も設定しているサービスに残っているためです。ポートを公開すると、ホストのポートからコンテナ独自のネットワーク名前空間へ転送する NAT ルールが追加されます。このモードのコンテナには独自のネットワーク名前空間がないため、利用できません。そのサービスから ports: ブロックを削除し、同じマッピングを gluetun サービスに追加してください。アプリケーションは共有名前空間内で引き続き同じポートを待ち受けるため、ポート番号は変わりません。
gluetun の背後にあるサービスへ、他のコンテナから接続するにはどうすればよいですか?
同じ名前空間内のコンテナは 127.0.0.1 で相互に接続します。外部のコンテナは gluetun サービス名を使用するため、http://qbittorrent:8080 ではなく http://gluetun:8080 が機能します。アプリケーションコンテナはどの Docker ネットワークにもアドレスを持たないため、組み込み DNS サーバーはその名前を解決できません。両方のコンテナが Compose ネットワークを共有していれば、この接続のためにポートを公開する必要はありません。
FIREWALL_OUTBOUND_SUBNETS には何を設定すればよいですか?
gluetun の背後にあるコンテナが接続を開始する必要があるアドレスだけを、可能な限り狭い範囲で記述します。1 台のマシンは /32 です。一般的な設定は、Docker ホストの 172.17.0.1/32 と、接続する Tailscale ピアごとの /32 です。0.0.0.0/0 は追加しないでください。また、VPN 自身のトンネルアドレスと重複する範囲も追加しないでください。公開ポートへの受信接続には、ここでの設定は必要ありません。
コンテナから Tailscale の MagicDNS 名を解決できないのはなぜですか?
MagicDNS は、ホストのリゾルバーを Tailscale の DNS サーバーに向けることで機能します。コンテナはホストのリゾルバーを使用しません。コンテナ自身の /etc/resolv.conf に従って DNS を使用します。gluetun の背後では、これは gluetun の DNS 設定です。docker exec <container> cat /etc/resolv.conf で確認してください。ピアの数値アドレス 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 のアドレスを返すはずです。2 つの応答が一致する場合、トンネルはコンテナのトラフィックを運んでいません。FIREWALL_OUTBOUND_SUBNETS を変更するたびに、この確認を再度実行してください。