Dockerって環境を汚さないで使えて便利ですよね。
vscodeっていろんな言語の開発環境を手軽に作れて便利ですよね。
ということでRemote Developmentによる作業コンテナ(devcontainer)を使ってコンテナ側に引き篭って開発できる環境を作ってみたので、環境構成と要点をここに書き出してみようと思います。
注意事項
この記事は
- ホストOSがWindows環境で
- vscodeとDockerコンテナを使って
- なるべくUnix系に寄せて使えるようにする
前提での環境構築のお話になっています。
ホストOSにLinuxやMacを使っていたりコンテナを使わなかったりホストはWindows上のシェル操作で十分な場合は、この記事は適合しない内容が多分に含まれています。
また、個々のツールや環境のセットアップ操作などはほぼ書かれない予定なので、必要に応じて他の記事を参照してください。
環境
- ホスト
- Windows 10
- WSL
- WSL2
- Ubuntu 20.04
- Docker
- Docker version 20.10.17, build 100c701
- Docker Compose version v2.10.2
- devcontainer
- Debian bullseye
私の場合はGoの開発環境としてdevcontainerを作ったのでDebianベース(golang:1.19-bullseye
)になっていますが、他のディストリビューションでも同じことはできると思います
TL;DR;
- ワークスペースはサービスのコンテナとは独立してdevcontainer上に構築する
- サービス用のコンテナとdevcontainerを同じネットワークにつなぐ
- DockerはDooDで操作し、ワークスペースの配置はWSL上とDocker上でパスを合わせる
経緯とか変遷の流れ
vscode + Remote Developmentを扱い始めた際、既存のコンテナにアタッチしてそこにvscodeのワークスペースを構築して使っていました。
ワークスペースのサービスコンテナからの分離
サービスコンテナ上にワークスペースを構築すると、
- ワークスペースに追加のツールなどをインストールするためには手動で行う必要がある
- DockerFileに書いてしまうとコミット時に差分を除く必要が出てしまう
- 全作業者が環境構成を共有するならありだけど普通はそうじゃない(と思っている)
- 別ボリュームでマウントすれば使い回せるけどdocker-compose.ymlに書いてしまうと以下略
- DockerFileに書いてしまうとコミット時に差分を除く必要が出てしまう
- サービスコンテナを破棄する度にツールの再インストールが必要になる
- コンテナは中身がよくわからなくて困ったら手軽に再作成できるのが売りの一つ
- 別ボリュームやホストに退避したりで使い回せるようにしたこともあったけど、たまに回収し忘れると再度入れ直したりするのが手間
というデメリットがありました。
この点を解消するために、ワークスペースを稼働中のサービスコンテナから切り離してホスト側で構築し直すようにしました。
この構成では通常のファイル操作などはvscodeからWindows上で行うことになりますが、コンテナにも同じファイルをマウントすることで変更が反映されるので問題なく作業できますし、vscode上で解決できない操作はサービスコンテナに対して docker exec
などで直接操作もできます。
ワークスペースをWindows => devcontainerへ
Windowsにワークスペースを配置することで、
- Docker関連のコマンドや周辺のCUIツールはWindows用でそろえる必要がある
- ワークスペースのシェルはPowerShellかbatになる
- bashとかzshが動かない(git bashは挙動がやや残念、、)
- コンテナ内ではUnix系シェルなので操作やスクリプトが統一できない
という別の課題が出てきました。
この点を解消するためには、ワークスペースをUnix系に配置する必要があります。
vscodeではRemote Developmentを使うとワークスペースをDockerコンテナ(devcontainer)上に構築できます。
この際、デフォルトではvscodeのプロジェクト(.devcontainerの一つ上のディレクトリ)がワークスペースとしてdevcontainer上にマウントされるので、あえてコンテナ内に作業用のファイルを配置する必要がなくほぼ透過的にコンテナ内でファイルを操作できます。
Remote Development経由でdevcontainerのワークスペースをvscodeで開いた場合、gitの認証情報はホストでの情報を引き継いで利用できるようです。
$ git config --global -l | grep credential
credential.helper=!f() { /home/vscode/.vscode-server/bin/e7f30e38c5a4efafeec8ad52861eb772a9ee4dfb/node /tmp/vscode-remote-containers-3cf86db779288bfea4d1bb570de933a68c3055de.js $*; }; f
仕組みとしてはおそらく上記のところで、認証処理をRemote Containersの拡張機能に含まれるスクリプトに紐付けてホストの認証情報に問い合わせてるのだと思われます。
ちなみに私の環境ではpushの際に以下のエラーが出ていますが、特にpushが失敗するわけでもないので今のところは手当していません。
$ git push
<3>init: (1388) ERROR: UtilConnectUnix:467: connect failed 111
<3>init: (1391) ERROR: UtilConnectUnix:467: connect failed 111
Enumerating objects: 7, done.
Counting objects: 100% (7/7), done.
Delta compression using up to 8 threads
Compressing objects: 100% (4/4), done.
Writing objects: 100% (6/6), 1.04 KiB | 1.04 MiB/s, done.
Total 6 (delta 1), reused 0 (delta 0), pack-reused 0
remote: Resolving deltas: 100% (1/1), completed with 1 local object.
To https://github.com/aaaaaaa/bbbbbb
04f2bcc..7749211 develop -> develop
devcontainerとサービスコンテナを同じネットワークで接続する
さて、ワークスペースをUnix系に押し込めることに成功しましたが、このままだとdevcontainerからサービスコンテナに対して通信することができません。
例えばmysqlのコンテナを立てた時にdevcontainer上から直接データをのぞいたり、ユニットテストがDBを触ることがあってもdevcontainer上で直接テストを行ったりしたいですよね?
devcontainerとサービスコンテナを通信できるようにするためにはdocker用のネットワークを作成し、それぞれのコンテナをネットワークでつなげてやる必要があります。
$ docker network create local-nw
サービスコンテナの起動時にオプションでネットワークを指定することで、コンテナをネットワークに接続できます。
$ docker run -d --name mysql --network=local-nw mysql:8.0 mysqld
docker-composeを使う場合はexternal属性を指定することで作成済のネットワークを指定できます。
services:
mysql:
image: mysql:8.0
container_name: mysql
networks: local
command: mysqld
networks:
local:
name: local-nw
external: true
docker-compose内で作成したネットワークにdevcontainerを接続することも可能です。
この場合はdocker-composeのプロジェクトに連動してネットワークが作成されるため、事前に docker network create
をしておく必要がありません。
networks:
local
// ネットワーク名はdocker-composeのプロジェクト名 + 内部で指定したネットワーク名になる
{
"runArgs": [ "--networks=hoge_local" ]
}
ただしこの構成の場合は
- devcontainer起動中に
docker-compose down
するとネットワーク削除がエラーになる- ネットワークが消えないだけで特に実害はなさそうだが、、
-
docker-compose up
でネットワークを作成してからでないとdevcontainerの起動ができない
などの問題があるため、 個人的にはあまりお勧めしません。
ただし、チーム開発などの場合は docker-compose up
の前に docker network create
が必要になるというのが、初期構築の際の負荷になる場合もあります。
上記のようなデメリットがあることを理解した上であれば作業負荷を低減するために、devcontainerをdocker-compose上のネットワークに接続する構成というのも選択肢の一つにはなりうることもあります。
ホスト側のワークスペースの配置パスをWindows => WSLへ
pythonやrubyなどスクリプトがそのまま動く言語ではdevcontainerのワークスペースと同じ階層をサービスコンテナにもマウントすることで編集内容をそのまま動作させることができますが、GoやTypeScriptなどのコンパイルやトランスパイルが必要な言語ではソースの編集後にもう一処理追加してやる必要があります。
そしてソースを修正後のコンパイルは手動で実行するのではなく、Goの場合はair、TypeScriptの場合はdev-serverなどを用いていわゆるホットリロードによって対処することが多いと思います。
Unix系環境ではホットリロードの仕組みをinotifyのファイルイベントをトリガーに動作する仕組みが多いですが、ここで問題になるのがWindowsからコンテナにマウントしたファイル群は編集してもinotifyのイベント通知が飛んできません。
Windowsの制限と言うよりはWSL2の上drvfsを利用した際の制限。
Docker Desktopを用いてWindows側のファイルをコンテナにマウントする場合、一度WSL上でdrvfsを用いて /mnt/C
配下に配置されるので、Docker側ではそのファイルパスをベースにマウントすることができます。
WSL1 + drvfsではinotifyの通知をサポートしていたようですが、WSL2では通知を受け取ることはありません。
この点を解消するためには、ワークスペースのファイルをWindows側のファイルパスに配置しない、つまりWSL側で /mnt
以外のパスに配置するようにします。
具体的にはWindowsからは \\wsl$\[distribution name]
でWSLのディストリビューション配下のファイルパスにアクセスできるのでそこを経由してファイルを配置するか、WSLにログインして以下でファイルを移動することで実現します。
# 権限によっては失敗するのでcpのほうがよいかも
$ mv /mnt/C/Users/[user]/some-project /home/[user]/some-project
このとき、WSL側でログインされるユーザーが操作できるパスにワークスペースのファイル群を配置する必要があります。
devcontainerからdockerを操作できるようにする
WSLからサービスコンテナにファイルをマウントすることで、inotifyの通知を受け取ることができるようになりました。
これで大体の作業はdevcontainer内で完結できるようになったのですが、今のところではコンテナのログを見たり作り直したりするのにはホストに切り替えて操作を行う必要があります。
そのため、Dockerの操作についてもdevcontainer内で行えるようにしていきます。
Dockerクライアントのインストール
まず、dockerコマンドとdocker-composeコマンドをdevcontainer内で利用できるようにクライアントをインストールします。
devcontainerのOSとしてDebian以外を選択した場合はgpgの周りを適宜調整してください。
(Ubuntuで構築したなら debian
となっている部分を ubuntu
と変えれば動きます)
# dockerクライアントとdocker-composeをインストール
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y && apt autoremove -y \
&& apt install -y docker-ce-cli docker-compose-plugin
# aptで取得されるdocker-composeは古いので新しいバージョンに置き換える
ARG DCVERSION="2.10.2"
ENV DCURL="https://github.com/docker/compose/releases/download/v${DCVERSION}/docker-compose-linux-x86_64"
RUN curl -L ${DCURL} -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
Dockerクライアントの通信先をホストのDockerデーモンに向ける
Dockerのデーモンが動いているのはdevcontainer内でなくWSL環境です。
そのためDockerのクライアントが操作する先のデーモンをWSLに向けます。
Dockerのクライアントから外部のデーモンに通信するためにはデーモン側でTCP接続を許可してDOCKER_HOST環境変数を指定するのがオーソドックスですが、Dockerデーモン自体が稼働させているコンテナからホストのデーモンを操作する場合に限りDocker outside of Docker(DooD)という仕組みを使って簡易に対応できます。
"mounts": [
"type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock"
],
DooDについて調べると
docker run -v /var/run/docker:/var/run/docker ...
のように省略記法で説明されている場合がありますが、devcontainerで使う場合は mounts
要素は --mount
オプションに相当するもので省略記法は使えないので注意してください。
これとは別にdocker-compose.yamlを用意してマウント定義をする場合は、volumesで定義することになるので省略記法での指定もできるようです。
services:
app:
volumes:
- /var/run/docker:/var/run/docker
WSL上のワークスペースの配置をコンテナ内とそろうように移動する
ここまででdevcontainer内からの docker-compose ps
や docker-compose logs mysql
のコマンドは動くようになりますが、docker-compose down
とした後に docker-compose up (-d)
とするとコンテナが起動できないことがあり、起動してもコンテナ内の処理で失敗することがあります。
ここで失敗するのはホストから 相対パスで指定したパス をマウントしている場合で、ワークスペースのパスがWSL上とdevcontainer上でずれているとdevcontainer上のdockerコマンドからはマウントすることができなくなります。
(ここを後で強調して説明するために、前の手順ではあえて失敗するパスにワークスペースを配置していました)
この点を解消するためには、WSL上でのワークスペースのファイルをコンテナにマウントされた時と同じ配置になるように移動します。
devcontainerではデフォルトで /workspaces/[プロジェクト名]
でワークスペースをマウントするので、WSL上でも同じように配置します。
$ mv /home/[user]/some-project /workspaces/some-project
もしWSL上でこのパスをすでに利用していた場合は、別のパスに配置した上でマウント先を変更する必要があります。
$ mv /home/[user]/some-project /dev-workspace/some-project
"workspaceMount": "src=/dev-workspace/some-project,target=/dev-workspace/some-project,type=bind",
"workspaceFolder": "/dev-workspace/some-project"
最終的な設定ファイル
これらを踏まえてdevcontainerの環境を作った結果、以下のようになりました。
ARG GO_VERISION="1.19-bullseye"
# [Choice] Go version (use -bullseye variants on local arm64/Apple Silicon): 1, 1.19, 1.18, 1-bullseye, 1.19-bullseye, 1.18-bullseye, 1-buster, 1.19-buster, 1.18-buster
FROM mcr.microsoft.com/vscode/devcontainers/go:0-${GO_VERISION}
# [Choice] Node.js version: none, lts/*, 18, 16, 14
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Uncomment this section to install additional OS packages.
RUN apt update -y \
&& export DEBIAN_FRONTEND=noninteractive \
&& apt autoremove -y \
&& apt update -y \
&& apt -y install --no-install-recommends \
curl \
git \
gnupg
# Docker
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& apt update -y && apt autoremove -y \
&& apt install -y docker-ce-cli docker-compose-plugin
# docker-composeは古いので指定したバージョンに更新
ARG DCVERSION="2.10.2"
ENV DCURL="https://github.com/docker/compose/releases/download/v${DCVERSION}/docker-compose-linux-x86_64"
RUN curl -L ${DCURL} -o /usr/local/bin/docker-compose && chmod +x /usr/local/bin/docker-compose
USER vscode
{
"name": "Go",
"build": {
"dockerfile": "Dockerfile",
"args": {
"GO_VERISION": "1.19-bullseye",
"NODE_VERSION": "18"
}
},
"runArgs": [
"--cap-add=SYS_PTRACE",
"--security-opt",
"seccomp=unconfined",
"--network=local-nw"
],
"mounts": [
"type=bind,src=/var/run/docker.sock,dst=/var/run/docker.sock"
],
"remoteUser": "vscode"
}
終わりに
今までは同じワークスペースでも操作に応じてvscodeを複数開くと言ったことをやっていましたが、これでdevcontainerの起動だけをホスト側で行えばその他のことはdevcontainer内で完結できるようになりました。
まだ作ったばかりで使用感は完全には測れていませんが、vscodeのプロセスが減らせてマシンが軽くなったのとコンテキストスイッチが減って集中が続くようになっただけでも、この構成にした甲斐はあるかなと感じています。
もっとこう作った方がいいとかご意見ありましたらコメントいただければと思います。