Dockerのプロキシ方式を先に整理する
Dockerの通信をClashまたはmihomo経由にしたい場合、最初に「どのプロセスの通信をプロキシ化するのか」を分けて考える必要があります。Dockerには、Docker CLIがレジストリへ接続する場合、Dockerデーモンがイメージを取得する場合、コンテナ内のアプリケーションが外部APIへ接続する場合という、少なくとも3種類の通信があります。ホストのブラウザーがClashで正常に開けても、Dockerデーモンやコンテナが自動的に同じ経路を使うとは限りません。
環境変数にHTTP_PROXYやHTTPS_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名を解決できなくなったりします。
デーモン設定で起きやすい失敗
- 再起動前に確認している:systemdの設定ファイルを編集しただけでは、既存のDockerプロセスに環境変数は反映されません。
daemon-reloadとDockerサービスの再起動を実行します。 - HTTPSプロキシにTLSのURLを指定する:多くのClash混合ポートでは、HTTPS宛ての通信もHTTP CONNECTで受け付けます。そのため値は環境に応じて
http://127.0.0.1:7890とし、実際のカーネル仕様を確認してください。 - ポートを間違えている:
9090は外部コントロールインターフェースであり、通常のWebプロキシポートではありません。 - 資格情報をURLに直接書く:プロキシのユーザー名やパスワードをサービスファイルへ書くと、管理者権限を持つユーザーから見える場合があります。可能なら認証不要のローカルポートを使います。
- 設定を変えたのに古い接続を見ている:以前に取得済みのレイヤーはローカルキャッシュから返されます。通信を確認する際は、未取得のタグまたはキャッシュを考慮したテストを使います。
コンテナの外向き通信をプロキシ化する
Dockerデーモンのプロキシ設定は、通常、イメージ取得に適用されます。コンテナ内のapt、apk、npm、pip、アプリケーションの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
- Clashまたはmihomoが実際に稼働し、想定したポートで待ち受けているか確認する。
- Dockerデーモンとコンテナで、プロキシ設定が別々に存在することを記録する。
NO_PROXYに内部レジストリ、Dockerソケット周辺の内部名、データベース名を必要に応じて追加する。- DockerブリッジとComposeネットワークのサブネットを確認し、TUNのルーティングループを防ぐ。
- Clashのルールログ、Dockerのサービスログ、コンテナのアプリケーションログを同じ時刻で比較する。
- 検証終了後は
debugログを戻し、不要なLAN公開や広すぎるプロキシ許可を解除する。
運用環境では、Dockerデーモンのプロキシ設定、Composeの環境変数、mihomoのTUN設定、DNSの例外、内部レジストリのルールを別々のファイルとして管理すると、更新時の差分を追跡しやすくなります。最初からすべてのコンテナを透過プロキシへ入れるのではなく、イメージ取得、外部API、パッケージ管理という目的ごとに必要な方式を選ぶことが、速度と安定性の両方を保つ近道です。