🐳

WSL2上のDockerをWindows上のVSCodeから操作する

2024/05/05に公開

目的

以前に WindowsでもサクサクDocker (Docker on WSL2 without Docker Desktop) という記事を公開していますが、WSL上のDockerを使ってVSCodeで開発する際の環境を構築します。

元になるWSL上でDocker環境を構築する手法や、その環境でGPUを使えるようにする方法に関しては、下記記事を参照してください。

https://zenn.dev/rhene/articles/docker-on-wsl2-without-docker-desktop
https://zenn.dev/rhene/articles/docker-on-wsl2-with-gpu

内容

WSL上のDockerを使って開発を行うためにはまずWSLにアクセスし、WSL上でリポジトリを展開したうえでコンテナを起動するのが通常の手順です。
ただ、WSLはあくまでサンドボックスとして scrap and build を繰り返したいですし、Windowsのファイルシステムとは別にWSL上のファイルを管理するのは非常に面倒です。
できればWindowsのファイルシステムでWSL上のDockerを使って開発を行いたいところです。

本記事ではWindows上で実行中のVSCodeから、WSL上のDockerを操作するための環境を構築します。

本記事の構成は、以下になります。

  1. DevContainers 拡張機能
  2. Docker 拡張機能
    1. Docker CLIのインストール
      1. Docker CLIのインストール
      2. Docker Composeのインストール
    2. WSL上のDocker Engineへのアクセス設定
      1. リモート(WSL側)の設定
      2. ホスト(Windows側)の設定
  3. 制限事項

方法

DevContainers 拡張機能

DevContainers 拡張機能はVSCodeからDevContainerを起動し、起動されたコンテナ内で開発を行う非常に便利な機能です。

Dev Containers Extension

https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers

Windowsで実行中のVSCodeからWSL上のDockerでDevContainerを実行するための設定は非常に簡単です。
@ext:ms-vscode-remote.remote-containersExecute in WSL をチェックするだけで、ホストPC(Windows)からDevContainerの起動時にはWSL上のDockerが使われるようになります。
また、必要に応じて Execute in WSLDistro を変更し、Docker Engineが起動しているディストリビューションを指定することが可能です。(未設定の場合はデフォルトのディストリビューション(通常はUbuntu)が使われます)

devcontainer extension setting

これらの設定を行うことでDevContainer利用時には、VSCodeは内部的に指定されたディストリビューションに接続したうえで、コンテナの起動を試みるようです。
コンテナの起動に成功すると、VSCodeは起動したコンテナにアタッチし、そのコンテナ内で開発を行うことができるようになります。

なお、こちらの設定は環境に依存するため、複数の環境をでVSCodeを使っている場合は、歯車アイコンから同期設定を外すことをオススメします。

また、ワークスペースオープン時などに表示される「フォルダーに開発コンテナーの構成ファイルが含まれています。…」の通知画面にある「コンテナーで再度開く」ボタンからコンテナを起動しようとすると下記のエラーが発生します。
(WSL内でWindowsのPathを使おうとしてエラーになっているっぽい)

エラー内容
[6810 ms] Start: Run in Host: wslpath -w c:/workspace
[6988 ms] Command failed: ls -a c:/workspace
[6988 ms] ls: cannot access 'c:/workspace': No such file or directory
[6988 ms] Exit code 2

こちらについては、通知ボタンは利用せず、VSCodeの画面の左下にあるリモートメニューから「コンテナーで再度開く」を実行するか、コマンドパレット(Ctrl+shift+P)から「Dev Containers: Reopen in Container」を実行すれば、正常にDevContainerが起動します。

Docker 拡張機能

続いてDocker 拡張機能の設定を行います。
Docker 拡張機能は、VSCodeからDockerコマンドを使ってコンテナの操作を行うための拡張機能です。
GUIでイメージやコンテナの管理を行えるため、CLIでの操作が苦手な方には非常に便利な拡張機能です。

Docker Extension

https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker

Docker 拡張機能に関しては DevContainers 拡張機能とは異なり、いくつかの事前準備が必要です。

まずは動作を確認します。
何も設定されていない状態でDocker 拡張機能を使おうとすると、以下のようなエラーが表示されます。

docker not installed

Dockerコマンドがインストールされていないため、このような表示になります。

また、Dockerコマンドがインストールされていたとしても、Docker Engine自体はWSL上で実行されているため、必要な情報を取得することができない状態のはずです。
WSL上のDocker Engineの情報を取得するには、ホスト(Windows)からリモート(WSL)にアクセスして情報を取得できるようにする必要があります。

Docker CLIのインストール

まずはWindowsにDockerコマンドをインストールします。
Dockerコマンド自体は公式サイトでビルド済みのバイナリが公開されているため、それをダウンロードしてパスを通すだけで使用できます。
公開先は下記になります。
https://download.docker.com/win/static/stable/x86_64/

上記のサイトより最新版のZIPファイをダウンロードし、適当なフォルダ(例えば c:/tools/docker )に解凍することで、Dockerコマンドを使用できるようになります。
このままだとフルパスで docker.exe を指定する必要がありますので、
環境変数 PATH に解凍した docker.exe を含むフォルダのパスを追加しておくことをオススメします。

続いてDocker Composeコマンドをインストールします。
Docker Composeコマンドは公式GitHubのReleasesページからダウンロードできます。
https://github.com/docker/compose/releases/

最新版のリリースページの Assets からWindows版である docker-compose-windows-x86_64.exe を取得し docker-compose.exe にリネームして、docker.exe と同じフォルダに保存しておきましょう。
Assets の中にWindows版が見当たらない場合、 Show all ** assets をクリックすると表示されます。
また docker-compose.exe%USERPROFILE%\.docker\cli-plugins に保存しておけば docker compose コマンドとして利用できるようになります。

なお、これらのコマンドのインストールと、コマンドパスを設定を行うための PowerShell スクリプトを用意しました。
このスクリプトでは c:\tools\docker フォルダ内に Docker コマンドと Docker Compose コマンドの最新版をインストールし、環境変数 PATH にコマンドパスを追加する処理を行います。(インストール済みの場合は上書き)

docker_cli_install.ps1
# インストール先フォルダ ($DEST_DIR/docker にインストールする)
# $DEST_DIR="$env:ProgramFiles" # ← ProgramFilesにインストールする場合は管理者権限が必要
$DEST_DIR="c:\tools"
$DOCKER_DIR=Join-Path $DEST_DIR "docker"

##############################
# docker.exe のインストール
##############################
# dockerの最新バージョンのファイル名を取得
$LATEST_VERSION_ZIP = (Invoke-WebRequest -UseBasicParsing -uri "https://download.docker.com/win/static/stable/x86_64/").Content.split("`r`n") | 
                 Where-Object { $_ -like "<a href=""docker-*"">docker-*" } | 
                 ForEach-Object { $zipName = $_.Split('"')[1]; [Version]($zipName.SubString(7,$zipName.Length-11).Split('-')[0]) } | 
                 Sort-Object | Select-Object -Last 1 | ForEach-Object { "docker-$_.zip" }
# dockerの最新版(ZIPファイル)をダウンロードして $DEST_DIR に展開
curl.exe -L "https://download.docker.com/win/static/stable/x86_64/$LATEST_VERSION_ZIP" -o $LATEST_VERSION_ZIP
Expand-Archive $LATEST_VERSION_ZIP -DestinationPath $DEST_DIR -Force
# ダウンロードしたZIPファイルを削除
Remove-Item -Force $LATEST_VERSION_ZIP


##############################
# docker-compose.exe のインストール
##############################
# docker-composeの最新バージョンを取得
$LATEST_VERSION=(curl.exe -s "https://api.github.com/repos/docker/compose/releases/latest" | ConvertFrom-Json).tag_name
# docker-composeの最新版をダウンロードして docker コマンドと同じパスに保存
curl.exe -L "https://github.com/docker/compose/releases/download/$LATEST_VERSION/docker-compose-windows-x86_64.exe" -o "$DOCKER_DIR\docker-compose.exe"
# cli-plugins にインストール
mkdir $env:USERPROFILE\.docker\cli-plugins -Force
cp $DOCKER_DIR\docker-compose.exe $env:UserProfile\.docker\cli-plugins\


##############################
# docker.exe と docker-compose.exe のバージョン表示
##############################
pushd $DOCKER_DIR
./docker.exe --version
./docker-compose version
popd


##############################
# 環境変数 Path (ユーザ設定) に $DEST_DIR\docker フォルダを設定
##############################
if (Test-Path -Path $DOCKER_DIR) {
    # 既存のPath設定を取得し $DOCKER_DIR の設定状況を確認
    $user_path= [System.Environment]::GetEnvironmentVariable("Path", "User")
    if (";$user_path;" -notlike "*;$DOCKER_DIR;*") {
        # 未設定の場合は既存の設定に追加する (永続設定)
        [Environment]::SetEnvironmentVariable("Path", "$user_path;$DOCKER_DIR", "User")
        Write-Host "Docker コマンドの Path を登録しました"
    }
    else
    {
        Write-Host "Docker コマンドの Path は登録済みです"
    }
}

WSL上のDocker Engineへのアクセス設定

Docker コマンドのインストールができたら、dockerコマンドのバージョン情報を確認してみましょう。
Windows上のターミナルで以下のコマンドを実行してください。

Dockerのバージョン確認 (Windows)
# パスが通っていない場合はフルパスで指定する (例: c:\tools\docker\docker.exe version)
docker version
バージョン確認結果
Client:
 Version:           26.1.0
 API version:       1.45
 Go version:        go1.21.9
 Git commit:        9714adc
 Built:             Mon Apr 22 17:07:39 2024
 OS/Arch:           windows/amd64
 Context:           default
error during connect: this error may indicate that the docker daemon is not running: Get "http://%2F%2F.%2Fpipe%2Fdocker_engine/v1.45/version": open //./pipe/docker_engine: The system cannot find the file specified.

Clientの情報は取得できていますが、サーバ(Docker Engine)の情報が取得できていません。
引き続き、WSL上のDocker Engineにアクセスするための設定を行います。

まずはWSL上のDockerの設定を行い、Windowsから TCP 接続でWSL上のDocker Engineへ接続できるようにします。

ここからはWSL上での作業になります。

まずはWSL上でDocker Engineの設定を行います。
WSLに接続したうえで /etc/docker/daemon.json を編集して、tcp プロトコルでリッスンするように設定します。

WSL上のDocker Engineの設定 (WSL)
sudo vi /etc/docker/daemon.json

/etc/docker/daemon.json に以下の内容を追記して保存します。

/etc/docker/daemon.json 設定内容
{
  "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"]
}

nvidia containerをインストール済みの場合は、すでに設定が記載されていますので、hosts セクションを追記すると以下のようになります。

/etc/docker/daemon.json 設定内容 (nvidia container設定済みの場合)
{
    "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"],
    "runtimes": {
        "nvidia": {
            "args": [],
            "path": "nvidia-container-runtime"
        }
    }
}

以上で Deocker Engine の設定は完了ですが、このままですとサービス起動時にエラーになるため、サービス設定を変更します。
具体的には /lib/systemd/system/docker.serviceExecStart セクションを以下のように書き換えます。

Dockerサービスの起動設定 (WSL)
sudo vi /lib/systemd/system/docker.service
/lib/systemd/system/docker.service 設定変更
# ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
ExecStart=
ExecStart=/usr/bin/dockerd

1行目は既存の設定なので、コメントアウトしています。(削除可能)
2行目と3行目が新たに追加した設定です。

設定が完了したら、Docker Engineを再起動します。

Docker Engineの再起動 (WSL)
# サービス設定の再読み込み
sudo systemctl daemon-reload
# Docker Engineの再起動
sudo systemctl restart docker

Docker Engine が再起動したらサービスの状態を確認して、正常に起動しているかを確認してください。

Docker Engineの状態確認 (WSL)
# Docker Engineの状態確認
sudo systemctl status docker

エラーがなく正常に起動しているようでしたら、dockerコマンドからTCPで接続できるか確認します。

TCP接続確認 (WSL)
docker -H tcp://127.0.0.1:2375 version
TCP接続確認結果
Client: Docker Engine - Community
 Version:           26.1.0
 API version:       1.45
 Go version:        go1.21.9
 Git commit:        9714adc
 Built:             Mon Apr 22 17:06:41 2024
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          26.1.0
  API version:      1.45 (minimum version 1.24)
  Go version:       go1.21.9
  Git commit:       c8af8eb
  Built:            Mon Apr 22 17:06:41 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.31
  GitCommit:        e377cd56a71523140ca6ae87e30244719194a521
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

エラーがなければ正常に接続できています。

続いてWindowsからWSL上のDocker Engineにアクセスするたの設定を行います。

以降の操作はWindows上のターミナルで実行してください。

まずはWSL上で行ったように、WindowsからTCP接続でWSL上のDocker Engineにアクセスできるか確認します。

WindowsからのTCP接続確認 (Windows)
docker -H tcp://127.0.0.1:2375 version
TCP接続確認結果
Client:
 Version:           26.1.0
 API version:       1.45
 Go version:        go1.21.9
 Git commit:        9714adc
 Built:             Mon Apr 22 17:07:39 2024
 OS/Arch:           windows/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          26.1.0
  API version:      1.45 (minimum version 1.24)
  Go version:       go1.21.9
  Git commit:       c8af8eb
  Built:            Mon Apr 22 17:06:41 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.31
  GitCommit:        e377cd56a71523140ca6ae87e30244719194a521
 runc:
  Version:          1.1.12
  GitCommit:        v1.1.12-0-g51d5e94
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

先程はエラーで出てこなかった、Serverの情報が表示されていれば正常に接続できています。
また、 OS/Arch の部分に注目してください。
Client側のDockerコマンドはWindowsバイナリが使われているのため windows/amd64 表記になっているのに対して、Server側はWSL上Docker Engineの情報を表示しているため、 linux/amd64 表記になっているはずです。

なお、動作確認のために docker version コマンドを実行していますが、他のコマンドも実行できるはずです。
正常に動作していればイメージの一覧やコンテナの一覧が表示されるはずです。

WindowsからのDockerコマンドの実行 (Windows)
# イメージ一覧
docker -H tcp://127.0.0.1:2375 images
# コンテナ一覧
docker -H tcp://127.0.0.1:2375 container ls

ここまでくれば、WindowsからWSL上のDockerを操作することができるようになりました。
ただ、いちいち -H オプションを使って接続先のホストを指定するのは面倒なため、Docker Context を変更して接続設定を省略できるようにします。
具体的には以下のコマンドを実行します。

WindowsのDocker Context設定 (Windows)
# WSL上のDocker Engineに接続するためのContextを作成 (Context名は任意 (例: `wsl-tcp`)
docker context create wsl-tcp --docker host=tcp://127.0.0.1:2375

# 作成したContextをデフォルトで使うように設定
docker context use wsl-tcp

上記の設定で、 -H オプションを使わずにWSL上のDocker Engineにアクセスできるようになります。

動作確認
docker images
docker container ls
docker version

上記コマンドで正しくWSL上の情報が表示されていれば、VSCodeの Docker 拡張機能からも同じことができるようになっているはずです。
先ほど作成した Docker Context もこちらで確認することができます。

access WSL docker engine

制限事項

ここまでいろいろな手順を踏んで、Windows上のVSCodeからWSL上のDocker Engineを操作する環境を構築しましたが、以下のような制約があります。

Windows上からボリュームをマウントできない

Windows上から docker run コマンドや docker-compose コマンドを使ってコンテナを起動する場合に、ファイルまたはディレクトリをマウントに失敗して、コンテナが起動しないことがあります。
これは、WindowsのファイルシステムとWSLのファイルシステムでパスの扱いが異なるためです。

例えばWindows上の C:\workspace フォルダをマウントする場合、WSL上では /mnt/c/workspace というパスになります。
このため workspace ファルダ内の target というファイルをマウントする場合、WSL上でのパスである /mnt/c/workspace/target を指定する必要があります。
ちゃんとマウントできるじゃないかと思うかもしれませんが、通常はマウントするパスはフルパスでは書かず、相対パスで書くと思います。
例えば docker run -v ./target:/workspace/target ... と記載したりしますが、この時 docker コマンドは内部的に絶対パスに変換して処理をしており、なおかつそのパスはWindowsのパス C:\workspace\target になります。
当然、Linuxで稼働しているWSL上のDocker EngineはWindowsのパスを認識できないためエラーになり、コンテナの起動に失敗します。

同じ理由で docker コマンドや docker-compose コマンドにおいて、 --file または -f オプションで Dockerfiledocker-compose.yml を指定する場合も同様の問題が発生します。

なお、この相対パスによる問題は、DevContainers 拡張機能を使ってコンテナを起動する場合には発生しません。
これは DevContainers 拡張機能の設定で Execute in WSL を有効にしている場合、VSCode が内部的に WSL にアクセスしたうえでコンテナを起動しているため、すべてWSLの中で処理が行われるからです。

Docker 拡張機能から起動中の DevContainer のコンテナグループの操作ができない

上記で Dev Containers 拡張機能を使ってコンテナを起動する方法を推奨していますが、この場合も問題がないわけではありません。
DevContainer において docker-compose.yml などを使って複数のコンテナを起動する場合があります。
このとき作成されるコンテナグループをVSCodeのDocker 拡張機能経由で docker compose downdocker compose logs などで操作しようとすると、エラーで処理が実行できません。
この現象は、例えば .devcontainer/docker-compose.yml で定義されたコンテナグループを操作する場合、Docker 拡張機能は内部的に -f オプションを用いてこのファイルを指定してコンテナグループを操作しようとしますが、DevContainers 拡張機能で実行されたコンテナはWSL上のコマンドで起動されているため、ファイル情報としてWSLのフルパスで渡されることになります。
しかし、Windows上のdocker-composeコマンドはこのパスを認識できずにエラーになります。

こちらの問題は有効な回避方法がありません。
Docker拡張機能にも Execute in WSL オプションがあると便利なのですが、現状は存在しません。
手動で docker-compose コマンドの -f オプションにWindows上のパスを指定してコマンドを実行するか、WSL上でコンテナグループを操作するしかないようです。
ただしこの状態でも、Docker 拡張機能経由でコンテナ一つ一つを個別に操作することが可能ですので、そこまで致命的でもありません。

docker run で直接コマンド実行時に結果が表示されない

Windows上で docker run コマンドを使ってコンテナを起動しコンテナ内で直接コマンドを実行する場合、コマンドの結果が表示されないことがあります。
具体的には下記コマンドを実行しても、結果が表示されません。 (WSL上で実行する場合は正常に表示されます)

結果が表示されない例
docker run --rm alpine echo "Hello, World!"

docker run --rm hello-world

本来ならば1つ目のコマンド実行時には Hello, World! と表示されるはずですが、何も表示されません。
また、2つ目のコマンドはDokcerの動作確認に使われるDockerイメージですが、このイメージのように Dockerfile 内の CMD 文などでコマンドが実行されている場合も同様に結果が表示されません。

この問題に対しては、インタラクティブオプション -i をつけて実行すると正常に表示されます。

インタラクティブオプションでのコマンド実行
docker run --rm -i alpine echo "Hello, World!"
# ‐> Hello, World!

docker run --rm -i hello-world
# ‐> Hello from Docker!

# attach オプションでも可
docker run --rm -a stdin -a stdout alpine echo "Hello, World!"
docker run --rm -a stdin -a stdout hello-world

パフォーマンスの問題

本記事で作成した環境で開発を行うと、WSL上にマウントされたWindowsファイルを操作することになるため、パフォーマンスが低下すると言われています。
本来であればWSL上にリポジトリを展開して、すべての操作をWSL上で行えば、上記のようなファイルパスの問題やパフォーマンス低下は発生しませんが、利便性の問題から本記事のようにWindows上ですべての操作を行いたい場合もあると思います。

そもそもDockerコンテナにマウントしたファイルを操作する場合にもパフォーマンス低下が見込まれるため、処理時間にシビアな操作を行う場合、最終的には実機での動作確認が必要になります。
では、開発に支障が出るほどのパフォーマンスの低下が発生するのかというと、そこまで問題になったことはないです。
実際にこの環境で大容量の画像を数万枚処理するような操作を行っていますが、開発に支障が出るほどのパフォーマンスの低下は発生していません。

まとめ

このように、ファイルパスの扱いに少し制約があるものの、WSL上のDocker EngineをWindows上から操作する環境を構築できました。
WSLを使うことでWindowsでも気楽にDocker環境が利用できますし、DevContainerと組み合わせることで開発環境の構築も非常に簡単に行えます。
どんどんDockerを使って開発を効率化していきましょう!!

Discussion