VSCode DevContainer の中で Docker を使う方法

__2022/07/31: 内容を充実させて書き直したものを投稿しました。そちらをご参照ください。__

https://qiita.com/frozenbonito/items/b1de3980ee0553fb1c2f

## DevContainer の作成

まずはコマンドパレットで `> Remote-Containers: Add Development Container Configuration Files...` を実行し、DevContainer の設定ファイルを作成します。
この時、テンプレートを選べるので `Docker from Docker` (DevContainer を docker-compose で動かす場合は `Docker from Docker Compose`) を選択します。

これでもう `> Remote-Containers: Reopen in Container` を実行すればコンテナ内で Docker を使うことができます。

追加で何か言語などをインストールしたい場合は `.devcontainer/Dockerfile` を編集してベースイメージを変えてしまうのが簡単です。Debian または Ubuntu ベースのイメージであれば何でも使うことができます。
以下は Go 言語の公式 DevContainer イメージをベースにする例です。

```dockerfile:.devcontainer/Dockerfile
# Note: You can use any Debian/Ubuntu based image you want. 
FROM mcr.microsoft.com/vscode/devcontainers/go:1.15

# 以下略
```

**2021/10/15 追記:**
新機能の Dev container features (preview) を利用することでもセットアップ可能です。
こちらは特に作成済みの DevContainer で Docker CLI を使いたい場合に便利です。
紹介記事を書いたのでこちらも参照してみてください。

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

## DevContainer 内で Docker の bind mount を使う

DevContainer 内で bind mount を使うときは注意が必要です。
以下のように何も考えずにマウントすると、コンテナ内にマウントしたはずのファイルが見つかりません。

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

これは DevContainer 内の Docker はホストで動作している Docker を共有していることに起因します。
そのため、bind mount を行う際は DevContainer 内のパスではなくホストのパスを指定してあげる必要があります。

この問題を解決するために、`.devcontainer/devcontainer.json` にはデフォルトで以下のような環境変数が定義されています。

```json:.devcontainer/devcontainer.json
{
	// ...

	// Use this environment variable if you need to bind mount your local source code into a new container.
	"remoteEnv": {
		"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
	},

	// ...
}
```

`${localWorkspaceFolder}` は `devcontainer.json` で使用できる変数で、開いているフォルダのホスト側のパスを意味します。
この設定により、DevContainer 内では環境変数 `LOCAL_WORKSPACE_FOLDER` からホストのパスを参照することができます。

つまり、先ほどの例を正しく動作させるためには以下のようにします。

```console
$ docker run --rm -it -v $LOCAL_WORKSPACE_FOLDER:/app golang:1.15 /bin/bash
```

## DevContainer 内で docker-compose の bind mount を使う

docker-compose の場合も同様に bind mount を使う場合はホスト側のパスを指定してあげる必要があります。

以下の設定は DevContainer 内では動作しません。

```yaml:docker-compose.yml
version: "3"

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

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

```yaml:docker-compose.yml
version: "3"

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

ただ、上記のような書き方をしてしまうと DevContainer の外では動かすことができません。
それでは不便なので DevContainer 用の設定は以下のように `docker-compose.override.yml` [^1] 等に書いてあげると良さそうです。

[^1]: docker-compose がデフォルトで読みこむオプショナルな設定ファイルです。`docker-compose.yml` 
 の設定を上書きします。詳しくは[こちら](https://docs.docker.com/compose/extends/)を参照してください。

```yaml:docker-compose.override.yml
services:
  app:
    volumes:
      - ${LOCAL_WORKSPACE_FOLDER}:/app
```

### ホスト OS が Windows の場合

ホスト OS が Windows の場合、上記のように `${LOCAL_WORKSPACE_FOLDER}` を使用した設定でもエラーが出てしまいます。

```console
$ docker-compose up
ERROR: Named volume "c:\Users\frozenbonito\dev\docker-from-docker:/app" is used in service "app" but no declaration was found in the volumes section.
```

これはホスト OS が Windows の場合、`${localWorkspaceFolder}` の値が Windows 形式のパスになり、docker-compose がそれを誤って Volume 名と認識してしまうためです。

解決方法は2つあります。

#### volumes の long syntax を使う

以下のように volumes 設定の [long syntax](https://docs.docker.com/compose/compose-file/#long-syntax-3) を使ってあげることで、docker-compose に Windows のパスを正しく解釈させることができます。

```yaml:docker-compose.override.yml
services:
  app:
    volumes:
      - type: bind
        source: ${LOCAL_WORKSPACE_FOLDER}
        target: /app
```

#### 環境変数 `COMPOSE_FORCE_WINDOWS_HOST` を設定する

環境変数 [`COMPOSE_FORCE_WINDOWS_HOST`](https://docs.docker.com/compose/reference/envvars/#compose_force_windows_host) に `true` または `1` が設定されていると、docker-compose が DevContainer のような Linux 環境で動作していたとしても [short syntax](https://docs.docker.com/compose/compose-file/#short-syntax-1) 中の HOST パスを Windows のパスとして解釈するようになります。

以下のように `devcontainer.json` で設定してあげると良いでしょう。

```json:.devcontainer/devcontainer.json
{
	// ...

	"remoteEnv": {
		"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}",
		"COMPOSE_FORCE_WINDOWS_HOST": "true"
	},

	// ...
}
```