【Docker Compose】Node.js / TypeScript / MySQL の開発環境構築

# はじめに
この記事では、Docker Compose を利用した Node.js / TypeScript / MySQL の開発環境構築手順について記載します。

https://docs.docker.com/compose/

# 開発環境
開発環境は以下の通りです。

- Windows11
- Docker Engine 27.0.3
- Docker Compose 2
- MySQL 9.0.0
- Node.js 20.15.1
- npm 10.8.2
- @types/node 20.14.11
- ts-node 10.9.2
- TypeScript 5.5.3

# Node.js / TypeScript の環境構築
まずは Node.js / TypeScript の環境構築をおこないます。

## `package.json` の作成
`package.json` を作成します。

https://docs.npmjs.com/cli/v10/commands/npm-init

コマンド実行中の質問をスキップするため、`-y` をつけて実行します。

```powershell
npm init -y
```

`package.json` が作成されます。

```package.json
{
  "name": "node-mysql-crud",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}
```

## TypeScript のインストールと設定
TypeScript 及び以下のパッケージをインストールします。

- `ts-node` : TypeScript を JavaScript へコンパイルせず実行できるようにする
- `@types/node` : Node.js の型定義

https://typestrong.org/ts-node/

https://github.com/DefinitelyTyped/DefinitelyTyped

```powershell
npm install typescript ts-node @types/node --save-dev
```

`package.json` にインストールしたパッケージが表示されます。

```package.json
{
  ...
  "devDependencies": {
    "@types/node": "^20.14.10",
    "ts-node": "^10.9.2",
    "typescript": "^5.5.3"
  }
}
```

TypeScirpt 設定ファイルを作成します。

```powershell
npx tsc --init
```

Node.js 20 系を、`ES Modules` (`ECMA Script Modules`) で利用する設定に変更します。

https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping

```tsconfig.json
{
  "compilerOptions": {
    /* Language and Environment */
    "target": "ES2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
    "lib": [
      "ES2023"
    ] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,

    /* Modules */
    "module": "node16" /* Specify what module code is generated. */,

    /* Interop Constraints */
    "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
    "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,

    /* Type Checking */
    "strict": true /* Enable all strict type-checking options. */,

    /* Completeness */
    "skipLibCheck": true /* Skip type checking all .d.ts files. */
  }
}

```

## ソースコードの実装
HTTP サーバーを起動して、利用ポートのログを出力するコードを実装します。

```src/index.ts
import { createServer } from "http";

const server = createServer();

const PORT = process.env.PORT || 3000;

server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
});

```

## Node.js 起動スクリプトの追加
`package.json` に Node.js 起動スクリプトを追加します。

```package.json
{
  ...
  "scripts": {
    "start": "ts-node src/index.ts",
    ...
  },
  ...
}
```

ここまでできたら一度起動確認をします。

```powershell
npm start
```

実装したログが出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/ba6e76c8-a0c6-d114-34b4-24e5cf7d8f4c.png)


## Dockerfile の作成
Node.js は Dockerfile からイメージをビルドしたいので、Dockerfile を作成します。

```Dockerfile
# Use the official Node.js image
FROM node:20-alpine3.19

# Create and set the working directory
WORKDIR /app

# Copy package.json and package-lock.json
COPY package*.json ./

# Install dependencies
RUN npm install

# Expose the application port
EXPOSE 3000

# Run the application
CMD ["npm", "start"]
```

# compose.yaml の作成
以下の要件を満たす `compose.yaml` を作成します。

- Node.js
    - コンテナ名：`app-container`
    - ビルド：Dockerfile
    - ポート：3000
    - ソースコード：ホストマシンで実装
- MySQL
    - コンテナ名：`db-container`
    - ビルド：[MySQL 公式イメージ](https://hub.docker.com/_/mysql)のタグ [9.0.0](https://github.com/docker-library/mysql/blob/3d11822710789e82f64f08c578162a5b804d2f2f/innovation/Dockerfile.oracle)
    - ポート：3306
    - 初期データを用意
    - データはコンテナを削除しても残す
    - ホストマシンからDB接続可能
- コンテナ間通信可能

## サービス・コンテナ名
各コンテナのサービスとコンテナ名を定義します。

```compose.yaml
services:
  app:
    container_name: "app-container"
  db:
    container_name: "db-container"
```

## ボリューム
MySQL の以下の要件を満たすため、DBコンテナ用のボリュームを定義します。

> - データはコンテナを削除しても残す

`services` と同じ階層に定義します。

```compose.yaml
services:
  ...
volumes:
  db-volume:
```

## ビルド・イメージ
Appコンテナは Dockerfile から、DBコンテナは公式イメージからビルドするように定義します。

```compose.yaml
services:
  app:
    ...
    build: . # Dockerfile path
  db:
    ...
    image: mysql:9.0.0
...
```

## ポート
公開するポートを定義します。

```compose.yaml
services:
  app:
    ...
    ports:
      - "3000:3000"
  db:
    ...
    ports:
      - "3306:3306"
...
```

## バインドマウント
Appコンテナのソースコード（後ほど `./src` に作成）とDBコンテナの初期データ作成ファイル（以下）をバインドマウントします。

```db/init-users.sql
CREATE TABLE users(id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(32) NOT NULL, email VARCHAR(32) NOT NULL);
INSERT users (name, email) VALUES ("Wyatt", "wyatt@example.com");
INSERT users (name, email) VALUES ("Billy", "billy@example.com");
```

```compose.yaml
services:
  app:
    ...
    volumes:
      - type: bind
        source: ./
        target: /app
  db:
    ...
    volumes:
      - type: bind
        source: ./db
        target: /docker-entrypoint-initdb.d
...
```

:::note
初期データ作成ファイルのマウント先の値 `/docker-entrypoint-initdb.d` は、Docker Hub の MySQL リポジトリの Initializing a fresh instance に記載があります。[^1]
:::
[^1]:https://hub.docker.com/_/mysql

## ボリュームマウント
データはコンテナを削除しても残せるようにDBボリュームをDBコンテナにマウントします。

```compose.yaml
services:
  ...
  db:
    ...
    volumes:
      ...
      - type: volume
        source: db-volume
        target: /var/lib/mysql
volumes:
  db-volume:
```

:::note
マウント先の `/var/lib/mysql` は、MySQL サーバーがデータを保存するデフォルトのディレクトリになります。MySQL 設定ファイル（`/etc/my.cnf`）で定義されています。

```powershell
docker container run --rm mysql:9.0.0 cat /etc/my.cnf
```

```powershell
[mysqld]
...
datadir=/var/lib/mysql
...
```
:::

## 環境変数
ホストマシンからのDBコンテナ接続やコンテナ間通信を可能にするため、DB接続情報を環境変数に定義します。

```compose.yaml
services:
  app:
    ...
    environment:
      DB_HOST: db
      DB_USER: user
      DB_PASSWORD: userpassword
      DB_NAME: sampledb
  db:
    ...
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: sampledb
      MYSQL_USER: user
      MYSQL_PASSWORD: userpassword
```

## 完成
完成した `compose.yaml` は以下の通りです。

```compose.yaml
services:
  app:
    container_name: "app-container"
    build: . # Dockerfile path
    ports:
      - "3000:3000"
    volumes:
      - type: bind
        source: ./
        target: /app
    environment:
      DB_HOST: db
      DB_USER: user
      DB_PASSWORD: userpassword
      DB_NAME: sampledb
  db:
    container_name: "db-container"
    image: mysql:9.0.0
    ports:
      - "3306:3306"
    volumes:
      - type: bind
        source: ./db
        target: /docker-entrypoint-initdb.d
      - type: volume
        source: db-volume
        target: /var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword
      MYSQL_DATABASE: sampledb
      MYSQL_USER: user
      MYSQL_PASSWORD: userpassword
volumes:
  db-volume:

```

# 動作確認
一度動作確認をします。

```powershell
docker compose up --build
```

`app-container` に実装したログが出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/14a301d6-d0f5-42f9-1754-9d5542fa32d8.png)

コンテナ一覧を確認します。

```powershell
docker container ls
```

`compose.yaml` で定義した名前通りのコンテナが作成されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/6c1e293e-af19-20a4-f5a2-1e45cd9fedbf.png)

:::note
この時点では、AppコンテナとDBコンテナがそれぞれ起動しているだけで、コンテナ間の通信は発生していません。
:::

# DB接続処理の実装
AppコンテナからDBコンテナに接続する処理を実装します。
まず MySQL クライアントとして MySQL2 をインストールします。

https://sidorares.github.io/node-mysql2/docs

```powershell
npm install mysql2
```

DB接続処理を実装します。先ほどインストールした MySQL2 の `createConnection` を利用して、DBへ接続する関数を実装します。

https://sidorares.github.io/node-mysql2/docs/examples/connections/create-connection

```src/database.ts
import mysql from "mysql2/promise";

export const createConnection = async () => {
  try {
    const connection = await mysql.createConnection({
      host: process.env.DB_HOST,
      user: process.env.DB_USER,
      password: process.env.DB_PASSWORD,
      database: process.env.DB_NAME,
    });
    console.log("Connected to database");
    return connection;
  } catch (error) {
    console.error("Failed to connecting to database", JSON.stringify(error));
    process.exit(1);
  }
};

```

DBへ接続する関数を `src/index.ts` で呼び出します。

```src.index.ts
import { createServer } from "http";
import { createConnection } from "./database";

const server = createServer();

const PORT = process.env.PORT || 3000;

server.listen(PORT, () => {
  console.log(`Server is running on port ${PORT}`);
  createConnection(); // Add
});
```

# 動作確認
最後に動作確認をします。
まず、コンテナを起動します。

```powershell
docker compose up --build
```

DB接続のログも出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/79e0bfc9-53b9-ea4e-1a0e-69ec77643a5d.png)

MySQL に接続します。

```powershell
mysql --host=127.0.0.1 --port=3306 --user=root --password=rootpassword sampledb
```

初期データとして追加ユーザーが確認できます。

```mysql
mysql> select * from users;
+----+--------+--------------------+
| id | name   | email              |
+----+--------+--------------------+
|  1 | Wyatt  | wyatt@example.com  |
|  2 | Billy  | billy@example.com  |
+----+--------+--------------------+
4 rows in set (0.01 sec)
```

# 参考
- [Dockerfile reference](https://docs.docker.com/reference/dockerfile/)
- [Node Target Mapping](https://github.com/microsoft/TypeScript/wiki/Node-Target-Mapping) 

# 関連
https://github.com/yasushi-mo/docker-samples/tree/main/node-mysql-crud

https://qiita.com/Yasushi-Mo/items/789871a7f3eb86b01a62

https://qiita.com/Yasushi-Mo/items/722a11608a1a8d5bde27