DockerコンテナをVPN経由でルーティングする方法
Gluetunのnetwork_mode: service:gluetunでポートが消える理由を解説します。共有ネットワーク名前空間の仕組みと、動作するcomposeファイルを紹介します。
VPN 経由で Docker コンテナをルーティングするとポートが消える理由
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ここでは Gluetun を使用します。Gluetun は商用 VPN (virtual private network) プロバイダーに WireGuard または OpenVPN で接続し、自身のファイアウォール機能も備えたコンテナです。2026 年 8 月時点の現行リリースは v3.41.3 です。例では WireGuard と Mullvad を使用するため、プロバイダーのアカウントと鍵が必要です。トンネルを自分で所有するハードウェア上で終端する場合は、VPS 上で自分の WireGuard サーバーを実行することで対向側を構築できます。また、Docker で wg-easy を使用すると、それを Web インターフェースで管理できます。
実際の network_mode: "service:gluetun" の動作
通常、Docker コンテナごとに独自のネットワーク名前空間が作成されます。そこには、独自のインターフェース、ルーティングテーブル、ファイアウォールルール、待ち受けソケットがあります。service: モードではこの処理を省略し、gluetun の名前空間内でコンテナを起動します。名前空間が同じなら IP アドレスも同じになり、次の6点が変わります。
- アプリに独自のアドレスはありません。gluetun のアドレスを使用します。
- アプリは Docker ネットワークに接続されないため、サービス名は登録されず、名前解決もできません。他のコンテナからは
gluetunを使用する必要があります。 - 名前空間内のコンテナ同士は
localhostで通信します。 - 1つの名前空間内にある2つのコンテナは、同じポートで待ち受けできません。Gluetun のドキュメントにも明記されているとおり、回避策はありません。
- Capability は名前空間ではなくコンテナに属します。トンネルインターフェースを作成するため、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 のアドレスが表示されるはずです。サーバー自身のアドレスが表示される場合、アプリは tunnel 内に入っていません。その場合、以下の内容は説明どおりに動作しません。
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 です。v3.41 以降の Gluetun は、自身の namespace 内から他のコンテナ名を解決します。名前を解決できない場合は、このバージョン以降を指定してください。
Gluetun のファイアウォールは、Gluetun への接続を開始できる送信元を制御します。Gluetun 自身の Docker ネットワークからの通信は許可されます。別のサブネット上のクライアント、LAN 上のラップトップ、別の bridge network 上のコンテナからの通信は、そのサブネットを指定するまで破棄されます。
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文書に記載された意味は明確です。Gluetun とそのネットワークスタックを共有するコンテナがアクセスを許可されるサブネットを、コンマ区切りで指定します。
インターネットからの受信接続は、別の問題です。torrent クライアントのピアは VPN 側から接続してくるため、ホストでポート 6881 を公開しても機能しません。プロバイダーからポートフォワーディングを受け、そのポートを FIREWALL_VPN_INPUT_PORTS に指定する必要があります。FIREWALL_VPN_INPUT_PORTS により、VPN サーバー側からのポートを許可できます。これは、多くの Docker Compose で構築したメディアスタックで未設定のままになっている部分です。
キルスイッチ: トンネルが切断された場合の動作
この構成の複雑さは、障害時に意味を持ちます。接続されたコンテナには、別の経路がありません。マシン外部への唯一の経路は共有している 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 --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckこのコマンドは、実行中の gluetun のヘルスサーバーに http://127.0.0.1:9999/ で問い合わせる、短時間だけ動作する gluetun の別プロセスを起動します。正常なトンネルは 200 OK を返します。異常なトンネルはエラー文字列を含む 500 Internal server error を返し、1 回の失敗でコンテナが unhealthy と判定されます。
condition: service_healthy がこの状態を待機します。通常の depends_on: [gluetun] はコンテナの起動だけを待機します。コンテナはハンドシェイク完了の数秒前に起動するため、アプリはネットワークが利用できない状態で起動し、最初の接続試行を断念することがよくあります。Docker Compose のヘルスチェックでは、構文とタイミング関連のフィールドについて説明しています。
注意が必要な制限が 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 のリゾルバーが表示されることを、実際のリークの兆候として扱ってください。
Tailscale を VPN サイドカーの隣に追加する場合と、どちらが優先されるか
Tailscale は、所有するマシンに接続するための WireGuard ベースのオーバーレイネットワークです。管理経路をスタック内に確保するため、プロバイダー VPN と併用されることがあります。両者が競合することはほとんどありません。その理由を理解しておく価値があります。Tailscale のドキュメントでは、デフォルト動作が次のように説明されています。Tailscale はオーバーレイネットワークとして動作し、Tailscale を実行しているデバイス間のトラフィックだけをルーティングします。パブリックインターネット向けのトラフィックには干渉しません。
結論は、1 つの設定に左右されます。
- Tailscale を独立したコンテナでデフォルト設定のまま実行する場合、アプリの送信トラフィックを認識することはありません。すべてのトラフィックは Gluetun が処理します。Tailscale からは、他の外部コンテナと同じように
gluetun:8080でアプリへ接続できます。 network_mode: "service:gluetun"を使用して Tailscale を gluetun の namespace に接続する場合、namespace を共有しても capability は付与されないため、独自の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 つにします。
Tailscale をトンネル内で実行すると、1 つの副作用が現れます。ピアからは VPN プロバイダーのアドレスに見えるため、リレーへフォールバックする頻度が高くなります。その場合、tailscale status ではピアの横に direct ではなく relay "..." と表示されます。接続は機能しますが、速度は低下します。実際に必要なのがオーバーレイだけであれば、通常の WireGuard と Tailscale の違いから始めるのが適切です。
発生する問題と表示されるメッセージ
Docker が app コンテナの作成を拒否します。 Error response from daemon: conflicting options: port publishing and the container type network mode は、接続先のサービスに ports: ブロックが残っていることを示します。これを gluetun に移動します。
Compose がファイル全体を拒否します。 1 つのサービスに network_mode と networks の両方を設定することはできません。ネットワークを gluetun に設定します。
別のコンテナから app を名前解決できません。 接続先のコンテナがネットワークに参加せず、名前も登録していないため、curl: (6) Could not resolve host: qbittorrent となるのは正しい動作です。gluetun とポートを使用します。
2 つ目の接続先コンテナが起動しません。 同じ namespace 内の 2 つのプロセスは同じポートを bind できません。後から bind したプロセスは、アドレスがすでに使用中であることを示すエラーを報告します。app の内部ポートを変更するか、2 つ目の gluetun を実行します。
gluetun を操作した後、app がネットワークに接続できません。 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 が古くなっていないか確認します。その後、ホストのファイアウォールが外向きの 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 は終了せず、WARN [vpn] restarting VPN because it failed to pass the healthcheck をログに記録しながら内部で VPN を再起動します。
同じ 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 つに決め、両方を重ねて使用しないでください。