DockerコンテナをVPN経由にする方法とポート消失の解決策
Gluetunを使用してDockerコンテナをVPN経由にするとポートが消える原因を解説します。network_mode指定時の仕様と、conflicting optionsエラーを回避し正しく通信をルーティングするためのdocker-compose.yml設定例を紹介します。
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 年 10 月時点の最新リリースは 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 の名前と、アプリケーションが待ち受けるポートを使用します。リバースプロキシコンテナは、gluetun:8080 で qBittorrent の Web インターフェースに接続します。この場合、ports: の設定は不要です。コンテナ間のトラフィックは Docker ネットワーク内にとどまり、ホストのポートを経由しないためです。
内部から外部へ接続する場合は、もう一方のコンテナのサービス名を使用します。たとえば postgres:5432 です。Gluetun は v3.41 以降、自身の namespace 内から他のコンテナ名を解決できるため、名前を解決できない場合はそのバージョン以降を固定してください。
Gluetun のファイアウォールは、Gluetun への接続を開始できる対象を決定します。Gluetun 自身の Docker ネットワークからのトラフィックは許可されます。別のサブネット上のクライアント、LAN 上のラップトップ、別の bridge network 上のコンテナから接続する場合は、Gluetun のファイアウォールとルーティングで戻り経路を許可する必要があります。
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24文書化されている意味は明確です。Gluetun と、そのネットワークスタックを共有するコンテナがアクセスを許可される、コンマ区切りのサブネットを指定します。
インターネットからの受信接続は、別の問題です。torrent クライアントの peer は VPN 側から到達するため、ホストでポート 6881 を公開しても機能しません。プロバイダーからポートフォワーディングを受け、そのポートを FIREWALL_VPN_INPUT_PORTS に指定する必要があります。FIREWALL_VPN_INPUT_PORTS により、VPN サーバー側からのポートが許可されます。この例で使用する Mullvad はポートフォワーディングをサポートしていないため、対応するプロバイダーが必要です。ここが、Docker Compose で構築したメディアスタックの多くで未解決のままになっている部分です。
トンネルが切断された場合のキルスイッチの動作
この構成の複雑さは、障害時に効果を発揮します。接続したコンテナは、Gluetun のネットワークインターフェースとルーティングテーブルを共有します。Gluetun のファイアウォールは、トンネル外への通常のインターネット通信をブロックします。一方で、VPN サーバーのエンドポイント、ローカルの Docker 通信、設定済みの例外は許可します。このファイアウォールにより、トンネル切断時に通常のインターフェースへフォールバックすることを防ぎます。クライアントが再接続する間に、パケットが通常のインターフェースから漏れる時間はありません。
Gluetun は自身の接続を監視します。毎分、HEALTH_ICMP_TARGET_IPS に指定されたアドレスへ ICMP echo(ping)を送信します。デフォルト値は 1.1.1.1,8.8.8.8 です。ICMP が許可されていない場合、この簡単なチェックは代わりに平文の UDP DNS クエリを使用します。5 分ごとに、HEALTH_TARGET_ADDRESSES(デフォルトは cloudflare.com:443,github.com:443)へ TCP と TLS(トランスポート層セキュリティ)による完全な接続を確立します。これらの接続に失敗すると、コンテナ内の 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=3 CMD /gluetun-entrypoint healthcheckこのコマンドは、実行中の gluetun のヘルスサーバーに http://127.0.0.1:9999/ で問い合わせる、短時間だけ実行される gluetun のコピーを起動します。正常なトンネルは 200 OK を返します。異常なトンネルはエラー文字列とともに 500 Internal server error を返し、チェックが3回連続で失敗すると Docker はコンテナを unhealthy と判定します。
condition: service_healthy は、この状態を待機する設定です。通常の depends_on: [gluetun] はコンテナの起動だけを待機します。コンテナはハンドシェイクの完了より数秒早く起動するため、アプリケーションはネットワークが利用できない状態で起動し、最初の接続試行を諦めることがよくあります。Docker Compose のヘルスチェックでは、構文とタイミング関連のフィールドについて説明しています。
注意が必要な制限が1つあります。Compose はスタックの起動時にこの条件を評価します。後から gluetun が unhealthy になっても、アプリケーションを停止または再起動することはありません。その場合は gluetun 内部の自動修復機能が処理します。そのため、コンテナではなく VPN プロセスを再起動します。
セットアップを信頼する前に DNS リークを確認する
DNS(domain name system)は、トンネルが正しく構成されていても発生するリークです。Gluetun は namespace 内で独自の resolver を実行し、デフォルトでは DoT(DNS over TLS)を使用して Cloudflare にクエリを転送します: DNS_UPSTREAM_RESOLVER_TYPE=dot および DNS_UPSTREAM_RESOLVERS=cloudflare。この2つは変更せず、名前解決が暗号化されてトンネル経由で送信される状態にします。
ローカル resolver を指定すると、この動作を壊す設定が DNS_UPSTREAM_PLAIN_ADDRESSES です。名前解決に失敗したときに、ルーターやプロバイダーの resolver に応答させるため、この設定を使いたくなることがあります。Gluetun のドキュメントでは、Docker bridge や LAN 上の resolver に送信したクエリは VPN トンネルの外へリークすると警告しています。公開されている平文 resolver を指定した場合も、クエリ自体はトンネルを通りますが、VPN プロバイダーはその内容を読み取り、変更できます。WireGuard で同じ設定ミスをした場合については、WireGuard トンネル経由で DNS が解決できなくなる場合で説明しています。
テストするには、gluetun で SHADOWSOCKS=on と SHADOWSOCKS_PASSWORD を設定し、8388:8388/tcp と 8388:8388/udp を公開します。次に、UDP DNS をトンネル経由で送信するよう構成した Shadowsocks クライアントで接続し、DNS リークテストを実行します。結果には自分のプロバイダーまたは Cloudflare が表示され、ホームルーターが表示されてはいけません。Gluetun のドキュメントでは、一部のリークテストが不自然な結果を報告する場合があると警告しています。namespace 内の resolver は、最終的に応答するサーバーではなく、ローカルのキャッシュ中継として動作するためです。国が誤って表示されることや、自分の ISP の resolver が表示されることを、実際のリークの兆候として扱ってください。
VPN sidecar の隣に Tailscale を追加する場合と、どちらが優先されるか
Tailscale は、自分のマシンへ接続するための WireGuard ベースのオーバーレイネットワークです。管理用の経路をスタック内に確保するため、プロバイダー VPN と併用されることがあります。ネットワーク名前空間を共有すると、ファイアウォールルールも共有されます。そのため、両者で互換性のあるファイアウォール設定とルーティング設定が必要です。Tailscale のドキュメントにあるデフォルト動作では、Tailscale はオーバーレイネットワークとして機能し、Tailscale を実行しているデバイス間の通信だけをルーティングします。パブリックインターネット向けの通信には関与しません。
答えは、1 つの設定に依存します。
- Tailscale を独自のコンテナで実行し、デフォルト設定を使う場合、Tailscale はアプリの外向き通信をまったく処理しません。すべての通信は Gluetun が処理します。Tailscale コンテナ内のプロセスは、共有 Docker ネットワーク上の
gluetun:8080を通じてアプリへ接続できます。この接続を tailnet クライアントへ公開するには、転送またはプロキシが必要です。 network_mode: "service:gluetun"を使って Tailscale を gluetun の名前空間に接続した場合、デフォルトの userspace networking モードではTS_USERSPACEが有効です。tailscaled はインターフェイスを作成せず、SOCKS5 または HTTP プロキシとして動作するため、ルーティングを変更できません。アプリが明示的にそのプロキシを使わない限り、通常の外向き通信は引き続き Gluetun が処理します。- 同じ構成で
TS_USERSPACE=falseを使う場合、Tailscale には専用の/dev/net/tunデバイスとケイパビリティが必要です。これらは名前空間を共有するだけでは付与されません。tailscaled はトンネルデバイスを作成し、Linux では通常、100.64.0.0/10用の単一の経路ではなく、各 IPv4 peer に対する /32 ルートを個別に設定します。TS_USERSPACE=falseだけを設定しても、集約ルートは有効になりません。別ノードのサブネットルートをインストールするには、--accept-routesをTS_EXTRA_ARGS経由で渡す必要があります。TS_ROUTESは逆方向に機能します。このコンテナから到達できるサブネットを、tailscale set --advertise-routes=に相当する形で tailnet の他のノードへ広告しますが、このコンテナにはルートを追加しません。パブリック向けの通信は引き続き gluetun 経由で送信されます。 - exit node を選択した状態で gluetun の名前空間内の kernel networking を使い、
sudo tailscale set --exit-node=<exit-node-ip>を指定する場合、Tailscale はその名前空間にデフォルトルートをインストールします。ただし、Gluetun のファイアウォールがその経路をブロックすることがあります。別のコンテナで exit node を選択しても、アプリのルートは変わりません。userspace モードでは、そのプロキシへ明示的に送信された通信だけが処理されます。gluetun の名前空間内で競合するデフォルトルートを併用しないでください。デフォルトルートは 1 つにし、所有者も 1 つにします。
広告したルートが目的であり、ホスト単体ではなく、そのホストの背後にあるプライベートネットワーク全体へ到達したい場合は、VPS で Tailscale サブネットルーターを実行する方法で、ルートの承認、IP forwarding、そして TS_ROUTES だけでは実行されないクライアント側のフラグを確認できます。
Tailscale の目的がルートではなく管理用 URL の提供であれば、tailscale serve と tailscale funnel により、ローカルの HTTP サービスの前段に HTTPS を配置できます。パブリックインターネットへ公開するのは funnel だけです。Tailscale v1.104.5 では、Tailscale コンテナから共有 Docker ネットワーク経由で到達できる場合、これらの HTTP リバースプロキシは http://gluetun:8080 を直接受け付けます。localhost 以外を対象にする場合は、http:// スキームを含めてください。gluetun のネットワーク名前空間を共有し、http://127.0.0.1:8080 を対象にする構成も機能します。
Tailscale をトンネル内で実行すると、別の影響も確認できます。Tailscale の peer からは VPN プロバイダーのアドレスに見えるため、リレーへ切り替わる頻度が高くなります。その場合、tailscale status では peer の横に 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 番目の接続先コンテナが起動しない。 1 つの namespace 内の 2 つのプロセスは、同じポートを bind できません。後から bind するプロセスは、アドレスがすでに使用中であることを示します。app の内部ポートを変更するか、2 つ目の gluetun を実行します。
gluetun を操作した後、app がネットワークに接続できない。 gluetun を再起動または再作成すると、接続されているすべてのコンテナの接続が失われます。これらのコンテナを再起動します。
小さいページは読み込めるが、大きいページが停止する。 これは MTU(maximum transmission unit)の問題です。トンネルによってオーバーヘッドが追加されるため、経路上の何かがサイズ超過のパケットをエラーを返さずに破棄しています。WIREGUARD_MTU を確認します。v3.41.3 のデフォルト値は 1320 です。経路でより小さいパケットが必要な場合は、この値を下げます。この設定を指定しない場合、development image は path MTU discovery を使用します。
Gluetun が正常状態にならない。 起動チェックでは、最初に確認すべき項目として 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 つの 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 上のノート PC から接続する場合は、gluetun のファイアウォールとルーティングで許可された戻り経路が必要です。そのサブネットを FIREWALL_OUTBOUND_SUBNETS に追加してください。
VPN が切断された場合、Gluetun はキルスイッチとして機能しますか?
はい。ファイアウォールを有効にしている場合に機能します。接続先コンテナは Gluetun のインターフェイスとルートを共有します。ファイアウォールは、トンネル外への通常のインターネット通信をブロックし、VPN サーバーのエンドポイント、ローカルの Docker 通信、設定済みの例外は許可します。Gluetun はその後、WARN [vpn] restarting VPN because it failed to pass the healthcheck と記録しながら VPN を内部的に再起動します。gluetun 自体を再起動すると、接続先のすべてのコンテナがネットワークを失うため、終了するのではありません。
同じスタックで Tailscale と Gluetun を使う場合、外向きの通信はどちらが担いますか?
明示的に Tailscale 経由にしない限り、通常の外向き通信は Gluetun が担います。Tailscale はデフォルトでは tailnet 内のデバイス間の通信だけをルーティングし、パブリックな通信には介入しません。コンテナイメージのデフォルトの userspace モードでは、インターフェイス自体を作成しないため、ルーティングに影響を与えられません。TS_USERSPACE=false を使用すると、Linux は通常、集約された 100.64.0.0/10 ルートではなく、ピアごとの IPv4 /32 ルートを取得します。また、--accept-routes を TS_EXTRA_ARGS 経由で追加した場合に限り、他のノードのサブネットルートも取得します。TS_ROUTES はこのコンテナのサブネットを tailnet に広告しますが、ローカルルートは追加しません。sudo tailscale set --exit-node=<exit-node-ip> で exit node を選択すると、Tailscale の kernel networking を使用する名前空間にだけデフォルトルートが設定されます。別のコンテナを選択してもアプリのルートは変わらず、userspace モードではプロキシ経由で送信された通信だけが処理されます。デフォルトルートを管理する製品は 1 つに決め、両方を重ねて使用しないでください。