VSCode devcontainer + Dockerでシームレスな開発環境を構築してみた

# はじめに
> 先日参加したハッカソンでDockerで環境構築してチームメンバーと開発をしたが、メンバーの１人がVSCodeの設定によるエラーで苦戦していた。そこで今回は、VSCode devcontainerとDockerを使って、Dockerのコンテナ内でVSCodeを起動して開発する方法についてまとめる。

前提知識：Docker / Docker-composeの概念をある程度理解している。
実行環境：Windows10, DockerHub, VSCode
到達レベル：面倒な環境構築をVScodeのエディタごとチームメンバーと共有できる

# VSCode devcontainerとは？
VSCode の拡張機能パックである **Dev Container**（旧 Remote-Containers）を使用して構築する開発環境のことを指す。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/631b8475-ff4a-d6a0-853a-4efceb8a0326.png)
引用元：<a href="https://code.visualstudio.com/docs/devcontainers/containers">Dev Container 公式ドキュメント</a>  

ローカルのワークスペースからファイルシステムをマウントするかコンテナにコピーまたは複製することでコンテナ上で作業することができる。また、どうやらVSCodeの拡張機能もコンテナにインストールして利用するみたい。つまり、コンテナごとでエディタを含めた開発環境全体をシームレスに切り替えることができる。  

これでチームメンバー間でのエディタの設定等によるエラーを防ぐことができる。
ただし、エディタはVSCodeに限られてしまうが......

# 利用手順
## Docker のインストール
詳細についてはここでは割愛します。
Docker Desktop のインストールは<a href="https://www.docker.com/products/docker-desktop/">ここから</a>

https://qiita.com/R_R/items/a09fab09ce9fa9e905c5

## Dev Container のインストール
VSCodeを起動して拡張機能検索から```Dev Container```を見つける。
![拡張機能.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/2aafde29-6f8e-bc8b-0641-daa3379d3c7a.png)
発見したら```Install```ボタンをクリック！

> ちなみに、```Dev Container```の下に```Remote Development```という似たような拡張機能があるが、これは**VSCode Remote Development**といい、Remote - WSLと、Remote - Containers、Remote - SSHをひとまとまりでインストールできる拡張機能パッケージのようなものらしい。

## devcontainerコンテナのビルド
```Dev Container```をインストールすると、左端に新しいステータスバー項目が表示される。
![ステータスバー.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/9ef8fb45-d5bf-1cd1-8ac9-a08e67c0ab52.png)

ステータスバーをクリックすると、以下のようにコンテナのメニュバーが表示される。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/c6ed477e-aaa3-9ad3-a669-9c5206cdbba7.png)
既に```.devcontainer```ディレクトリが存在するリポジトリがあれば、```Open Folder in Container...```を選択してコンテナを展開すればOK。  

ここでは、```Try a Dev Container Sample...```でサンプルの Node.js コンテナを展開する。
選択後は、コンテナがビルドされるのでしばらく待機...

ビルドが完了すると、左端のステータスバーにコンテナ名が表示される。
これでVSCodeとビルドしたコンテナとの接続は完了。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/20047f78-1115-240c-3f00-1e5c64f5c9aa.png)

コンテナのビルド後は、以下のようなディレクトリ構造になる（**Node.jsの場合**）
```
.
├── CODE_OF_CONDUCT.md
├── .devcontainer
├── .eslintrc.json
├── .git
├── .gitattributes
├── .gitignore
├── LICENSE
├── node_modules
├── package.json
├── README.md
├── SECURITY.md
├── server.js
├── .vscode
└── yarn.lock
```

## devcontainer.json
```devcontainer.json```は、開発コンテナの構築方法と起動方法を決定する構成ファイル。
```.devcontainer```ディレクトリ直下にある。
```json:devcontainer.json
{
	"name": "Node.js",
	"image": "mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye",

	"customizations": {
		"vscode": {
			"settings": {},
			"extensions": [
				"streetsidesoftware.code-spell-checker"
			]
		}
	},

	"portsAttributes": {
		"9000": {
			"label": "Hello Remote World",
			"onAutoForward": "notify"
		}
	},

	"postCreateCommand": "yarn install"
}
```  

|主なオプション|内容|
|:---:|:---:|
|```dockerfile```|Dockerイメージとして使用したいファイルへの相対パス|
|```customizations```|VSCodeなどのツール固有のプロパティを設定|
|```settings```|コンテナ/マシン固有の設定ファイルの追加|
|```extensions```|コンテナ作成時にインストールする拡張機能を指定|
|```forwardPorts```|コンテナ内のポートをローカルで利用できるようにする|
|```postCreateCommand```|コンテナ作成後に実行されるコマンド|
|```remoteUser```|コンテナ内でVSCodeを実行するユーザーを上書き|

※ ```remoteUser```のデフォルトは```containerUser```
全てのオプション集は<a href="https://containers.dev/implementors/json_reference/">こちら</a>

## Dockerfile と docker-compose.ymlの作成
特にDockerコンテナをカスタマイズしない人は作成する必要はないが、大抵は開発していく中で色々といじりたくなってくる。そこで.devcontainer ディレクトリ下に Dockerfileとdocker-compose.ymlを作成してコンテナを好きなようにカスタマイズする。

```Dev Container```のような Docker コンテナ内で Docker を使用する方法には、**docker-from-docker** という方式と **docker-in-docker** という方式の 2 つがあるらしい。

今回は、Docker コンテナにホストの Docker ソケットをバインドマウントすることにより、コンテナ内からホストの Docker を使用するため、前者の```docker-from-docker```の方式を採用する。

詳細は以下を参照。

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

### Dockerfile
```dockerfile:Dockerfile
FROM mcr.microsoft.com/devcontainers/javascript-node:0-18-bullseye
# 以下略
```

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

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile

    volumes:
      - ${LOCAL_WORKSPACE_FOLDER}:/app

    working_dir: /app
    command: sh -c "yarn install"
```
```${localWorkspaceFolder}```は```devcontainer.json```で使用できる変数で、ホスト側のフォルダのパスを指す。

詳細は以下を参照。

https://qiita.com/frozenbonito/items/776baeea9b26079a2add

---
### 追記[2022/12/31]：
docker-compose の ver.3.2以降はvolumesは```long syntax```で記述すると、良いそう。
```yaml:docker-compose.yml
version: "3.9"

# 略
volumes:
      - type: bind
        source: ${LOCAL_WORKSPACE_FOLDER}
        target: /app
# 略
```
```short syntax```の場合、
- 意図せずroot権限でホストにディレクトが自動生成される。
- ファイルをマウントしようとしたが、ディレクトリがマウントされる。
- ```docker-compose up```でエラーを吐く場合が度々ある。
などのデメリットがあるみたい。

詳細は以下を参照。

https://zenn.dev/daifukuninja/articles/5f0f4c99418a36

---


### devcontainer.json
```json:devcontainer.json
{
    "name": "Node.js",
    "dockerComposeFile": "docker-compose.yml",
    "service": "app",
	"remoteEnv": {
		"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
	},
    "extensions": [
        "streetsidesoftware.code-spell-checker"
    ]
}
```








## ローカルサーバの起動
ひと通り環境が整ったのでサーバを立ち上げてみる。
```
$ node server.js
```
上記のコマンドでローカルサーバを起動。
```javascript:server.js
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';

const express = require('express');

// Constants
const PORT = 3000;
const HOST = '0.0.0.0';

// App
const app = express();
app.get('/', (req, res) => {
	res.send('Hello remote world!\n');
});

app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
```
ルートディレクトリ直下に```server.js```があるので、「Hello remote world!」と返されれば成功。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/f46f139e-89a5-fe7c-8a8b-8a4e83552ed0.png)

## コンテナのリモート接続を閉じる
```File```->```Close Remote Connection```でローカルのVSCodeに戻すことができる。

![接続終了.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/7c777bb9-6503-5d53-2e57-9aa0d558fc26.png)


# 参考文献
https://code.visualstudio.com/docs/devcontainers/tutorial

https://qiita.com/d0ne1s/items/d2649801c6f804019db7

https://qiita.com/frozenbonito/items/776baeea9b26079a2add

# まとめ
今回は、```VSCode devcontainer```を使って、Dockerコンテナ上でシームレスな開発環境を構築してみた。ファイルシステムだけでなく、開発環境全体を揃えられることで、チーム開発で不必要なエラーで悩まされることは無くなりそう。ただ、メンバー全員がVSCode縛りになってしまうのが少し痛い......