WSL2 + Dev Containers で Node.js + MySQL の開発環境を構築する

## はじめに

本投稿は、[42Tokyo × GENEROSITY ハンズオンイベント](<https://qiita.com/yi42/items/e29c06b2c6b8e8e4bd07>)に先立ち、Node.js + MySQL の開発環境を WSL2 + Dev Containers で事前に構築した際の覚書をまとめたものです。

### コンテナ利用の背景

イベントの事前準備に際し、コンテナ技術の利用は必須ではありませんでしたが、コンテナは開発環境を宣言的に定義でき、その再現性の高さが非常に魅力的です。習熟を目的として、このような機会では積極的に採用しています。

なお、Dev Containers の利点を十分に活かしきれていないですが、第一歩として暖かくご容赦いただければ幸いです。

### ご意見・ご指摘のお願い

内容に不備や誤りがございましたら、誠に恐縮ではございますが、ご指摘いただけますと幸いです。

## 事前準備の資料

事前準備として、以下のソフトウェアおよびツールが案内されていました。

- Visual Studio Code
- Postman
- Node.js (Volta で管理)
- MySQL
    - インストール
    - パスワード設定
    - `mysql/bin` の PATH を通す

明記されていたわけではありませんが、いずれも最新版を推奨と判断しました。

## Windows 側での準備

### Visual Studio Code

導入済みとし、本稿では特に言及しません。以降は「VSCode」と表記いたします。

### Postman

API の検証用です。公式サイトよりインストーラーをダウンロードし、導入しました。

https://www.postman.com/downloads/

### WSL2

こちらも導入済みとします。念の為バージョンを確認する場合は

```powershell
wsl.exe -l -v
```

ちなみに設定ファイル `C:\Users\NAME\.wslconfig`  (NAME は適宜変更) は

```ini
[wsl2]
networkingMode=mirrored
```

### WSL ディストリビューション

こちらも導入済みとします。
なお、デフォルトの WSL ディストリビューションとして Ubuntu 24.04 LTS を利用しております。以降、便宜上「Ubuntu」と表記いたします。

### Docker Desktop (WSL2 バックエンド)

必須ではありませんが、初学者なので手間とトラブルを避けるために導入します。

プランも Docker Personal サブスクリプションに該当すると判断しました。

https://www.docker.com/ja-jp/products/personal/#disclaimer

手順に従ってインストールおよび設定を行います。

https://docs.docker.jp/desktop/windows/wsl.html#wsl-rerequisites

以降の作業中には起動しておきます。

### Volta

Node.js のバージョン管理には Volta を利用する方法が紹介されていました。しかし、特定のバージョンが必要になった場合でも、コンテナ環境ではその場で準備できると判断し、Volta の導入は見送りました。

## WSL ディストリビューション側での準備

Windows 上で立ち上げた VSCode にて Remote SSH 拡張機能を利用して接続します。

### 構成

Ubuntu 上に今回のプロジェクトディレクトリを作成します。テンプレートやコマンドが豊富に用意されているものの、理解が不十分なため、ゼロから準備を進めました。

事前準備時ではなく最終的な構成です (なおかつ後日、微修正しています)。パスの指定などに無理があり、イマイチかと思いますが、下記の要件を満たせているので良しとします。

- frontend と backend のそれぞれのソースコードについて
    - 異なる VSCode のウィンドウで開き、同時編集が可能
    - 非コンテナ環境のものと共通であり、~~そのまま配置するだけで対応可能~~

※ 一部 localhost と記述していた箇所をコンテナのサービス名に変更する必要がありました。Docker Compose を利用する場合、各コンテナはデフォルトで同一のブリッジネットワークに所属します。これにより、サービス間はサービス名をホスト名として内部で直接通信が可能となります。

```shell
$ tree -a --dirsfirst
.
├── .devcontainer
│   ├── backend-container
│   │   ├── Dockerfile
│   │   └── devcontainer.json
│   └── frontend-container
│       ├── Dockerfile
│       └── devcontainer.json
├── backend
│   ├── .env
│   └── (API のソースコード)
├── frontend
│   ├── .env
│   └── (UI のソースコード)
└── compose.yml
```

https://code.visualstudio.com/remote/advancedcontainers/connect-multiple-containers

各ファイルの中身は、最小限の構成からスタートしました。ハンズオン中には、以下の様なコマンドを用いてコンテナ内で操作を行い、必要に応じて設定ファイルを更新しました。

```shell
# 個別に立ち上げ
docker compose -f compose.yml up -d db

# コンテナ内へ接続
docker compose -f compose.yml exec db bash

# データベースの各種操作など
mysql -u root -p
```

### compose.yml

複数のコンテナを利用する場合に、各コンテナ間の連携やネットワーク設定、ボリューム管理を定義します。
Docker Compose の設定ファイル (`docker-compose.yml` など) の書き方については、少し混乱しました。歴史的な経緯もあって、ネット上で公開されている例では古い形式も散見されます。現在は Compose Specification という標準仕様に基づいているようなので、基本的にはこれに準拠するのが良さそうです。

https://docs.docker.jp/compose/compose-file/index.html


```yaml
services:
  frontend:
    build:
      # frontend 用のビルドコンテキストは frontend のみとする
      context: ./frontend
      dockerfile: ../.devcontainer/frontend-container/Dockerfile
    ports:
      - "5173:5173" # Vite のデフォルトポート
    volumes:
      # ソースコードのホットリロードのため、ホスト側の frontend をマウント
      - ./frontend:/workspace:cached
      # node_modules はコンテナ側専用ボリューム（ホスト側に不要）
      - frontend_node_modules:/workspace/node_modules
    env_file:
      # 接続情報は環境変数として .env に記載  
      - ./frontend/.env
    depends_on:
      - backend

  backend:
    build:
      # backend 用のビルドコンテキストは backend のみとする
      context: ./backend
      dockerfile: ../.devcontainer/backend-container/Dockerfile
    ports:
      - "3000:3000"
    volumes:
      - ./backend:/workspace:cached
      - backend_node_modules:/workspace/node_modules
    env_file:
      - ./backend/.env
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=rootpass
      - MYSQL_DATABASE=calendar_db # 後から設定
    ports:
      - "3306:3306"
    volumes:
      - db_data:/var/lib/mysql

# 名前付きボリューム
volumes:
  db_data:
  backend_node_modules:
  frontend_node_modules:

```

- `ports`
    - ホストの 3000 番ポートをコンテナの 3000 番ポートにマッピング (ポートフォワーディング)
    - ホストの localhost:3000 へアクセスすると、その通信はコンテナ内のアプリケーションに転送される
- `depends_on` を使用すると、MySQL コンテナなどが先に起動するように依存関係を設定できる
    - ただし、MySQL が完全に初期化されてから接続を試みるようにするためにはこれだけでは不十分
- MySQL コンテナのサービス名 (ホスト名) は db とする
    - Docker Hub の公式 MySQL イメージ
    - 環境変数
        - `MYSQL_ROOT_PASSWORD`
        - `MYSQL_DATABASE`
    - secrets などの利用が理想的か
    - MySQL のデータ永続化
        - 名前付きボリュームを利用
        
### Dockerfile

コンテナイメージをビルドするための設定ファイルです。ベースとなる OS や必要なパッケージ、ツールのインストール手順を記述します。

```Dockerfile
# backend

FROM node:lts-bookworm-slim

# 作業ディレクトリの設定
WORKDIR /workspace

# ビルド時のみ
RUN apt-get update && \
    rm -rf /var/lib/apt/lists/*

COPY package*.json ./
RUN npm install

COPY . .

# 起動時
CMD ["node", "server.js"]
```

```Dockerfile
# frontend

FROM node:lts-bookworm-slim

WORKDIR /workspace

RUN apt-get update && \
    rm -rf /var/lib/apt/lists/*

COPY package*.json ./
RUN npm install

COPY . .

CMD ["npm", "run", "dev"]
```

- FROM
    - ベースイメージ (ベースとなる Docker イメージ) の指定
    - Docker Hub から Node.js の公式 Docker イメージを取得
    - Debian ベースの標準イメージを選択
    - <https://hub.docker.com/_/node/>
- RUN
    - パッケージリストの更新
    - 依存関係のインストール
- COPY
    - 今回はバインドマウントしているので必須ではないはず

### devcontainer.json

VSCode に対して、利用するコンテナ、必要な拡張機能、設定、フォワードするポートなどを指定します。

```json
{
    "name": "Frontend Container",
    "service": "frontend",
    "dockerComposeFile": "../../compose.yml",
    "workspaceFolder": "/workspace",
    "forwardPorts": [
        5173
    ]
    "settings": {
        "terminal.integrated.defaultProfile.linux": "bash"
    },
    "extensions": [
        "Vue.volar",
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-azuretools.vscode-docker"
    ]
}
```

```json
{
    "name": "Backend Container",
    "service": "backend",
    "dockerComposeFile": "../../compose.yml",
    "workspaceFolder": "/workspace",
    "forwardPorts": [
        3000
    ]
    "settings": {
        "terminal.integrated.defaultProfile.linux": "bash"
    },
    "extensions": [
        "dbaeumer.vscode-eslint",
        "esbenp.prettier-vscode",
        "ms-azuretools.vscode-docker"
    ]
}
```

## その他

起動は VSCode のコマンドパレットにて `Dev Containers: Reopen in Container` などで。
終了は `Close Remote Connection` などで切断するとコンテナも停止します。(正しい方法か確認不十分)

---

一度、コンテナなど諸々を起動した状態でラップトップを閉じてサスペンドさせたところ、復帰時にエラーが発生しました。

```
致命的なエラーです。
エラー コード: Wsl/Service/E_UNEXPECTED
Press any key to continue...
```

`wsl --shutdown` を実行してから再起動させました。
不安を感じたため、その後の休憩中もサスペンドさせず、起動させたままの状態を維持しました。

## 未解決

- VSCode のターミナルで操作をすると、日本語入力を受け付けない
- ファイルパーミッション等の権限