VSCode の Remote - Containers や GitHub Codespaces のような Dev Container 環境で Docker を使う方法について解説します。
以前にも似たような記事を書きましたが、色々と内容に不足があったので改めて書き直すことにしました。
docker-from-docker と docker-in-docker
Dev Container のような Docker コンテナ内で Docker を使用する方法には、docker-from-docker と呼ばれる方式か docker-in-docker と呼ばれる方式の 2 つがあります。
両者の違いについて理解した上で要求にあっている方式を選択しましょう。
docker-from-docker
docker-outside-of-docker とも呼ばれます。
これは Docker コンテナにホストの Docker ソケットをバインドマウントすることにより、コンテナ内からホストの Docker を使用するというアプローチになります。
この方式を使う場合、Dev Container 内で作成されるコンテナは Dev Container の「兄弟」コンテナとなります。
この方式の特徴は次の通りです。
- bind mount を行う場合、マウントするディレクトリはホスト側のパスで指定する必要がある
- ホストの Docker を使用しているため、Dev Container 内のパスは使えません
- Dev Container 内からホスト側のパスを取得するために工夫が必要となります
- Dev Container から作成したコンテナに対して
localhost
でアクセスすることはできない- 作成したコンテナのポートはあくまでもホストに対して公開されるため、Dev Container にとっての
localhost
ではありません - Dev Container から作成したコンテナにアクセスするためには何らかの工夫が必要となります
- 作成したコンテナのポートはあくまでもホストに対して公開されるため、Dev Container にとっての
- コンテナやイメージ、ボリュームなどのリソースをホストと共有できる
- ホスト側で build や pull したイメージを DevCotainer 内でも利用できます
- Dev Container で build や pull したイメージをホスト側でも利用できます
docker-in-docker
Docker コンテナ内でホストとは異なる独自の Docker デーモンを持つ方法です。
この方式を使う場合、Dev Container 内で作成されるコンテナは Dev Container の純粋な「子」コンテナとなります。
この方式の特徴は次の通りです。
- ホストから Docker を使うときと同じ感覚で Docker を使うことができる
- bind mount に Dev Container 内のパスを使うことができます
- 作成したコンテナに
localhost
でアクセスすることができます
- Dev Container を
--privileged
オプションつきで実行する必要がある- Dev Container がホストのすべてのデバイスにアクセスする特権を得ることになるため、相応のリスクが伴います
- イメージやボリュームなどのリソースは独自にコンテナ内で管理される
- コンテナやイメージ、ボリュームなどのリソースは独自にコンテナ内で管理される
- ホストと共有することはできません
docker-from-docker で Docker を使う
導入方法
主に 3 つの方法があります。
-
Dev Container 定義 を使う
- Dev Container を作成する際に Docker from Docker を選択する方法です
- 開発に使う言語などは後から自分で Dockerfile をカスタマイズして追加することになります
- Dev Container を docker-compose で動かしたい場合は Docker from Docker Compose を使うことができます
-
Development Container Scripts を使う
- Development Container Scripts とは Dockerfile の中で呼び出すことで様々な言語やツールのセットアップを行えるスクリプトです
- Docker-from-Docker Install Script が提供されています
- Dev container features (preview) を使う
この記事では最もお手軽な 3 の方法のみ詳しく紹介します。
2022-11-25 追記: Dev container features は preview ではなくなっていますが、Dev Container 仕様のオープン化に伴い名称とリポジトリが変更されています。今後は こちら を参照してください(以下の情報は古くなっています)。
Dev container features については次の記事でも紹介しているので参照してください。
新規に Dev Container を作成する場合は、Select additional features to install
で表示される Docker (Moby) support, reuse host Docker Engine (Docker-from-Docker)
にチェックを入れれば OK です。
既存の Dev Container に Docker を導入したい場合は .devcontainer/devcontainer.json
に次の設定を追加します。
{
// ...
"features": {
"docker-from-docker": {
"version": "latest"
}
}
}
使用する docker-compose のバージョンを指定することもできます。
{
// ...
"features": {
"docker-from-docker": {
"version": "latest",
"dockerDashComposeVersion": "v2"
}
}
}
ゾンビプロセス防止のため、使用する Dev Container のベースイメージに代替の init
システムが含まれていない場合は --init
オプションをつけて Dev Container を起動するようにします。
{
// ...
"runArgs": [
// ...
"--init"
]
}
Dev Container を docker-compose で動かす場合は次のようになります。
services:
app:
# ...
init: true
Dev Container 内で bind mount を使う
前述の通り、docker-from-docker で bind mount を使う場合はマウントするディレクトリをホストのパスで指定する必要があります。
例えば、次のようなコマンドでは正しく bind mount できません。
$ docker run --rm -it -v $(pwd):/app golang:1.18 /bin/bash
ホスト側のパスを取得するためには .devcontainer/devcontainer.json
で次のような環境変数を定義します。
{
// ...
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
}
}
この環境変数により Workspace のホスト側のパスを参照することができます。
つまり、先ほどの例を正しく動作させるためには以下のようにします。
$ docker run --rm -it -v ${LOCAL_WORKSPACE_FOLDER}:/app golang:1.18 /bin/bash
docker-from-docker の場合、Workspace 外のディレクトリを bind mount することはできません。
Dev Container 内で docker-compose の bind mount を使う
docker-compose でも bind mount を使う場合はホスト側のパスを指定してあげる必要があります。
次の設定は Dev Container 内では動作しません。
services:
app:
image: ruby:2.7
volumes:
- .:/app
working_dir: /app
command: bundle exec rackup
次のように書くことで動作します。
services:
app:
image: ruby:2.7
volumes:
- ${LOCAL_WORKSPACE_FOLDER}:/app
working_dir: /app
command: bundle exec rackup
ただ、上記のような書き方をしてしまうと Dev Container の外では動かすことができません。
それでは不便なので Dev Container 用の設定は別ファイルに分けてあげると良さそうです。
services:
app:
volumes:
- ${LOCAL_WORKSPACE_FOLDER}:/app
$ docker compose up -f compose.yaml -f compose.devcontainer.yaml
Dev Container 内で作成したコンテナにアクセスする
前述の通り、docker-from-docker の場合次のようなコマンドでコンテナを作成しても localhost:8080
でアクセスすることができません。
$ docker run --rm -it -p 8080:80 nginx
$ curl localhost:8080
curl: (7) Failed to connect to localhost port 8080: Connection refused
これは作成したコンテナがポートを公開しているのはホストであるためです。
兄弟コンテナである Dev Container 側からは localhost
ではアクセスできません。
アクセスするには次のような工夫が必要です。
ホスト経由でアクセスする
Docker for Windows や Docker for Mac では、ホストの API アドレスを指す特殊なドメイン host.docker.internal
をコンテナ内で使用することができます。
これを使用してホスト経由でアクセスすることができます。
$ curl host.docker.internal:8080
ただし、Docker for Linux では host.docker.internal
がサポートされていません。
代わりに次のようにして host.docker.internal
を使えるようにすることが可能です。
{
// ...
"runArgs": [
// ...
"--add-host=host.docker.internal:host-gateway"
]
}
Dev Container を docker-compose で動かす場合は次のようになります。
services:
app:
# ...
extra_hosts:
- host.docker.internal:host-gateway
サービスディスカバリを利用する
Dev Container と作成するコンテナを同一ブリッジネットワークに所属させることで、サービスディスカバリを利用してアクセスすることができます。
これを行うには予めユーザー定義ブリッジネットワークを作成しておきます。
$ docker network create dev
次のようにして Dev Container を作成したネットワークに接続するようにします。
{
"runArgs": [
// ...
"--net=dev"
]
}
コンテナを作成する際に同一ネットワークを指定します。
コンテナ間通信となるためポートの公開は不要です。
$ docker run --rm -it --name web --net dev nginx
コンテナ名を使ってアクセスできます。
$ curl web
Dev Container を docker-compose で動かす場合は docker-compose 側で作成されるネットワークをそのまま利用できます。
$ docker run --rm -it --name web --net workspace_devcontainer_default nginx
$ curl web
docker-in-docker で Docker を使う
導入方法
docker-from-docker
と同様に、主に 3 つの方法があります。
-
Dev Container 定義 を使う
- Dev Container を作成する際に Docker in Docker を選択する方法です
-
Development Container Scripts を使う
- Docker-in-Docker Install Script が提供されています
- Dev container features (preview) を使う
最もお手軽な 3 の方法のみ詳しく紹介します。
2022-11-25 追記: Dev container features は preview ではなくなっていますが、Dev Container 仕様のオープン化に伴い名称とリポジトリが変更されています。今後は こちら を参照してください(以下の情報は古くなっています)。
新規に Dev Container を作成する場合は、Select additional features to install
で表示される Docker (Moby) support (Docker-in-Docker)
にチェックを入れれば OK です。
既存の Dev Container に Docker を導入したい場合は .devcontainer/devcontainer.json
に次の設定を追加します。
{
// ...
"features": {
"docker-in-docker": {
"version": "latest"
}
}
}
使用する docker-compose のバージョンを指定することもできます。
{
// ...
"features": {
"docker-in-docker": {
"version": "latest",
"dockerDashComposeVersion": "v2"
}
}
}
ゾンビプロセス防止のため、使用する Dev Container のベースイメージに代替の init
システムが含まれていない場合は --init
オプションをつけて Dev Container を起動するようにします。
{
// ...
"runArgs": [
// ...
"--init"
]
}
Dev Container を docker-compose で動かす場合は次のようになります。
services:
app:
# ...
init: true