DockerコンテナをVPN経由でルーティングする方法
Gluetunのnetwork_mode: service:gluetunでポートが消える理由を解説します。共有network namespaceの制約と、動作するCompose設定、v3.41.3の注意点を確認できます。
Docker コンテナを VPN 経由でルーティングするとポートが消える理由
Docker コンテナを VPN 経由でルーティングするには、1 つのコンテナにトンネルを割り当て、network_mode: "service:gluetun" で他のコンテナをそのネットワーク名前空間に接続します。この接続方法が、混乱を招く部分です。接続先のコンテナは独自のネットワークを持たなくなるため、公開ポートと Docker のサービス名も失われます。ポートは VPN コンテナで公開し、他のコンテナからは VPN コンテナの名前でアプリケーションに接続します。
接続先のコンテナに ports: ブロックを残すと、Docker はそのコンテナ自体の作成を拒否します。
Error response from daemon: conflicting options: port publishing and the container type network modeここでは、WireGuard または OpenVPN で商用 VPN(virtual private network)プロバイダーに接続し、独自のファイアウォールも備えたコンテナである Gluetun を使用します。2026 年 8 月時点の最新リリースは v3.41.3 です。例では WireGuard と Mullvad を使用するため、プロバイダーのアカウントと key が必要です。自分で所有するハードウェア上でトンネルを終端したい場合は、VPS 上で独自の WireGuard サーバーを実行することで反対側のエンドポイントを構築できます。また、Docker で wg-easy を使用すると、それを Web インターフェースで管理できます。
実際の network_mode: "service:gluetun" の動作
通常、Docker コンテナごとに独自のネットワーク namespace が割り当てられます。独自のインターフェース、ルーティングテーブル、ファイアウォールルール、待ち受けソケットを持つ構成です。service: モードではこの処理を省略し、gluetun の namespace 内でコンテナを起動します。namespace が 1 つになると IP アドレスも 1 つになり、次の 6 つが変わります。
- アプリは独自のアドレスを持ちません。gluetun のアドレスを使用します。
- アプリは Docker ネットワークに接続されないため、サービス名が登録されず、名前解決もできません。他のコンテナからは
gluetunを使用する必要があります。 - namespace 内のコンテナ同士は
localhost経由で通信します。 - 1 つの namespace 内にある 2 つのコンテナは、同じポートで待ち受けできません。Gluetun のドキュメントにも明記されているとおり、回避策はありません。
- Capability は namespace ではなくコンテナに属します。トンネルインターフェースを作成するため、Gluetun は
NET_ADMINと/dev/net/tunを保持します。接続されたコンテナはこれらを継承しません。 - 1 つのサービスで
network_modeとnetworksの両方を設定した Compose ファイルは拒否されます。gluetun をネットワークに接続し、アプリをそのネットワーク経由で動作させてください。
gluetun を再起動すると、接続されているすべてのコンテナが切断されます。これはドキュメントに記載された動作です。そのため、接続が失敗しても gluetun は終了せず、コンテナ内の VPN プロセスを再起動します。gluetun を自分で再起動または再作成した場合は、接続されているコンテナも再起動してください。
動作する compose ファイル
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- SERVER_CITIES=Amsterdam
- TZ=Europe/Amsterdam
env_file:
- ./gluetun.env
volumes:
- ./gluetun:/gluetun
ports:
- 127.0.0.1:8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- ./downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped:v3 タグは、v3 系列の最新の安定版リリースです。:latest タグは master ブランチの最後のコミットを指します。これは開発中の最新状態なので、火曜日にデバッグしたくないマシンでは :v3 を固定してください。
WEBUI_PORT=8080 は公開ポートと一致させる必要があります。qBittorrent は gluetun のネットワーク名前空間内で待ち受け、公開ルールはホストからのトラフィックをその名前空間の port 8080 に転送するためです。一方の番号だけを変更すると、ポートは応答しません。127.0.0.1:8080:8080 により、Web インターフェイスはホストの loopback アドレスだけで待ち受けます。単独の 8080:8080 はすべてのインターフェイスで公開し、独自の firewall ルールも追加します。これが、Docker の公開ポートが ufw をそのまま通過する仕組みです。
起動したら、次の順序で確認します。
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30docker compose ps では gluetun が healthy、qbittorrent が running と表示されるはずです。次に、ネットワーク名前空間内から外部接続元アドレスを確認します。これが、それ以外の確認結果を左右する重要なチェックです。
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"この JSON の ip フィールドには、VPN provider のアドレスが表示されるはずです。サーバー自身のアドレスが表示される場合、アプリケーションはトンネル内に接続されていません。その場合、以下の内容は説明どおりに動作しません。
Compose ファイルに鍵を記述しない
gluetun.env に認証情報を保存し、git の管理対象から除外します。
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32どちらの値も、プロバイダーのアカウント画面で生成する WireGuard 設定ファイルから取得します。ファイルのモードを 600 に設定してください。この方法で得られる効果を正しく理解する必要があります。鍵はリポジトリに含まれませんが、docker inspect gluetun は Docker socket に到達できるすべてのユーザーに対して、引き続きすべての環境変数を表示します。より強固な方法については、Docker Compose の環境ファイルと Secret を参照してください。
トンネルの外側にあるコンテナから内側のコンテナへ接続する方法
どちらの方向の接続も機能しますが、それぞれ異なる名前を使用します。2 つのコンテナには共有 Docker ネットワークが必要です。接続先のコンテナには独自のネットワークがないため、これは gluetun のネットワークになります。デフォルトの動作については、Docker Compose のネットワーク構成で説明しています。
外側から内側へ接続する場合は、gluetun の名前と、アプリケーションが待ち受けるポートを使用します。リバースプロキシコンテナから qBittorrent の Web インターフェイスへは gluetun:8080 で接続します。この場合、ports: の設定は不要です。コンテナ間のトラフィックは Docker ネットワーク内に留まり、ホストのポートを経由しないためです。
内側から外側へ接続する場合は、もう一方のコンテナのサービス名を使用します。たとえば postgres:5432 です。Gluetun は v3.41 以降、独自のネットワーク名前空間内から他のコンテナ名を解決します。名前を解決できない場合は、そのバージョン以降を使用してください。
Gluetun のファイアウォールは、Gluetun への接続を開始できる送信元を制御します。Gluetun 自身の Docker ネットワークからのトラフィックは許可されます。別のサブネット上のクライアント、LAN 上のノート PC、別の bridge ネットワーク上のコンテナからのトラフィックは、そのサブネットを指定するまで破棄されます。
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文書化されている意味は明確です。Gluetun と、そのネットワークスタックを共有するコンテナからのアクセスを許可するサブネットを、コンマ区切りで指定します。
インターネットからのインバウンド接続は、別の問題です。torrent クライアントのピアは VPN 側から接続してくるため、ホストでポート 6881 を公開しても機能しません。VPN プロバイダーからポートフォワーディングされたポートが必要です。また、そのポートを FIREWALL_VPN_INPUT_PORTS に指定します。これにより、VPN サーバー側からのポートを許可できます。これは、Docker Compose で構築したメディアスタックの多くで未解決のままになっている部分です。
トンネル切断時のキルスイッチの動作
この構成の複雑さは、障害発生時に効果を発揮します。接続されたコンテナには、別の経路がありません。マシン外部へ出る唯一の経路は、共有している network namespace です。そのため、トンネルが停止すると、フォールバック先はありません。Gluetun のファイアウォールも、反対側から同じルールを適用します。送信トラフィックはトンネルまたは VPN サーバーのエンドポイントを経由し、それ以外はすべて破棄されます。クライアントの再接続中に、通常のインターフェースからパケットが漏れる時間帯はありません。
Gluetun は自身の接続を監視します。1 分ごとに、HEALTH_ICMP_TARGET_IPS に指定されたアドレスへ ICMP echo(ping)を送信します。デフォルト値は 1.1.1.1,8.8.8.8 です。5 分ごとに、HEALTH_TARGET_ADDRESSES へ完全な TCP および TLS(transport layer security)接続を確立します。デフォルト値は cloudflare.com:443,github.com:443 です。これらの接続に失敗すると、コンテナ内の VPN を再起動し、次の内容をログに記録します。
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutこの順序を踏まえて、接続されたコンテナのログを読みます。アプリ内に表示される connection refused、operation not permitted、i/o timeout などの行は、トンネル停止の結果であり、原因ではありません。Gluetun のドキュメントにもこの点が明記されています。結果だけを報告して、その原因を何時間も追い続ける人がいるためです。
HEALTH_RESTART_VPN=on はデフォルトで有効になっているため、そのままにします。特定の障害をデバッグする場合に限り、一時的に無効にしてください。無効にすると、停止したトンネルは停止したままになるためです。
起動順序: トンネル確立前のスタック起動を停止する
このイメージには Docker healthcheck が組み込まれています。
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckこのコマンドは gluetun の短時間だけ実行される別インスタンスを起動し、http://127.0.0.1:9999/ で実行中のインスタンスの health server に問い合わせます。トンネルが正常なら 200 OK を返します。トンネルに問題がある場合はエラー文字列とともに 500 Internal server error を返し、1 回失敗した時点でコンテナは unhealthy と判定されます。
condition: service_healthy がこの状態を待機します。単純な depends_on: [gluetun] はコンテナの起動だけを待機します。コンテナはハンドシェイク完了の数秒前に起動するため、アプリはネットワークが停止した状態で起動し、最初の接続試行で失敗することがよくあります。Docker Compose の healthcheck では、構文とタイミング関連のフィールドを説明しています。
注意が必要な制限が 1 つあります。Compose はコンテナの作成時にこの条件を 1 回だけ評価します。後から gluetun が unhealthy になっても、アプリを停止または再起動しません。その場合は gluetun 内部の自動復旧機能が対応します。そのため、コンテナではなく VPN プロセスが再起動されます。
信頼する前に DNS リークを確認する
DNS(domain name system)は、トンネルが正しく構成されていても発生するリークです。Gluetun は namespace 内で独自のリゾルバーを実行し、デフォルトでは DoT(DNS over TLS)を使用して Cloudflare にクエリを転送します: DNS_UPSTREAM_RESOLVER_TYPE=dot と DNS_UPSTREAM_RESOLVERS=cloudflare。この2つは変更しないでください。名前解決は暗号化され、トンネルを経由します。
この動作を壊す設定が DNS_UPSTREAM_PLAIN_ADDRESSES です。名前解決に失敗したとき、ルーターやプロバイダーのリゾルバーに応答させるために、この設定を使いたくなることがあります。Gluetun のドキュメントには、その代償が明記されています。DNS トラフィックは VPN トンネルを通らず、トンネルの外部へリークします。通信自体はプライベートなままです。しかし、アクセスしたホスト名の一覧は保護されません。同じ問題の WireGuard 版については、WireGuard トンネル経由で DNS の名前解決が停止する場合で説明しています。
テストするには、gluetun で HTTPPROXY=on を設定し、8888:8888/tcp を公開します。次に、そのプロキシをブラウザーで指定し、DNS リークテストを実行します。結果にはプロバイダーまたは Cloudflare が表示される必要があります。自宅のルーターが表示されてはいけません。Gluetun のドキュメントでは、一部のリークテストが不規則な結果を報告することにも注意しています。namespace 内のリゾルバーは、最終的に応答するサーバーではなく、ローカルのキャッシュ中継だからです。異なる国が表示される場合や、自分の ISP のリゾルバーが表示される場合を、実際のリークの兆候として扱ってください。
VPN sidecar に Tailscale を追加する場合と、どちらが優先されるか
Tailscale は、 włas own machines に接続するための WireGuard ベースのオーバーレイネットワークです。管理用の経路をスタック内に確保するため、プロバイダー VPN と並行して動かす構成が使われます。両者が競合することはほとんどありません。理解しておくべき理由があります。Tailscale のドキュメントでは、デフォルト動作が明記されています。Tailscale はオーバーレイネットワークとして動作し、Tailscale を実行しているデバイス間のトラフィックだけをルーティングします。パブリックインターネット向けのトラフィックには干渉しません。
答えは、1 つの設定で決まります。
- Tailscale を独立したコンテナでデフォルト設定のまま動かす場合、アプリの送信トラフィックを Tailscale が見ることはありません。すべてのトラフィックは Gluetun が処理します。Tailscale からは、他の外部コンテナと同じように
gluetun:8080でアプリへ接続できます。 network_mode: "service:gluetun"を使って Tailscale を gluetun の namespace に接続する場合、namespace を共有しても機能は付与されないため、独自のcap_add、net_admin、net_rawが必要です。デフォルトの userspace networking モードではTS_USERSPACEが有効です。tailscaled はインターフェースを作成せず、SOCKS5 または HTTP プロキシとして動作するため、ルーティングを変更できません。引き続き、すべてのトラフィックは Gluetun が処理します。- 同じ構成で
TS_USERSPACE=falseを使う場合、tailscaled はトンネルデバイスを作成して経路を追加します。ただし対象は tailnet の範囲100.64.0.0/10と、TS_ROUTESで広告したサブネット経路だけです。パブリックトラフィックは引き続き gluetun 経由で送信されます。 - 上記のいずれかで exit node を選択し、
sudo tailscale set --exit-node=<exit-node-ip>を使う場合、Tailscale がデフォルト経路を取得して優先されます。これを gluetun と組み合わせないでください。デフォルト経路の所有者は 1 つだけです。
広告した経路が目的であり、ホスト自身だけでなく、そのホストの背後にあるプライベートネットワーク全体へ接続したい場合は、VPS で Tailscale サブネットルーターを実行する方法を参照してください。経路の承認、IP forwarding、そして TS_ROUTES が単独では実行しないクライアント側フラグについて説明しています。
Tailscale を経路ではなく管理用 URL のために使う場合は、tailscale serve と tailscale funnel により、tailnet 向けに gluetun:8080 の前段へ HTTPS を配置できます。public internet に公開するのは funnel だけです。
Tailscale をトンネル内で実行すると、別の影響も現れます。Tailscale のピアからは VPN プロバイダーのアドレスに見えるため、リレーへフォールバックする頻度が高くなります。その場合、tailscale status ではピアの横に direct ではなく relay "..." と表示されます。接続は機能しますが、速度は低下します。必要なのがオーバーレイだけであれば、通常の WireGuard と Tailscale の違いから始めるのが適切です。
発生する問題と表示されるメッセージ
Docker がアプリコンテナの作成を拒否する。 Error response from daemon: conflicting options: port publishing and the container type network mode は、接続先のサービスに ports: ブロックが残っていることを示します。それを gluetun に移動します。
Compose がファイル全体を拒否する。 1 つのサービスで network_mode と networks を同時に設定することはできません。ネットワークを gluetun に設定します。
別のコンテナからアプリを名前解決できない。 接続先のコンテナはどのネットワークにも参加せず、名前も登録していないため、curl: (6) Could not resolve host: qbittorrent は正しい動作です。gluetun とポートを使用します。
2 つ目の接続先コンテナが起動しない。 1 つの namespace で 2 つのプロセスが同じポートを bind することはできません。後から bind したプロセスは、アドレスがすでに使用中であることを示すエラーを報告します。アプリの内部ポートを変更するか、2 つ目の gluetun を実行します。
gluetun を操作した後、アプリのネットワーク接続が失われる。 gluetun を再起動または再作成すると、接続しているすべてのコンテナの接続性が失われます。それらのコンテナを再起動します。
小さいページは読み込めるが、大きいページで処理が止まる。 MTU (maximum transmission unit) の問題です。トンネルによってオーバーヘッドが追加され、経路上の何かがサイズ超過のパケットを破棄しているにもかかわらず、エラーを返していません。WIREGUARD_MTU を下げ、1400 を試してから、1320 を実行します。
Gluetun が healthy にならない。 起動時のチェックに最初に確認すべき項目が示されます。WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout。key の有効期限が切れていないか、次に server list が古くなっていないかを確認し、その後、ホストの firewall が外向き UDP をブロックしていないかを確認します。
FAQ
コンテナの公開ポートが Gluetun 経由で機能しなくなったのはなぜですか?
network_mode: "service:gluetun" によりコンテナが gluetun のネットワーク名前空間に入るためです。1 つの名前空間には、1 つの IP アドレスと 1 組の待ち受けポートしかありません。アプリケーションは待ち受けを続けますが、公開ルールはその名前空間を所有するコンテナに設定する必要があります。ports: の一覧を gluetun サービスへ移してください。接続先サービスに残した場合、Docker は公開ルールを作成しません。エラーは Error response from daemon: conflicting options: port publishing and the container type network mode です。
VPN トンネル内のコンテナに、トンネル外のコンテナから接続するにはどうすればよいですか?
gluetun のサービス名と、アプリケーションが待ち受けるポートを使用します。たとえば gluetun:8080 です。接続先コンテナには独自の Docker ネットワークがないため、そのコンテナ自身の名前は名前解決されません。コンテナ間の通信に公開設定は必要ありません。逆方向では、名前空間内のコンテナから、Gluetun v3.41 以降で postgres:5432 のようにサービス名を使って外部コンテナへ接続できます。LAN 上のラップトップなど、別のサブネットにあるクライアントからの通信は、FIREWALL_OUTBOUND_SUBNETS にそのサブネットを追加するまで gluetun のファイアウォールにより破棄されます。
VPN が切断された場合、Gluetun はキルスイッチとして機能しますか?
はい。2 つの理由があります。接続先コンテナには共有名前空間内の経路以外に経路がないため、トンネルが停止するとマシン外部への経路もなくなります。さらに、Gluetun のファイアウォールは、トンネル経由の通信と VPN サーバーのエンドポイントへの通信だけを外向きに許可します。Gluetun 自体が再起動すると接続先コンテナはすべてネットワークを失うため、Gluetun は終了せず、内部で VPN を再起動して WARN [vpn] restarting VPN because it failed to pass the healthcheck をログに記録します。
同じ stack で Tailscale と Gluetun を使用する場合、外向き通信を処理するのはどちらですか?
1 つの構成を除き、常に Gluetun です。Tailscale はデフォルトでは tailnet 内のデバイス間の通信だけをルーティングし、パブリックな通信には関与しません。コンテナイメージのデフォルトである userspace mode ではインターフェースを作成しないため、ルーティングに影響を与えません。TS_USERSPACE=false を使用した場合も、100.64.0.0/10 と広告したサブネットへの経路だけをインストールします。例外は exit node です。sudo tailscale set --exit-node=<exit-node-ip> により Tailscale がデフォルト経路になり、その場合は Tailscale が優先されます。デフォルト経路を管理する製品は 1 つに決め、両方を重ねて使用しないでください。