ClashでDockerを透過プロキシ化する高度な設定と障害対策

Docker HubやGHCRからのイメージ取得が遅い、npmやpipの通信が失敗するといった問題を、Clash/Mihomoの透過プロキシで解決します。TUNとredir-hostの違い、コンテナDNS、ゲートウェイ経由の通信、rule-providers、YAML設定、ログを使った切り分けまで実運用向けに解説…

Dockerのプロキシ方式を先に整理する

Dockerの通信をClashまたはmihomo経由にしたい場合、最初に「どのプロセスの通信をプロキシ化するのか」を分けて考える必要があります。Dockerには、Docker CLIがレジストリへ接続する場合、Dockerデーモンがイメージを取得する場合、コンテナ内のアプリケーションが外部APIへ接続する場合という、少なくとも3種類の通信があります。ホストのブラウザーがClashで正常に開けても、Dockerデーモンやコンテナが自動的に同じ経路を使うとは限りません。

環境変数にHTTP_PROXYHTTPS_PROXYを設定する方法は、HTTPベースの通信には有効です。しかし、すべてのプログラムが環境変数を読み取るわけではなく、DNS問い合わせ、UDP通信、プロキシに対応していないバイナリは別途処理が必要です。特にDockerのイメージ取得では、Dockerデーモンの設定と、イメージ内で実行されるアプリケーションの設定を混同しないでください。

通信の主体 主な用途 確認する設定
Docker CLI docker pullなどの操作 シェル環境、Dockerクライアント設定
Dockerデーモン レジストリからのイメージ取得、認証 systemdのサービス環境、daemon設定
コンテナ パッケージ取得、外部API、アプリケーション通信 コンテナ環境変数、ネットワーク、TUN経路
DNSリゾルバー レジストリや依存サイトの名前解決 DockerのDNS、mihomoのDNS設定、ルーティング

DockerデーモンにHTTPプロキシを設定する

イメージの取得だけを安定させたい場合は、まずDockerデーモンのプロキシ設定を試してください。LinuxでDockerがsystemdサービスとして動作しているなら、サービスのドロップイン設定にプロキシ環境変数を追加します。Clashがホストのループバックアドレスだけで待ち受けている場合、Dockerデーモンが同じホスト上で動作していても、サービスの実行環境や権限によって到達方法が異なることがあります。最初は127.0.0.1のポートが実際に待ち受けているかを確認します。

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/proxy.conf >/dev/null <<'EOF'
[Service]
Environment="HTTP_PROXY=http://127.0.0.1:7890"
Environment="HTTPS_PROXY=http://127.0.0.1:7890"
Environment="NO_PROXY=localhost,127.0.0.1,::1,.local,registry.local"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker
sudo systemctl show --property=Environment docker

設定後は、いきなり大きなイメージを取得するのではなく、比較的小さな公開イメージで検証します。docker pullが成功しても、すべてのレイヤーがプロキシ経由になったとは限りません。DockerデーモンのログとClashの接続ログを同じ時刻に確認し、レジストリのホスト名、認証エンドポイント、レイヤー配信元のドメインがどのように処理されたかを見ます。

sudo journalctl -u docker --since "10 minutes ago" --no-pager
docker pull alpine:latest
docker image inspect alpine:latest

企業内レジストリやLAN上のミラーを使う場合は、NO_PROXYに内部ドメインとプライベートアドレスを追加します。すべての通信をNO_PROXY=*で除外すると、設定したプロキシが一切使われなくなるため注意してください。逆に、社内レジストリを外部プロキシへ送ると、認証情報が意図しない経路へ流れたり、内部DNS名を解決できなくなったりします。

デーモン設定で起きやすい失敗

コンテナの外向き通信をプロキシ化する

Dockerデーモンのプロキシ設定は、通常、イメージ取得に適用されます。コンテナ内のaptapknpmpip、アプリケーションのHTTP API通信まで自動的に変更する設定ではありません。コンテナにプロキシ環境変数を渡す場合は、実行時に明示します。

docker run --rm \
  -e HTTP_PROXY=http://host.docker.internal:7890 \
  -e HTTPS_PROXY=http://host.docker.internal:7890 \
  -e ALL_PROXY=socks5://host.docker.internal:7891 \
  -e NO_PROXY=localhost,127.0.0.1,.internal \
  alpine:latest sh -c 'env | grep -i proxy'

host.docker.internalはDocker Desktopで使いやすい名前ですが、LinuxのDocker Engineでは環境によって自動定義されません。その場合は、実行時にホストゲートウェイを登録します。

docker run --rm \
  --add-host=host.docker.internal:host-gateway \
  -e HTTP_PROXY=http://host.docker.internal:7890 \
  -e HTTPS_PROXY=http://host.docker.internal:7890 \
  alpine:latest wget -qO- https://example.com

この方式では、ClashのプロキシポートがDockerブリッジから到達できなければなりません。Clash側が127.0.0.1だけで待ち受けている場合、コンテナからホストのループバックへ接続することはできません。allow-lan: trueとLAN向けの待ち受けアドレスを使用する方法もありますが、プロキシポートを無制限に公開するのは危険です。ファイアウォールでDockerブリッジのサブネットだけを許可し、外部インターフェースから直接アクセスできないようにしてください。

mixed-port: 7890
allow-lan: true
bind-address: "*"
mode: rule
log-level: info

Composeで設定を固定する

手動のdocker runで毎回環境変数を入力すると、サービスごとに設定がずれます。Composeを使う場合は、プロキシのURLと除外対象をサービス単位で明示します。機密情報や環境ごとに異なるホスト名は、.envファイルやシークレット管理を使い、YAMLへ直接固定しない方法が安全です。

services:
  worker:
    image: example/worker:latest
    extra_hosts:
      - "host.docker.internal:host-gateway"
    environment:
      HTTP_PROXY: http://host.docker.internal:7890
      HTTPS_PROXY: http://host.docker.internal:7890
      NO_PROXY: localhost,127.0.0.1,db,.internal
    depends_on:
      - db

  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: change-me

データベース、メッセージキュー、監視エージェントなど、内部通信しか行わないサービスに外部プロキシを渡す必要はありません。プロキシ環境変数を全サービスへ一括注入すると、内部接続までプロキシへ送信して障害になることがあります。外部アクセスが必要なコンテナだけを対象にしてください。

TUNを使った透過プロキシの設計

環境変数を使えないバイナリ、DNS以外のUDP通信、プロキシ対応を持たないアプリケーションまで処理したい場合は、ホスト上のmihomo TUNを検討します。TUNは仮想ネットワークインターフェースで受け取ったIPパケットをカーネルへ渡し、ルール判定後にプロキシまたは直接接続へ振り分けます。これはコンテナへプロキシ変数を渡す方式とは異なり、経路、ルーティング、DNS、権限をまとめて設計する必要があります。

tun:
  enable: true
  stack: mixed
  auto-route: true
  auto-detect-interface: true
  dns-hijack:
    - any:53

dns:
  enable: true
  enhanced-mode: fake-ip
  nameserver:
    - https://1.1.1.1/dns-query
  fake-ip-filter:
    - "*.lan"
    - "*.local"

上記は構成の考え方を示す例であり、すべてのクライアントやカーネルで同じフィールドが利用できるとは限りません。FlClashなどのGUIでは、TUNのスタック、自動ルート、厳格ルート、DNSハイジャックに相当する項目を画面から設定する場合があります。実際に有効になった設定をYAMLまたはカーネルログで確認してください。

DockerブリッジのサブネットをTUNが誤ってプロキシへ送り返すと、ループが発生します。Dockerデーモンが使う内部アドレス、ホストの管理ネットワーク、クラスターネットワークは、必要に応じて直接接続のルールやルート除外へ追加します。Dockerのサブネットは環境により異なるため、固定値を盲目的にコピーしないでください。

docker network inspect bridge
ip route
ip addr show

たとえばDockerブリッジが172.17.0.0/16であるとは限りません。複数のComposeプロジェクト、Podman、VPN、Kubernetesを同じホストで使うと、172.18.0.0/16や別のプライベートレンジが追加されます。実際の出力を確認してから、内部宛ての通信がプロキシへ入らないように調整します。

Docker DNSとFake-IPの衝突を避ける

コンテナは通常、Dockerが提供するDNSリゾルバーへ問い合わせます。ホスト側でFake-IPを使っている場合、コンテナから見えるDNS応答と、ホストのTUNが期待するDNS経路が一致しないことがあります。名前解決は成功しても、返されたアドレスへ接続できない、HTTPSのSNIと宛先が合わない、レジストリだけタイムアウトするといった症状が発生します。

切り分けでは、まずコンテナから名前解決とHTTPS接続を別々に確認します。DNSだけ、TCP接続だけ、TLSを含むHTTP接続という順番で試すと、失敗箇所を限定できます。

docker run --rm --add-host=host.docker.internal:host-gateway \
  alpine:latest sh -c '
    apk add --no-cache bind-tools curl
    nslookup registry-1.docker.io
    curl -Iv --connect-timeout 10 https://registry-1.docker.io/v2/
  '

認証が必要なレジストリでは、/v2/への応答が必ずしも200になるとは限りません。401が返っても、HTTPS接続とレジストリの到達自体は成功している場合があります。DNSエラー、接続タイムアウト、TLS証明書エラー、認証エラーを同じ「Dockerが使えない」とまとめず、レスポンスの種類を記録してください。

レジストリのルール、ログ、速度を検証する

Dockerでアクセスするドメインは、使用するレジストリや認証方式によって変わります。Docker Hubでは、イメージのメタデータ、認証、レイヤー配信で複数のホスト名が登場することがあります。プライベートレジストリでも、証明書、認証サーバー、オブジェクトストレージを別ドメインに分離している構成があります。単一のレジストリ名だけをルールへ追加して、認証やレイヤー取得を見落とさないようにします。

rules:
  - DOMAIN-SUFFIX,docker.io,PROXY
  - DOMAIN-SUFFIX,docker.com,PROXY
  - DOMAIN,registry-1.docker.io,PROXY
  - DOMAIN,auth.docker.io,PROXY
  - DOMAIN-SUFFIX,ghcr.io,PROXY
  - DOMAIN-SUFFIX,quay.io,PROXY
  - DOMAIN-SUFFIX,registry.local,DIRECT
  - GEOIP,LAN,DIRECT
  - MATCH,DIRECT

実際のルール名やプロキシグループ名は、現在の設定にある名前へ置き換えてください。ルールは上から順に評価されます。広すぎるDOMAIN-SUFFIX,ioのようなルールを先に置くと、内部レジストリまで外部プロキシへ送られることがあります。プライベートレジストリのドメインを直接接続にする場合は、DNS、証明書、ネットワークの到達性が内部環境で成立していることも確認します。

Clashやmihomoのログレベルを一時的にdebugへ変更すると、ルール判定や接続失敗の情報が増えます。ただし、長時間の常用はログ量とCPU使用量を増やします。検証が終わったらinfoまたは環境に適したレベルへ戻してください。URLにトークンが含まれる認証リクエストや、ヘッダーを含むログをそのまま共有しないことも重要です。

症状 優先して確認する項目 次の対策
proxyconnect tcp: connection refused プロキシポート、待ち受けアドレス、Clashの起動状態 ss -lntpとカーネルログを確認する
名前解決だけ失敗する Docker DNS、TUNのDNSハイジャック、Fake-IP除外 nslookupと直接接続を別々に検証する
認証画面で止まる レジストリ本体と認証ドメインのルール HTTPステータスと接続ログを時刻で照合する
最初は速いがレイヤー取得が遅い 配信CDN、MTU、QUIC、プロキシノードの混雑 同じタグで複数ノードと直接接続を比較する
コンテナ内部から外部APIだけ失敗する コンテナ環境変数、NO_PROXY、アプリのプロキシ対応 実行環境の変数とアプリ固有の設定を確認する

速度を比較するときは、同じイメージ、同じタグ、同じネットワーク、同じキャッシュ条件を使います。Dockerは取得済みのレイヤーを再利用するため、2回目のdocker pullだけを見て「プロキシが速い」と判断することはできません。テスト前にイメージの状態を確認し、必要なら別タグを使って、DNS解決時間、接続確立時間、総ダウンロード時間を分けて記録してください。

docker system df
docker images
time docker pull alpine:3.20
sudo journalctl -u docker --since "5 minutes ago" --no-pager

障害発生時の切り戻しと運用チェック

設定変更後にDocker全体が通信できなくなった場合は、まずTUNを一時停止し、Dockerデーモンのプロキシドロップインを退避してサービスを再起動します。変更前の設定を残しておけば、ClashのルールやDNSを一つずつ戻すより短時間で原因範囲を限定できます。Dockerソケット、証明書、レジストリの認証情報を削除する必要は通常ありません。

sudo mv /etc/systemd/system/docker.service.d/proxy.conf \
  /etc/systemd/system/docker.service.d/proxy.conf.disabled
sudo systemctl daemon-reload
sudo systemctl restart docker
docker info
docker pull alpine:3.20

運用環境では、Dockerデーモンのプロキシ設定、Composeの環境変数、mihomoのTUN設定、DNSの例外、内部レジストリのルールを別々のファイルとして管理すると、更新時の差分を追跡しやすくなります。最初からすべてのコンテナを透過プロキシへ入れるのではなく、イメージ取得、外部API、パッケージ管理という目的ごとに必要な方式を選ぶことが、速度と安定性の両方を保つ近道です。

FlClash ダウンロード 各プラットフォームのクライアントを見る