チームで開発するならDev Containersで環境構築工程をスキップしませんか？

## 読み飛ばしてください

みなさまどうも。
限界派遣SESと言われて心が折れるnikawamikanです。

最近、学生さんと一緒になんやかんや開発することがあり、その中で使ってみてよかった技術の中に**Dev Cointaners**と言われる技術があります。
VSCode限定ではありますが、開発環境の差異を可能な限り埋めてくれるスゴイやつです。

さらに言えば新たにチームに参加するメンバーに開発環境の構築を逐一説明する必要もなくなるので、入れ替わりの激しい限界派遣SESにこそ使う技術です。

# 本題

前提として以下の環境はインストールされているものとします。

- Docker
- docker compose (WindowsやMacの場合DockerDesktopをインストールしているのでインストール不要のはずです）
- VSCode

OSは上記がインストールできるのであればわりとなんでもOKだと思います（例外はどこにでもあるので断言はしません）

## Dev Containersで環境構築を行うというのはどういうことか？

例えば私のgithubにある[discord Botのテンプレート](https://github.com/nikawamikan/bot-template)では[Pycord](https://pycord.dev)というdiscord.pyのフォークされたライブラリを利用して開発を行うテンプレートを定義しているのですが、これを使うことでDockerとVSCodeがインストールされた環境であれば特定のLinuxディストリビューションにPythonの指定バージョンのインストールやライブラリの指定バージョンのインストールなどがDockerの仮想環境上に構築されPythonに必要なVSCodeの拡張機能のインストールまでが自動で行われます。

つまり、すでに存在しているrepositoryから環境構築にかかる時間はせいぜい`git clone` + コンテナをビルドする時間程度で実際に行う作業はほとんどありません。

あとは数行のコードを記述すればBOTを実行することも可能ですし。第三者のコントリビューターが環境構築をする手間もかなり省けると言えます。

## Dev Containersで環境構築をしていく

実際に環境構築をしていきましょう。


VSCodeに[Dev Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers)をインストールします。
この拡張機能をインストールすることで前提となる[Remote - SSH](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-ssh)のインストールも行われるはずです。

次にDev Containerの設定フォルダを作成します。（この時のディレクトリは適宜プロジェクトごとに設定するなどしてください）
これはVSCode左下にある`リモート ウィンドウを開きます`と表示されているアイコンをクリックし、`開発コンテナー構成ファイルを追加`をクリックすることで対話式のプロンプトが開始されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/c570dfee-2047-682b-832f-b6a93d75e796.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/de8bda0f-ea4e-aa72-5f28-eb0e5813b583.png)

今回はサンプルとして`Node.js & TypeScript`の環境を構築していきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/ff44ca2b-cf77-1f14-89e8-1741e18aeeea.png)

Apple SiliconのMacでは一部のバージョンで[バグ](https://github.com/nodejs/docker-node/issues/1912)が存在しているようなので今回は`18-bullseys`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/690858ca-f968-ef27-3e70-5c22d7ac6cca.png)

追加機能については今回は必要ないのでそのままOKを押します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/d60f667c-4925-7887-d467-417f8c0135ee.png)

すると`.devcontainer`というフォルダに`devcontainer.json`というファイルが作成されています。
また右下にコンテナーで再度開くという通知が出てることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/83cbee4f-84cc-46b5-fbb0-914b4458cf00.png)

通知は消えてしまうと分からなくなるので、とりあえず見逃してしまったら左下のリモートウィンドウのアイコンをクリックし`コンテナーで再度開く`を選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/26561359-821b-97ac-934f-086e00395f62.png)

するとDockerのimageが展開され仮想環境にSSH接続された状態と同じ状態が再現されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/e53ca176-4a07-726f-6313-543099173e50.png)


これでひとまず、ローカル環境を汚さずにNodeの実行環境が用意できました。

## Next.jsプロジェクトの作成

このNode環境に対してNext.jsのプロジェクトを作成してみます。

手順は[一般的なNext.jsの構築](https://nextjs.org/docs/getting-started/installation)と変わりません。
`npx create-next-app@latest`のコマンドを実行し対話式のプロンプトに対し適宜入力をしてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/228380a4-f04e-fdde-70e3-0fdd7c2a05e0.png)

一度`yarn dev`コマンドを実行しサーバーを起動します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/7a100e20-5c6b-7708-fa80-0bd47e2d3388.png)

`http://localhost:3000`をブラウザで開くと実行されていることが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/2001e09e-92ed-77db-c8ad-49e807b28ad4.png)

これはRemote SSHの拡張機能により自動的にコンテナ内のPortとlocalhostのPortが紐付けられるためです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/1062ffd9-be69-9277-0783-3a6d0e88d29e.png)

これによりほぼローカルの環境と変わりなく作業ができることが確認できたかと思います。

## 拡張機能のインストール

このDev Containersを利用して開発環境を作成すると拡張機能が入っていない状態となります。
これはRemote SSHでサーバー機に接続した状態と一緒といえます。
つまり、コンテナ内に対してSSH接続を行い、VSCode Serverをインストールしている状態となっていると同じといえます。

そのため、拡張機能を新たにインストールする必要があります。

メンドクサイと思ったかもしれませんが、これは余計な拡張機能が入らずクリーンな状態で環境が分けられていると考えると喜ばしいとも言えます。
また、チーム内で共通の拡張機能を使って設定も共有することができます。

実際に拡張機能をインストールしていきます。

### 共通で利用する拡張機能のインストール

今回のプロジェクトではTailwind CSSを利用していくため、拡張機能として[Tailwind CSS IntelliSense](https://marketplace.visualstudio.com/items?itemName=bradlc.vscode-tailwindcss)をインストールします。
この時、ここにインストールしただけでは`devcontainer.json`に反映されないため、歯車アイコンから`devcontainer.jsonに追加`を選択し、`devcontainer.json`へ記述します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/f1c9eccf-a7db-5d30-1f96-0a029e773c7a.png)

すると`extensions`の項目が追加されていることが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/1fe87289-0e8a-1504-34bd-0ab372e1863e.png)

`devcontainer.json`など、`.devcontainer`配下にあるファイルはDockerの環境の設定ファイルなので、変更時は再ビルドするのが好ましいです。（拡張機能のインストールではすでにインストールされている状態だと思うので必要ありませんが）

リビルドは左下のリモート ウィンドウアイコンをクリックして`コンテナーのリビルド`を実行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/8b5e74c9-213e-69b8-c9b7-f662ece6fb59.png)

また、tailwindを利用する際は[一部で警告が発生する](https://qiita.com/P-man_Brown/items/bf05437afecde268ec15)ため、これを解消するために`.vscode`の`settings.json`に以下の記述を追記します。

```json
{
    "files.associations": {
      "*.css": "tailwindcss"
    }
}
```


### 個人でのみ利用する拡張機能のインストール

逆に個人でのみしか利用しない拡張機能を`devcontainer.json`に記述するのは好ましくありません。
なぜなら誰もが[Copilot](https://marketplace.visualstudio.com/items?itemName=GitHub.copilot)を使えるわけでもありませんし、`nyan cat`プラグインは複数の種類があり好みがあるからです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/a3e62597-4251-8825-db22-564f1fe5b04d.png)

しかし、これらを毎回インストールするのはメンドクサイですし、コンテナをリビルドする度に行う必要があるので、軽く賽の河原の石積みを思わせる苦行を感じることになります。

これを回避するために`Default Extentions`を設定します。
設定画面の検索欄に`devcontainer default`など入力すればすぐに出てきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/344ca2b4-e708-1e41-dc76-5a67ae917a15.png)

拡張機能の歯車アイコンから`拡張機能IDのコピー`をし、IDを貼り付けて追加します。
これにより、あなたの賽の河原に費やす時間を節約できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/90124e2c-0eb9-e5e9-9d90-16f668a73fa0.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/87c66d86-00d5-4cfc-70bd-ee826da52558.png)

ここまでで同じ環境構築複数人で行う不毛な状況を回避することができるようになりました。（DockerとVSCodeのインストールは結局する必要がありますが）

## Node.js特有の注意点など

これはNode.js環境特有の問題です。
ただし、docker composeを利用する方法なども掲載するため必要な場合はそれ以外でも参考にしてください。

この環境から開発をしようとすると以下の問題が発生します。

- サーバーの起動に時間がかかる
- ホットリロードができない

これらについて一つずつ解説します。

### サーバーの起動に時間がかかる

これは`node_modules`の保存先がローカルにあり、それをDockerのコンテナに対してbindする形になっているのが原因です。
そのため、`node_modules`のフォルダをDockerのVolumeを利用して管理するように変更します。


先ほど作った`.devcontainer`に対して`compose.yaml`を作成し以下のようにします。

```
.devcontainer
  ┣━ compose.yaml
  ┗━ devcontainer.json
```

この`compose.yaml`では`devcontainer.json`に記述されているimageを利用し、volumesの記述などをします。
具体的には以下のようなコードになります。

```yaml
volumes:
  node_modules:

services:
  main:
    # Docker imageの指定
    image: mcr.microsoft.com/devcontainers/typescript-node:1-18-bullseye
    volumes:
      # ローカルのワークスペースをコンテナのワークスペースにマウント
      - ..:/workspace/app
      # volumeのnode_modulesをコンテナのnode_modulesにマウント
      - node_modules:/workspace/app/node_modules
    # ttyとstdin_openをtrueにするのはbuild時を失敗しないため（ないとすぐ閉じちゃう)
    tty: true
    stdin_open: true
```

次にこれを利用してDev Containerを構築するために`.devcontainer`ファイルを編集します。
このjsonファイルはコメントが許容されているため必要であればコメントを記述するとわかりやすくなります。

```json-doc
{
	"name": "my project",
	// docker composeのファイル名
	"dockerComposeFile": "compose.yaml",
	// アタッチを行うdocker composeのサービス名
	"service": "main",
	// workspaceをバインドするディレクトリ
	"workspaceFolder": "/workspace/app",
	// node_modulesのディレクトリの所有者をnodeユーザーに変更してからyarnを実行する
	"postCreateCommand": "sudo chown -R node:node ./node_modules && yarn",
	// リモートユーザーをnodeユーザーに変更する
	"remoteUser": "node",
	// 拡張機能のインストール
	"customizations": {
		"vscode": {
			"extensions": [
				"bradlc.vscode-tailwindcss"
			]
		}
	}
}
```

この状態でコンテナのリビルドを行い再度開発コンテナにアタッチします。
この時、プロジェクトのPathに2byte文字が含まれていると`compose.yaml`をbuildするときにエラーとなるため日本語などは含めないようにしてください。

以下のようにビルドに成功していればnode_modulesがDockerのVolume側に作成されている状態となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1239781/884562c9-5eaa-02d1-df66-2845dc31214a.png)

### ホットリロードができない

これはNext.jsだけでなくvueでも同じような現象を確認しています。
不便極まりないのでそれぞれ解決策を提示します。

#### Next.js の場合

`package.json`ファイルにある`scripts`の項目の`dev`コマンドに`WATCHPACK_POLLING=true`の変数を追加し対応します。

これによりホットリロードが行われるようになり、幸せになれます。

```json-doc
{
  "name": "my-app",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "WATCHPACK_POLLING=true next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  }
// 中略
}
```

#### vueの場合

`vue.config.js`ファイルの`defineConfig`に以下の設定を追加することでホットリロードが可能になります。

```js
const { defineConfig } = require('@vue/cli-service')
module.exports = defineConfig({
  transpileDependencies: true,

  configureWebpack: {
    watchOptions :{
      aggregateTimeout: 300,
      poll: 1000
    }
  },
})
```

## 最後に

これにてDev Containers布教は終了です。

実際、私が現在新規でWebアプリやコンソールアプリケーションの環境構築を行っていく中でほとんどの場合Dev Containersを利用して作成しています。
これにより、開発環境の構築のような本質とは無関係な部分でのコンフリクトが起こりにくくなり、円滑なコミュニケーションをとることができ満足しています。

皆さんも開発環境をスキップして快適な開発生活をスタートしましょう！