【Docker Compose】NestJS / TypeScript / MySQL の開発環境構築

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

https://nestjs.com/

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

- Windows11
- Docker Engine 26.1.1
- Docker Compose 2
- Node.js 20.15.1
- NestJS 10.0.0
- TypeScript 5.1.3
- MySQL 9.0.0
- MySQL2 3.11.0
- TypeORM 0.3.20\

# NestJS / TypeScript の環境構築
まずは NestJS アプリケーションの開発支援ツールである Nest CLI をインストールします。

https://docs.nestjs.com/cli/overview

```powershell
 npm i -g @nestjs/cli
```

次に以下のコマンドでプロジェクトを作成します。
`nest-mysql` はプロジェクト名です。また、`--strict` を追加することで、TypeScript の strict 系のコンパイラオプションが有効になります。

```powershell
nest new nest-mysql --strict
```

パッケージマネージャーは npm を利用します。

```powershell
⚡  We will scaffold your app in a few seconds..

? Which package manager would you ❤️  to use? npm
CREATE nest-mysql/.eslintrc.js (688 bytes)
CREATE nest-mysql/.prettierrc (54 bytes)
CREATE nest-mysql/nest-cli.json (179 bytes)
CREATE nest-mysql/package.json (2018 bytes)
CREATE nest-mysql/README.md (3413 bytes)
CREATE nest-mysql/tsconfig.build.json (101 bytes)
CREATE nest-mysql/tsconfig.json (562 bytes)
CREATE nest-mysql/src/app.controller.ts (286 bytes)
CREATE nest-mysql/src/app.module.ts (259 bytes)
CREATE nest-mysql/src/app.service.ts (150 bytes)
CREATE nest-mysql/src/main.ts (216 bytes)
CREATE nest-mysql/src/app.controller.spec.ts (639 bytes)
CREATE nest-mysql/test/jest-e2e.json (192 bytes)
CREATE nest-mysql/test/app.e2e-spec.ts (654 bytes)

✔ Installation in progress... ☕

🚀  Successfully created project nest-mysql
👉  Get started with the following commands:

$ cd nest-mysql
$ npm run start


                          Thanks for installing Nest 🙏
                 Please consider donating to our open collective
                        to help us maintain this package.


               🍷  Donate: https://opencollective.com/nest
```

プロジェクトを作成したディレクトリに移動します。

```powershell
cd nest-mysql
```

ローカルサーバーを起動します。

```powershell
npm start
```

プロジェクト作成時に自動生成されたコードの中に 'Hello World!' を返す `getHello` メソッドがあるので、動作確認します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/fe0e60d4-e9e4-4cc9-2f0c-d778c2f07298.png)

# NestJS の Dockerfile 作成
次は NestJS の 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"]
```

動作確認のため、作成した Dockerfile を利用したイメージのビルドとコンテナの起動確認をします。
まずはイメージをビルドします。

```powershell
docker image build --tag nest-ts:1.0.0 .
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/590a0354-a57f-c6b2-8bdd-9a161a564f07.png)

イメージがビルドできたか確認します。

```powershell
docker image ls nest-ts
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/ce285a93-5c2b-100b-a056-7625d39c76e8.png)

ビルドしたイメージを使ってコンテナを起動します。また、コンテナ起動時にホストマシンのファイルをバインドマウントします。

```powershell
docker container run                          `
--name nest-ts                                `
--rm                                          `
--publish 3000:3000                           `
--mount type=bind,source="$(pwd)",target=/app `
nest-ts:1.0.0
```

NestJS のプロジェクトファイル作成時と同じく `getHello` メソッドを呼び出して動作確認をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/999871b7-f2ad-dfc1-b147-87409a41f781.png)

# compose.yaml と DB 初期データ作成ファイルの作成
以下の要件を満たす `compose.yaml` を作成します。

- NestJS
    - コンテナ名：`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'
    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:
```

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

:::note
DB のボリュームマウント先の `/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/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");
```

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

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

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

```powershell
docker container ls
```

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/15ca8025-1415-7a18-3ef2-0b52e6d78445.png)

App コンテナの動作確認として、今回も `getHello` メソッドを呼び出して動作確認をします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/2f3296fe-4a6b-523a-a38a-7e046d13dce8.png)

DB コンテナの動作確認として、初期データが登録されているか確認します。
まずは MySQL に接続します。

```powershell
mysql --host=127.0.0.1 --port=3306 --user=user --password=userpassword
```

データベース一覧に環境変数として指定したデータベースが存在することを確認します。

```mysql
mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| performance_schema |
| sampledb           |
+--------------------+
3 rows in set (0.08 sec)
```

初期データ作成時に指定したテーブル及びユーザーデータがあることを確認します。

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

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

# DB 接続処理の実装
App コンテナ（NestJS）から DB コンテナへの接続処理を実装します。

## パッケージのインストール
必要なパッケージをインストールします。

- `MySQL2` : MySQL クライアント
- `TypeORM` : TypeScript / JavaScript 向けの ORM（DB とプログラミング言語のデータ変換）
- `@nestjs/typeorm` : NestJS 向けの TypeORM モジュール

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

https://typeorm.io/

https://github.com/nestjs/typeorm

```powershell
npm install --save @nestjs/typeorm typeorm mysql2
```

## TypeORM の設定
先ほどインストールした TypeORM の設定ファイルを作成します。

```src/ormconfig.ts
import { TypeOrmModuleOptions } from '@nestjs/typeorm';

const config: TypeOrmModuleOptions = {
  type: 'mysql',
  host: 'db',
  port: 3306,
  username: 'user',
  password: 'userpassword',
  database: 'sampledb',
  entities: [],
  synchronize: true,
};

export default config;
```

作成した設定値を `src/app.module.ts` の `imports` に `TypeORMModule.forRoot` の引数として渡します。

```src/app.module.ts
import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import config from './ormconfig';

@Module({
  imports: [TypeOrmModule.forRoot(config)], // Add
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

```

## アプリ初期化時にクエリを実行する
設定ができたので、実際 DB に接続する処理を追加します。
今回はアプリ初期化時に簡単なクエリを実行します。

```src/app.module.ts
import { Module, OnModuleInit } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { InjectDataSource, TypeOrmModule } from '@nestjs/typeorm';
import config from './ormconfig';
import { DataSource } from 'typeorm';

@Module({
  imports: [TypeOrmModule.forRoot(config)],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule implements OnModuleInit {
  constructor(@InjectDataSource() private readonly dataSource: DataSource) {}

  async onModuleInit() {
    try {
      await this.dataSource.query('SELECT 1');
      console.log('Database connection is successful');
    } catch (error) {
      console.error('Error connecting to the database', error);
    }
  }
}

```

# 動作確認
Docker を起動して動作確認をします。

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

先ほど実装したログ `Database connection is successful` が出力されます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/318188/76139fdd-a6d3-73f5-cf0a-e58b93de2080.png)


# 参考
- [Database](https://docs.nestjs.com/techniques/database)
- [First steps](https://docs.nestjs.com/first-steps)
- [mysql - Official Image](https://hub.docker.com/_/mysql)
- [node - Official Image](https://hub.docker.com/_/node)
- [Overview - CLI](https://docs.nestjs.com/cli/overview)
- [TypeORM](https://typeorm.io/)