【初心者向け】Dockerによるバックエンド開発環境構築ガイド

現在、アーリーステージのスタートアップで働いているのですが、新機能開発といった攻めの部分に集中しすぎて、テストなどの守りの部分がおろそかになっています。　

特に、開発環境のコンテナ化もできておらず、devとローカルで同じDB(RDS)を参照しているため、チーム開発にかなりの支障が出てしまっています...笑

主にフロントエンドとAIまわりを担当しているため、Dockerについてはあまり詳しくなかったのですが、チーム開発の崩壊を防ぐために、以下の目的から開発環境へのDocker導入を決意しました。
- 開発環境でRDSに負荷をかけない(RDSを使用しない)
- メンバー間でのマイグレーション影響をなくす(共有DB問題の回避)
- 開発環境立ち上げの速度改善(環境構築の迅速化)
- アイドリングタイムアウトによるサーバ再立ち上げの手間改善

既存のDBをコンテナDBに移行し、バックエンドとDBをdocker-composeで立ち上げるまでの手順について、以下に詳しくまとめました。

# 1. Docker関連ファイルの作成
開発しているサービスは、複数のフロントエンド（`./frontend/`)とバックエンド（`./backend/`)をモノレポで管理しています(フロントエンドはReact、バックエンドはNestJS)。

docker-composeを用いることで、これらの各サービスを一括して起動することも可能ですが、フロントエンドの中には開発中に使用しないものも存在するため、今回は必要なバックエンドとDBだけをDockerコンテナで立ち上げることにしました。

## Dockerfileの作成
まず、バックエンドのDockerfile（`Dockerfile.dev`）を以下のように作成します。

```Dockerfile:./backend/Dockerfile.dev
FROM node:18.15.0

# タイムゾーンを東京(JST)に設定
ENV TZ=Asia/Tokyo
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

WORKDIR /app

# パッケージの依存関係をコピー
COPY package*.json ./

# huskyインストールのエラーを回避するためにprepareスクリプトを無視
RUN yarn install --ignore-scripts

EXPOSE 8000

CMD ["yarn", "start:dev"]
```
既存DB(RDS)の設定がJSTだったため、タイムゾーンの設定を加えています。
また、huskyのインストールステップが失敗したので、Yarnで依存関係をインストールする際に`--ignore-scripts`でスクリプトを無視するようにしています。

## docker-compose.ymlの作成
次に、`docker-compose.yml`ファイルを以下のように作成します。
このファイルはバックエンドとDB(MySQL)のコンテナを管理します。

```yml:docker-compose.yml
version: "3.9"

services:
  backend:
    build:
      context: ./backend
      dockerfile: Dockerfile.dev
    ports:
      - "3010:3010" # ローカルとコンテナのポートをマッピング
    depends_on:
      - db # DBサービスの起動を待つ
    env_file: ./backend/.env # 環境変数を指定
    volumes:
      - ./backend:/app # ホストとコンテナのディレクトリをマッピング

  db:
    image: mysql:8.0.28
    platform: linux/x86_64
    container_name: db
    env_file: ./backend/.env # 環境変数を指定
    volumes:
      - db-data:/var/lib/mysql # データを永続化
    ports:
      - "3306:3306" # ローカルとコンテナのポートをマッピング
    command: --default-authentication-plugin=mysql_native_password --sql_mode=NO_ENGINE_SUBSTITUTION

volumes:
  db-data: # DBのデータを永続化するためのボリュームを定義
```
ここで作成した`Dockerfile.dev`と`docker-compose.yml`を利用して、バックエンドとDBを立ち上げることが可能になります。

フロントエンドが必要になった場合は、`./frontend/`に同様の手順でDockerファイルを作成し、`docker-compose.yml`にサービスを追加することで対応できます。

### 補足1
MySQLでは、DBの設定により、デフォルト値が設定されていないDATETIME型のフィールドに`0000-00-00 00:00:00`が自動で入るようにすることができるのですが、コンテナDBではそのような設定になっておらず、コンテナ起動後にINSERTエラーが発生してしまいました。

以下でMySQLの設定を確認しました。

```sql
SHOW VARIABLES;
```
`sql_mode`を確認したところ、コンテナDBと既存DB(RDS)で設定が異なっていたため、`db`の`command`に`--sql_mode=NO_ENGINE_SUBSTITUTION`を追加しました。

```
# コンテナDB
ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

# 既存DB(RDS)
NO_ENGINE_SUBSTITUTION
```

### 補足2
コンテナ起動後にバックエンドの修正を行ってもホットリロードされませんでした。
そのため、`backend`の`volumes`に`./backend:/app`を追加し、ホストとコンテナのディレクトリをマッピングしました。

### 補足3
モノレポのサービスであること、フロントエンドについてはコンテナ化しないことを前述しましたが、将来的にフロントエンドもコンテナ化する可能性もあるので、`docker-compose.yml`をルートディレクトリに配置しました。
`./backend/Dockerfile.dev`を読み込むように、以下の記述を行いました。

```yml
build:
  context: ./backend
  dockerfile: Dockerfile.dev
```

MySQLの接続設定については、`env_file: ./backend/.env`で読み込むようにしました。

# 2. 環境変数の追加
DBの設定情報を`./backend/.env`に環境変数として追加します(任意の値)。

```./backend/.env
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=
```

記述例

```
MYSQL_DATABASE=db_local
MYSQL_USER=admin
```

# 3. コンテナの起動

ルートディレクトリで以下のコマンドを実行し、複数コンテナ(backend, DB)を起動します。

```bash
$ docker-compose up
```

# 4. MySQL Workbenchの設定

MySQL Workbenchを使用してDocker上のMySQL DBに接続します。

1. MySQL Workbenchを開く
2. 画面左側のMySQL接続一覧で[+]ボタンをクリックする
3. 接続設定を以下のように設定する
    - **Connection Name**: 任意の接続名（例：db-local-docker）
    - **Hostname**: `localhost`（またはDockerが稼働しているホストのIPアドレス）
    - **Port**: `3306`（`docker-compose.yml`で設定したポート番号）
    - **Username**: `./backend/.env`のMYSQL_USER
    - **Password**: `./backend/.env`のMYSQL_PASSWORD
4. [Test Connection]をクリックして接続テストを行い、問題がなければ[OK]をクリックして接続を保存する。

![スクリーンショット 2023-05-27 13.11.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/998b4f1c-6fa2-4311-8111-7295e2c37948.png)

# 5. 既存DBのダンプエクスポート
dev環境で使用しているRDS for MySQLのDBから、ダンプ(テーブル情報とデータ)をエクスポートします。

ローカルマシンのポート(`<local_port>`)から、踏み台サーバー(`<ssh_user>@<remote_server_ip>`)経由でRDSのポート(`<remote_database_port>`)へ接続します(SSHトンネル)。

ローカルマシン上で既にMySQLが実行されている場合などでポートの競合を避けるために、今回は`local_port`と`remote_database_port`は別のポートを使用します。

```bash
$ ssh -f -N -L <local_port>:<remote_database_url>:<remote_database_port> -i <path_to_your_ssh_key> <ssh_user>@<remote_server_ip>
```

- **local_port**: ローカルマシンで開放したいポート（例：3307）
- **remote_database_url**: リモートのデータベースのURL（例：database.db.coedadadas22.ap-northeast-1.rds.amazonaws.com）
- **remote_database_port**: リモートのデータベースが使用しているポート（例：3306）
- **path_to_your_ssh_key**: SSHキーへのパス（例：~/.ssh/db-pem-key.pem）
- **ssh_user**: リモートサーバーへの接続に使用するユーザ名（例：ec2-user）
- **remote_server_ip**: リモートサーバーのIPアドレス（例：54.168.32.10）

ローカルホストのポート(`<local_port>`)に接続して、RDSの該当DB(`<database_name>`)のすべてのテーブル名をテキストファイル(`<output_file>`)に書き込みます。

```bash
＄ mysql -h <localhost_ip> -P <local_port> -u <username> -p <database_name> -e 'show tables' | tail -n +2 > <output_file>
```

- **localhost_ip**: MySQLクライアントが実行されているホストのIPアドレス（例：127.0.0.1）
- **local_port**: SSHポートフォワーディングを通じてリモートデータベースにアクセスするためのローカルマシンのポート（例：3307）
- **username**: データベースへの接続に使用するユーザ名（例：admin）
- **database_name**: 接続するデータベースの名前（例：db-prod）
- **output_file**: データベースから取得したテーブル名を保存するためのファイル名（例：all_tables.txt）

エクスポートしたいテーブルのリストを元にDBをダンプします。

```bash
$ mysqldump -h <localhost_ip> -P <local_port> -u <username> -p <database_name> $(cat <tables_list_file>) > <output_file>
```

- **localhost_ip**: データベースホストのIPアドレス（例：127.0.0.1）
- **local_port**: データベースがリッスンしているポート（例：3307）
- **username**: データベースに接続するユーザー名（例：admin）
- **database_name**: ダンプを作成するデータベースの名前（例：db-prod）
- **tables_list_file**: ダンプを作成するテーブルのリストが含まれるファイル（例：all_tables.txt）
- **output_file**: 出力（ダンプファイル）を保存するファイル名（例：database_export.sql）

# 6. コンテナDBへのダンプインポート

MySQL WorkbenchからコンテナDBにダンプをインポートします。

1. MySQL Workbenchを開く
2. メイン画面から接続したいデータベースの接続をクリックする
3. データベースの接続が開かれたら、メニューバーから「Server」->「Data Import」を選択する
4. 「Data Import」画面が表示されたら、「Import from Self-Contained File」を選択し、その右側の「...」ボタンをクリックして、インポートしたいSQLファイルを選択する
5. 「Default Target Schema」ドロップダウンメニューからインポート先のデータベースを選択する
6. 「Import Progress」セクションの「Start Import」ボタンをクリックする

![スクリーンショット 2023-05-28 22.27.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/2ae6ad06-eb4b-a005-d006-4b4be11269c3.png)