Dockerのイメージ取得を高速化するClash透過プロキシ設定術

Docker開発で起きやすいイメージ取得エラーを対象に、Clashをホスト側の透過プロキシとして利用する方法を紹介します。Docker Engine、コンテナ、CIランナーの設定と切り分けを学べます。

Docker通信がホストのプロキシ設定を使わない理由

Dockerで docker pull を実行すると、画面上ではホストのターミナルからコマンドを入力しています。しかし、イメージを取得している主体は通常のシェルやブラウザではなく、Docker CLIから要求を受けたDocker Engineです。Linuxではdockerdがホスト上のサービスとして動作し、Docker Desktopでは仮想マシン内部のEngineが通信を担当します。この違いを理解しないままClashのシステムプロキシをオンにしても、イメージレイヤーのダウンロード経路は変わりません。

システムプロキシは、アプリケーションがその設定を読み取る場合にだけ有効です。ブラウザはOSのHTTPプロキシを参照できますが、Docker Engineは独立したデーモンであり、起動時の環境変数やdaemon設定を使ってプロキシを決定します。さらに、コンテナ内で実行する aptnpmpip、アプリケーション自身の通信も、それぞれ別のネットワーク名前空間と設定を持ちます。

そのため、Dockerでは通信する場所を3つに分けて考える必要があります。第一はイメージのmanifestとlayerを取得するDocker Engine、第二はビルド中に外部へアクセスするBuildKitまたはビルドコンテナ、第三は起動後のコンテナです。Clashをホスト側のHTTPプロキシとして使う場合も、この3箇所は別々に設定しなければなりません。

通信主体主な用途設定場所よくある誤解
Docker Engineイメージのpull、認証、registry通信systemdまたはDocker Desktopの設定シェルのHTTP_PROXYだけで動くと思う
BuildKitDockerfileのRUN、依存パッケージ取得build引数またはbuilder設定pullの設定がそのままビルドにも伝わると思う
コンテナ起動後のAPI、更新、外部サービス接続compose.yamlやコンテナ環境変数ホストの127.0.0.1へ接続できると思う
CIランナー自動ビルド、テスト、公開ランナー環境とDocker daemonローカルのClash設定をCIが共有すると思う

Clash側でDocker向けの待受を準備する

Docker EngineをClashへ接続するには、まずClashがホストのループバックアドレスだけでなく、Dockerから到達できるアドレスでHTTPまたはmixedポートを待ち受ける必要があります。一般的な設定では mixed-port がHTTPとSOCKS5の両方を受け付けます。Docker Engineのdaemon設定にはHTTPプロキシとして指定するため、用途を明確にしたい場合は port または mixed-port のどちらを使うか確認してください。

ホスト以外から接続させる場合、Clashの allow-lan を有効にします。ただし、待受アドレスを無制限に公開すると、同じネットワーク上の別端末からプロキシを利用される可能性があります。自宅や開発用の閉じたネットワークでも、OSのファイアウォールで必要なインターフェースとポートだけを許可し、管理APIの外部公開とは分けてください。

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

external-controller: 127.0.0.1:9090

上の例では、ホストのLANアドレスやDockerブリッジ経由でポート7890へ接続できます。external-controller は管理画面用のAPIであり、Dockerの通信先にする必要はありません。管理APIを 0.0.0.0 で公開すると設定変更まで外部から操作される危険があるため、通常は127.0.0.1に固定します。

127.0.0.1は接続元から見た自分自身

コンテナ内の 127.0.0.1:7890 はClashが動作するホストではなく、そのコンテナ自身を指します。Linuxの通常ブリッジではホストゲートウェイのアドレス、Docker Desktopではホスト名 host.docker.internal など、実行環境に合ったアドレスを使ってください。

待受ポートを確認する

設定を読み込んだ後、Clashのログに設定エラーがないことを確認します。ホスト側では次のコマンドで7890番ポートの待受状態を確認できます。

ss -lntp | grep 7890
curl -x http://127.0.0.1:7890 https://example.com -I

最初のコマンドで 127.0.0.1:7890 だけが表示される場合、Dockerブリッジから到達できない可能性があります。0.0.0.0:7890 またはホストのDockerインターフェースに割り当てられたアドレスで待ち受けているかを確認し、変更後にClashを再起動してください。curlの結果だけで外部通信全体を判断せず、後続のDockerテストでもClashの接続ログを照合します。

Docker EngineにClashを設定する

LinuxのDocker Engineをsystemdサービスとして動かしている場合、dockerdへプロキシ環境変数を渡す方法が扱いやすく、設定の所在も明確です。ここではホスト側のClashがLANアドレス 192.168.1.20、mixedポート7890で待ち受けている例を使います。実際の環境では、ホストの固定アドレスまたはDockerから到達できるゲートウェイアドレスに置き換えてください。

sudo mkdir -p /etc/systemd/system/docker.service.d

sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf <<'EOF'
[Service]
Environment="HTTP_PROXY=http://192.168.1.20:7890"
Environment="HTTPS_PROXY=http://192.168.1.20:7890"
Environment="NO_PROXY=127.0.0.1,localhost,::1,registry.local,192.168.0.0/16,172.16.0.0/12,10.0.0.0/8"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

HTTP_PROXYHTTPS_PROXY は、HTTPSのregistryへ接続する場合もプロキシの入口がHTTP形式なら同じURLで指定できます。Clashのmixedポートが受け付けるプロトコルと、Dockerのdaemon設定で求められるURL形式を混同しないことが重要です。URL末尾のポート番号を省略すると通常の80番へ接続してしまうため、7890のような実際の待受ポートを必ず記述します。

NO_PROXY には、ローカルregistry、社内ドメイン、プライベートIP帯を登録します。内部registryまで外部ノードへ送ると、名前解決や認証、証明書検証が失敗する場合があります。CIDRの記法に対応するかどうかはDockerのバージョンと実行環境によって差が出るため、必要なら registry.local のようにホスト名を明示し、ログで直接接続になっているか確認してください。

Docker Desktopでの注意点

Docker DesktopではDocker EngineがLinux仮想環境内で動作します。ホストOSのClashポートを仮想環境から利用できるかは、Windows、macOS、ネットワークモードによって異なります。Docker DesktopのSettingsにProxy項目がある場合は、まずそこへClashのHTTPプロキシURLを入力します。自動プロキシ設定と手動設定を同時に有効にすると、どの値が優先されたか分かりにくくなるため、一度に1方式だけを有効にしてください。

Docker Desktopの設定変更後はEngineを再起動し、次のコマンドで状態を確認します。

docker info
docker pull alpine:3.20

docker info の末尾にHTTP ProxyやHTTPS Proxyが表示されるバージョンでは、daemonが設定を読み込んだかを確認できます。表示されない場合でも、直ちに失敗とは限りません。最終的には docker pull 実行時のClash接続ログ、Dockerのエラー内容、registryへの到達テストを組み合わせて判断します。

コンテナとビルド処理へプロキシを渡す

Docker Engineの設定は、通常イメージを取得するdaemonの通信に適用されますが、コンテナ内部のプロセスへ自動的に環境変数を注入するものではありません。イメージの取得は成功したのに、Dockerfileの RUN apt-get updatenpm install が失敗する場合、ビルド段階へのプロキシ設定が不足しています。

DockerfileではARGを使ってビルド時だけプロキシを渡し、最終イメージに認証情報や不要な設定を残さない構成が基本です。以下はHTTP_PROXYとHTTPS_PROXYをビルド引数として受け取る最小例です。

FROM debian:12-slim

ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY

RUN apt-get update \
    && apt-get install -y --no-install-recommends ca-certificates curl \
    && rm -rf /var/lib/apt/lists/*

ビルド時には次のように指定します。

docker build \
  --build-arg HTTP_PROXY=http://192.168.1.20:7890 \
  --build-arg HTTPS_PROXY=http://192.168.1.20:7890 \
  --build-arg NO_PROXY=localhost,127.0.0.1,registry.local \
  -t docker-proxy-test:local .

社内の認証付きプロキシURLやサブスクリプショントークンをDockerfileへ直書きしてはいけません。BuildKitのsecret機能やCIの保護された変数を使い、ビルドキャッシュやレイヤー履歴に秘密情報が残らないようにします。Clash自体に認証を設定していない場合でも、共有ネットワーク上でポート7890を無制限に公開しないことが前提です。

Composeで起動後の通信を設定する

起動後のアプリケーションにプロキシを利用させる場合は、Composeのenvironmentへ値を渡します。LinuxのDockerブリッジでホスト側ゲートウェイを固定したい場合は、ホストゲートウェイのアドレスを追加します。Docker Desktopでは host.docker.internal が利用できる環境がありますが、Linuxでは明示的な追加設定が必要になる場合があります。

services:
  worker:
    image: alpine:3.20
    environment:
      HTTP_PROXY: http://host.docker.internal:7890
      HTTPS_PROXY: http://host.docker.internal:7890
      NO_PROXY: localhost,127.0.0.1,worker,db,registry.local
    extra_hosts:
      - "host.docker.internal:host-gateway"
    command: ["sh", "-c", "wget -S -O- https://example.com"]

すべてのアプリケーションがHTTP_PROXYを尊重するわけではありません。Go、Python、Node.jsなどは対応しやすい一方、独自のTLS実装を持つツールやUDPを使うクライアントは別の設定が必要です。Dockerのプロキシ設定とコンテナのプロキシ設定を一括して考えず、どのプロセスがどの環境変数を読むかを確認してください。

Docker向けルール、DNS、TUNをどう選ぶか

Dockerのregistry通信は、通常HTTPSの443番ポートを使います。Clashのルールではregistryのドメインを明示的にプロキシグループへ送り、内部registryやDockerネットワークはDIRECTにする構成が分かりやすいです。例として外部のイメージ配布先をまとめて扱う場合は、実際に利用するregistryのドメインを正確に記述してください。短いキーワードの DOMAIN-KEYWORD は関係のないドメインまで巻き込む恐れがあるため、最初から多用しない方が安全です。

rules:
  - DOMAIN,registry-1.docker.io,PROXY
  - DOMAIN-SUFFIX,docker.io,PROXY
  - DOMAIN-SUFFIX,internal.example,DIRECT
  - IP-CIDR,10.0.0.0/8,DIRECT,no-resolve
  - IP-CIDR,172.16.0.0/12,DIRECT,no-resolve
  - IP-CIDR,192.168.0.0/16,DIRECT,no-resolve
  - MATCH,DIRECT

上のドメインは構成例です。実際のDocker Hub取得ではmanifest、認証、レイヤー配布が異なるホスト名へ分かれることがあります。1つのドメインだけを許可しても、認証エラーやlayer取得エラーが残る場合は、Clashの接続ログに出た実際の宛先を確認し、必要なドメインをルールへ追加します。

DNSについては、ホストの名前解決が成功しているからといってコンテナ内も成功するとは限りません。Dockerは通常、コンテナへ組み込みDNSのアドレスを渡し、そのDNSがホスト側の設定を中継します。TUNモードを併用する場合はDNS hijackやfake-ipの対象範囲を確認し、Dockerブリッジの予約アドレスを誤って外部プロキシへ送らないようにします。まずHTTPプロキシでTCP通信を安定させ、必要性が確認できた段階でTUNを追加する順番が安全です。

DockerだけならTUNを必須にしない

Docker EngineとコンテナがHTTPプロキシを正しく利用できる環境では、透過的な全体取り込みを行うTUNは必須ではありません。TUNはプロキシ環境変数を無視する通信やUDP通信まで取り込みたい場合に検討します。HTTPプロキシとTUNを同時に設定すると経路が二重になり、ループや判定の分かりにくさを招くため、段階的に有効化してください。

取得エラーを層ごとに切り分ける

Dockerのエラーメッセージは、失敗した層を正確に示さないことがあります。まずClashのログに対象ドメインが現れているかを確認し、次にホスト、Engine、コンテナの順で同じ宛先への通信を比較します。

  1. ホストから curl -x http://127.0.0.1:7890 https://example.com -I を実行し、Clashのプロキシポート自体が応答するか確認します。
  2. Clashの接続ログで、Docker registryや認証用ドメインが表示されるか確認します。表示されなければ、Docker Engineがプロキシを使っていないか、NO_PROXYに入っている可能性があります。
  3. docker info でdaemonのプロキシ情報を確認し、設定変更後にEngineを再起動したか確認します。
  4. docker pull alpine:3.20 のような小さなイメージで再現し、大きなイメージや複数stageのビルドと分離します。
  5. ビルドだけ失敗する場合は、docker build のARGとDockerfileのARG宣言を確認します。pullとRUNは別経路として扱います。
  6. コンテナ起動後だけ失敗する場合は、コンテナ内で env | grep -i proxygetent hosts example.comwget または curl を順番に実行します。
症状可能性の高い原因確認ポイント
proxyconnect tcp timeoutClashポートへ到達できない待受アドレス、ホストファイアウォール、ゲートウェイ
no such hostDocker側のDNS解決失敗resolv.conf、Clash DNS、コンテナのDNS設定
certificate signed by unknown authorityHTTPS検査やCA不足ClashのTLS設定、コンテナのca-certificates
unauthorizedregistry認証情報または経路の問題docker login、認証ホストのログ、時刻
pullは成功するがRUNが失敗BuildKitへプロキシが渡っていないbuild-arg、DockerfileのARG、builder設定
起動後のAPI通信だけ失敗コンテナ環境変数が不足HTTP_PROXY、HTTPS_PROXY、NO_PROXY

よくある設定ミス

  • ポートを間違える。Clashの外部コントローラー9090をプロキシポートとして指定しても、通常のHTTP通信は処理されません。7890などのHTTPまたはmixedポートを使います。
  • ホストの127.0.0.1をコンテナから使う。コンテナのループバックはコンテナ自身です。ホストゲートウェイを指定し、必要なら extra_hosts を追加します。
  • Clashのallow-lanだけをオンにする。OSファイアウォールがポートを遮断していれば接続できません。Listen状態とファイアウォールの両方を確認します。
  • NO_PROXYを広くしすぎる。NO_PROXY=* はすべての通信を直接接続にするため、プロキシが必要なregistryまでClashを迂回します。
  • 証明書エラーを無視する。Dockerやコンテナ側のCA証明書を無効化して回避するのは安全ではありません。まず時刻、CAパッケージ、TLS経路を確認します。

CIランナーで再現性を保つ設定

CIランナーは、手元のホストで動くClashをそのまま共有できるとは限りません。セルフホストランナーならランナーのネットワークからClashの待受ポートへ到達できるか確認できますが、クラウド上の一時RunnerではローカルClashへ接続できません。その場合はCI環境が提供するHTTPプロキシ、専用の出口ノード、またはネットワーク側のegress設定を使います。

CIでDocker-in-Dockerを使う場合、ジョブコンテナとDocker daemonが同じネットワーク名前空間にいるとは限りません。HTTP_PROXY をジョブへ設定しただけでは、別コンテナのdockerdに届かないことがあります。daemon側の環境変数、BuildKitのbuilder設定、ジョブ内のパッケージマネージャ設定を分けて記述し、ログに秘密情報が出ないようマスクを有効にしてください。

また、CIではClashのルールを毎回変更するより、必要なregistryとパッケージ配布先を固定し、失敗時のログを保存する方が保守しやすくなります。キャッシュを有効にすれば取得回数を減らせますが、キャッシュはプロキシ設定の代替ではありません。初回取得、キャッシュミス、認証トークン更新の3ケースをテストしておくと、ネットワーク変更の影響を発見しやすくなります。

最終確認:三つの通信を別々に検証する

設定を終えたら、イメージ取得、ビルド、起動後通信を別々に確認します。最初から大きな本番イメージで試すのではなく、軽量な検証用イメージと単純なDockerfileを使うと、問題の場所を短時間で特定できます。

  1. docker pull alpine:3.20 を実行し、Clashのログにregistry関連の接続が記録されることを確認します。
  2. プロキシ用のARGを渡して小さなDockerfileをビルドし、RUN wget やパッケージ取得が成功することを確認します。
  3. Composeでコンテナを起動し、コンテナ内のプロキシ環境変数、DNS解決、HTTPSアクセスを確認します。
  4. 内部registryやLANサービスが必要な場合は、NO_PROXYとClashのDIRECTルールが期待通りに働くことを確認します。
  5. Clashのログをinfoへ戻し、認証トークン、サブスクリプションURL、内部ホスト名などの機密情報が共有ログに残っていないことを確認します。

DockerをClash経由で安定させる要点は、ホストのシステムプロキシをオンにすることではありません。Docker Engine、BuildKit、コンテナ、CIランナーという通信主体を分離し、それぞれが到達できるClashのアドレスとポートを明示することです。まずHTTPまたはmixedポートでTCP通信を確立し、ルールとNO_PROXYを整えたうえで、必要な場合だけDNS制御やTUNへ進むと、経路が複雑にならず再現性の高い開発環境を構築できます。

Clashクライアントをダウンロード

利用するプラットフォームに合ったクライアントを選び、インストール後に本記事の手順でDocker Engineとコンテナのプロキシ経路を設定してください。

Clashをダウンロード