Dev Container 環境で Docker を使う

VSCode の [Remote - Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) や [GitHub Codespaces](https://github.com/features/codespaces) のような Dev Container 環境で Docker を使う方法について解説します。

以前にも[似たような記事](https://qiita.com/frozenbonito/items/776baeea9b26079a2add)を書きましたが、色々と内容に不足があったので改めて書き直すことにしました。

## 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 から作成したコンテナにアクセスするためには何らかの工夫が必要となります
- コンテナやイメージ、ボリュームなどのリソースをホストと共有できる
  - ホスト側で 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 つの方法があります。

1. [Dev Container 定義](https://github.com/microsoft/vscode-dev-containers/tree/main/containers) を使う
   - Dev Container を作成する際に [Docker from Docker](https://github.com/microsoft/vscode-dev-containers/blob/main/containers/docker-from-docker/) を選択する方法です
   - 開発に使う言語などは後から自分で Dockerfile をカスタマイズして追加することになります
   - Dev Container を docker-compose で動かしたい場合は [Docker from Docker Compose](https://github.com/microsoft/vscode-dev-containers/tree/main/containers/docker-from-docker-compose) を使うことができます
2. [Development Container Scripts](https://github.com/microsoft/vscode-dev-containers/tree/main/script-library) を使う
   - Development Container Scripts とは Dockerfile の中で呼び出すことで様々な言語やツールのセットアップを行えるスクリプトです
   - [Docker-from-Docker Install Script](https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md) が提供されています
3. [Dev container features (preview)](https://code.visualstudio.com/docs/remote/containers#_dev-container-features-preview) を使う
   - [Docker-from-Docker Install Script の Feature](https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker.md#feature-use) が提供されています

この記事では最もお手軽な 3 の方法のみ詳しく紹介します。
**2022-11-25 追記: Dev container features は preview ではなくなっていますが、Dev Container 仕様のオープン化に伴い名称とリポジトリが変更されています。今後は [こちら](https://github.com/devcontainers/features/tree/main/src/docker-outside-of-docker) を参照してください(以下の情報は古くなっています)。**

Dev container features については次の記事でも紹介しているので参照してください。

https://qiita.com/frozenbonito/items/6d73418a7e6c2d3746e4

新規に Dev Container を作成する場合は、`Select additional features to install` で表示される `Docker (Moby) support, reuse host Docker Engine (Docker-from-Docker)` にチェックを入れれば OK です。

既存の Dev Container に Docker を導入したい場合は `.devcontainer/devcontainer.json` に次の設定を追加します。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "features": {
    "docker-from-docker": {
      "version": "latest"
    }
  }
}
```

使用する docker-compose のバージョンを指定することもできます。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "features": {
    "docker-from-docker": {
      "version": "latest",
      "dockerDashComposeVersion": "v2"
    }
  }
}
```

ゾンビプロセス防止のため、使用する Dev Container のベースイメージに代替の `init` システムが含まれていない場合は `--init` オプションをつけて Dev Container を起動するようにします。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "runArgs": [
    // ...
    "--init"
  ]
}
```

Dev Container を docker-compose で動かす場合は次のようになります。

```yaml:.devcontainer/compose.yaml
services:
  app:
    # ...
    init: true
```

### Dev Container 内で bind mount を使う

前述の通り、docker-from-docker で bind mount を使う場合はマウントするディレクトリをホストのパスで指定する必要があります。

例えば、次のようなコマンドでは正しく bind mount できません。

```console
$ docker run --rm -it -v $(pwd):/app golang:1.18 /bin/bash
```

ホスト側のパスを取得するためには `.devcontainer/devcontainer.json` で次のような環境変数を定義します。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "remoteEnv": {
    "LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
  }
}
```

この環境変数により Workspace のホスト側のパスを参照することができます。
つまり、先ほどの例を正しく動作させるためには以下のようにします。

```console
$ 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 内では動作しません。

```yaml:compose.yaml
services:
  app:
    image: ruby:2.7
    volumes:
      - .:/app
    working_dir: /app
    command: bundle exec rackup
```

次のように書くことで動作します。

```yaml:compose.yaml
services:
  app:
    image: ruby:2.7
    volumes:
      - ${LOCAL_WORKSPACE_FOLDER}:/app
    working_dir: /app
    command: bundle exec rackup
```

ただ、上記のような書き方をしてしまうと Dev Container の外では動かすことができません。
それでは不便なので Dev Container 用の設定は別ファイルに分けてあげると良さそうです。

```yaml:compose.devcontainer.yaml
services:
  app:
    volumes:
      - ${LOCAL_WORKSPACE_FOLDER}:/app
```

```console
$ docker compose up -f compose.yaml -f compose.devcontainer.yaml
```

### Dev Container 内で作成したコンテナにアクセスする

前述の通り、docker-from-docker の場合次のようなコマンドでコンテナを作成しても `localhost:8080` でアクセスすることができません。

```console
$ docker run --rm -it -p 8080:80 nginx
```

```console
$ 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` をコンテナ内で使用することができます。
これを使用してホスト経由でアクセスすることができます。

```console
$ curl host.docker.internal:8080
```

ただし、Docker for Linux では `host.docker.internal` がサポートされていません。
代わりに次のようにして `host.docker.internal` を使えるようにすることが可能です。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "runArgs": [
    // ...
    "--add-host=host.docker.internal:host-gateway"
  ]
}
```

Dev Container を docker-compose で動かす場合は次のようになります。

```yaml:.devcontainer/compose.yaml
services:
  app:
    # ...
    extra_hosts:
      - host.docker.internal:host-gateway
```

#### サービスディスカバリを利用する

Dev Container と作成するコンテナを同一ブリッジネットワークに所属させることで、サービスディスカバリを利用してアクセスすることができます。

これを行うには予めユーザー定義ブリッジネットワークを作成しておきます。

```console
$ docker network create dev
```

次のようにして Dev Container を作成したネットワークに接続するようにします。

```jsonc:.devcontainer/devcontainer.json
{
  "runArgs": [
    // ...
    "--net=dev"
  ]
}
```

コンテナを作成する際に同一ネットワークを指定します。
コンテナ間通信となるためポートの公開は不要です。

```console
$ docker run --rm -it --name web --net dev nginx
```

コンテナ名を使ってアクセスできます。

```console
$ curl web
```

Dev Container を docker-compose で動かす場合は docker-compose 側で作成されるネットワークをそのまま利用できます。

```console
$ docker run --rm -it --name web --net workspace_devcontainer_default nginx
```

```console
$ curl web
```

## docker-in-docker で Docker を使う

### 導入方法

`docker-from-docker` と同様に、主に 3 つの方法があります。

1. [Dev Container 定義](https://github.com/microsoft/vscode-dev-containers/tree/main/containers) を使う
   - Dev Container を作成する際に [Docker in Docker](https://github.com/microsoft/vscode-dev-containers/blob/main/containers/docker-in-docker/README.md) を選択する方法です
2. [Development Container Scripts](https://github.com/microsoft/vscode-dev-containers/tree/main/script-library) を使う
   - [Docker-in-Docker Install Script](https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md) が提供されています
3. [Dev container features (preview)](https://code.visualstudio.com/docs/remote/containers#_dev-container-features-preview) を使う
   - [Docker-in-Docker Install Script の Feature](https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/docker-in-docker.md#feature-use) が提供されています

最もお手軽な 3 の方法のみ詳しく紹介します。
**2022-11-25 追記: Dev container features は preview ではなくなっていますが、Dev Container 仕様のオープン化に伴い名称とリポジトリが変更されています。今後は [こちら](https://github.com/devcontainers/features/tree/main/src/docker-in-docker) を参照してください(以下の情報は古くなっています)。**

新規に Dev Container を作成する場合は、`Select additional features to install` で表示される `Docker (Moby) support (Docker-in-Docker)` にチェックを入れれば OK です。

既存の Dev Container に Docker を導入したい場合は `.devcontainer/devcontainer.json` に次の設定を追加します。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "features": {
    "docker-in-docker": {
      "version": "latest"
    }
  }
}
```

使用する docker-compose のバージョンを指定することもできます。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "features": {
    "docker-in-docker": {
      "version": "latest",
      "dockerDashComposeVersion": "v2"
    }
  }
}
```

ゾンビプロセス防止のため、使用する Dev Container のベースイメージに代替の `init` システムが含まれていない場合は `--init` オプションをつけて Dev Container を起動するようにします。

```jsonc:.devcontainer/devcontainer.json
{
  // ...
  "runArgs": [
    // ...
    "--init"
  ]
}
```

Dev Container を docker-compose で動かす場合は次のようになります。

```yaml:.devcontainer/compose.yaml
services:
  app:
    # ...
    init: true
```