開発と AI · Clash 技術ブログ

VS Code Dev Containers で Clash を使用する方法

Dev Container のビルド、コンテナー内の Git、拡張機能のダウンロードは、同じプロキシを使うとは限りません。失敗がどの層で起きているかを先に特定し、その層へホストのアドレスと NO_PROXY を渡します。

  • VS Code
  • Dev Containers
  • Docker
  • プロキシ
目次

まずタイムアウトが Docker、ビルド、起動済みコンテナのどこで発生しているかを特定する

VS Code Dev Containers から Clash を利用する設定は、プロキシアドレスを一つ入力するだけでは完了しません。ベースイメージの取得、Dockerfile の実行、コンテナ内の Git、リモート拡張機能のダウンロードは、それぞれ異なるプロセスから開始されます。

「Dev Containers」の出力から失敗したコマンドの段階を特定し、その処理を行うプロセスへプロキシを渡します。そうしなければ containerEnv をどれほど詳しく記述しても、まだ作成されていないコンテナの問題は修正できません。

四つのネットワーク箇所

失敗した操作実際にリクエストを送るコンポーネント設定箇所
docker pull / ベースイメージの取得Docker daemonDocker Desktop または daemon のプロキシ
Dockerfile RUN apt/npmイメージのビルド段階build args または BuildKit の設定
コンテナ内の curl/Git/pip実行中のコンテナcontainerEnv、remoteEnv、またはツールの設定
VS Code リモート拡張機能のダウンロードDev Containers のリモートサービスコンテナ環境と各拡張機能によるネットワークリクエスト

エラーが「Starting Dev Container」より前に出る場合、コンテナがまだ実行されていないため、通常は .devcontainer/devcontainer.json の containerEnv では修正できません。まず失敗したコマンドと、VS Code の出力パネルに表示された段階を記録してください。

Dev Containers における四段階のネットワーク
  1. VS Code のホスト拡張機能の取得と Docker の起動を担当する
  2. Docker のビルド段階build args または Docker のプロキシ設定を読み込む
  3. 実行中のコンテナcontainerEnv を読み込む
  4. リモート拡張機能とターミナルremoteEnv またはツール固有の設定を読み込む

失敗した段階にだけプロキシを設定します。ホスト、ビルド段階、コンテナの実行環境を同じ環境変数として扱わないでください。

コンテナ内の 127.0.0.1 はホストを指さない

Clash がホスト上で動作している場合、コンテナ内の http://127.0.0.1:7890 はコンテナ自身を指します。Windows と macOS の Docker Desktop では host.docker.internal を利用できます。Linux Docker では host-gateway を使い、同じ名前を割り当てられます。

Clash がホストのループバックアドレスだけを待ち受けている場合、コンテナから接続できないことがあります。ローカルネットワークからの接続を許可するか、mixed-port をコンテナから到達できるインターフェースで待ち受ける場合は、同時にシステムのファイアウォールで接続元を制限してください。7890 をインターネットへ直接公開してはいけません。

まずコンテナからポートを確認する。CLASH_PORT はクライアントの実際の mixed-port に合わせて変更する
CLASH_HOST=host.docker.internal
CLASH_PORT=7890
getent hosts "$CLASH_HOST" || true
curl -v -x "http://$CLASH_HOST:$CLASH_PORT" https://www.example.com/

# Linux Docker 临时测试
docker run --rm --add-host=host.docker.internal:host-gateway curlimages/curl:latest -v -x http://host.docker.internal:7890 https://www.example.com/

Linux ホストでは host-gateway を devcontainer.json に記述する

.devcontainer/devcontainer.json の抜粋
{
  "runArgs": [
    "--add-host=host.docker.internal:host-gateway"
  ]
}

変更後に Dev Containers: Rebuild Container を実行します。以前のコンテナには、新しい hosts マッピングが自動では反映されません。

Docker Compose プロジェクトでは、サービスの下に extra_hosts: ["host.docker.internal:host-gateway"] を設定します。二か所へ重複して追加しないでください。

ポートへ到達できることを確認してから、実行中のコンテナにプロキシを設定する

前の手順は、curl が HTTP レスポンスを受け取って初めて成功と判断できます。次に、同じアドレスをコンテナ内のターミナルと VS Code のリモートプロセスへ渡します。mixed-port が 7890 でない場合は、以下の三か所のポートをすべて変更してください。

プロジェクト単位の devcontainer.json の例
{
  "containerEnv": {
    "HTTP_PROXY": "http://host.docker.internal:7890",
    "HTTPS_PROXY": "http://host.docker.internal:7890",
    "NO_PROXY": "localhost,127.0.0.1,::1,host.docker.internal"
  },
  "remoteEnv": {
    "HTTP_PROXY": "${containerEnv:HTTP_PROXY}",
    "HTTPS_PROXY": "${containerEnv:HTTPS_PROXY}",
    "NO_PROXY": "${containerEnv:NO_PROXY}"
  }
}

HTTPS_PROXY が http:// で始まるのは通常正しい設定です。これは HTTP CONNECT プロキシ経由で HTTPS へアクセスすることを示し、プロキシポート自体を HTTPS に変えるわけではありません。NO_PROXY には Compose のサービス名、社内ネットワーク、ローカル開発用ドメインも追加してください。そうしないと、コンテナからデータベースや API へアクセスする通信まで Clash を経由することがあります。

保存後に Rebuild Container を実行し、新しいターミナルで env | grep -i proxy を実行します。起動済みのターミナルプロセスには、remoteEnv の変更が自動では反映されません。

イメージの取得と Dockerfile のビルドは個別に設定する

FROM イメージの取得は Docker daemon が行うため、Docker Desktop の Proxies 画面または daemon の設定で処理します。Dockerfile の RUN コマンドはビルド用コンテナ内で実行されるので、build.args から一時プロキシを渡せます。ただし、個人用のポートや認証情報を ENV としてイメージレイヤーに残さないでください。

devcontainer のビルド引数の抜粋。7890 は実際の mixed-port に合わせて変更する
{
  "build": {
    "dockerfile": "Dockerfile",
    "args": {
      "HTTP_PROXY": "http://host.docker.internal:7890",
      "HTTPS_PROXY": "http://host.docker.internal:7890",
      "NO_PROXY": "localhost,127.0.0.1"
    }
  }
}

curl の成功後も、Git、apt、拡張機能をそれぞれ一度テストする

コンテナのターミナルで HTTP_PROXY、HTTPS_PROXY、NO_PROXY が設定されていることを確認し、Git とパッケージマネージャーのコマンドを個別に実行します。通常 Git は環境変数を読み込みます。まだ以前のアドレスを参照する場合は、Git 固有の設定も確認してください。

リモート拡張機能は、コンテナ側の VS Code Server がダウンロードします。remoteEnv を変更した後は Rebuild Container を実行し、出力パネルで marketplace へのリクエストを確認してください。

コンテナ内で Git を確認する。REPO_URL はプロジェクト自身の公開リポジトリに置き換えられる
REPO_URL='https://github.com/octocat/Hello-World.git'
env | grep -i '_proxy'
git config --show-origin --get-regexp 'http.*proxy' || true
git ls-remote "$REPO_URL"

# 只有 Git 不读取环境变量时才临时写入
git config --global http.proxy http://host.docker.internal:7890
git config --global --unset-all http.proxy

curl は成功するが、Git がまだ以前のポートへ接続する

git config --show-origin --get-regexp 'http.*proxy' を実行し、ユーザー設定またはリポジトリに残った設定を削除します。

apt update が失敗する

実際のリポジトリドメイン、証明書、/etc/apt/apt.conf.d にある個別のプロキシ設定を確認します。

プロジェクトは実行できるが、リモート拡張機能をインストールできない

Dev Containers のログで marketplace へのリクエストを探し、リモートサービスが remoteEnv を継承していることを確認します。

コンテナはインターネットへ接続できるが、ローカルデータベースへの接続に失敗する

データベースのサービス名とプライベートネットワークの範囲を NO_PROXY に追加します。

Rebuild の段階でまだ失敗する

daemon によるイメージ取得または Dockerfile の build args へ戻り、実行中のコンテナにはこれ以上変更を加えません。

共有リポジトリに個人用プロキシを固定値で書き込まない

チームプロジェクトでは、devcontainer.json で ${localEnv:HTTP_PROXY} を使って開発者のローカル環境を読み込むか、実際のアドレスを含まない .env.example を用意できます。

README には、HTTP_PROXY を http://host.docker.internal:7890 のような完全なアドレスで記述することを明記します。コミット前に、プロキシの URL にユーザー名、パスワード、token が含まれていないことを確認してください。

検証後は、docker pull、Rebuild Container、コンテナ内の Git とパッケージマネージャー、ローカルサービスへのアクセスをそれぞれ完了できる必要があります。失敗した項目があれば、その段階のログを残し、四層のネットワークを再び「Dev Container がネットワークへ接続できない」という一つの問題にまとめないでください。

参考資料