Dockerでフロントエンド(React)+バックエンド(TypeScript)+DB（MySQL）の開発環境を構築してみた

## はじめに

この記事はDockerについて何もわからないLv.1のエンジニアが時代についていくために
Dockerでフロントエンド+バックエンド+DBの開発環境を構築してみようと思い立った顛末を記した記事です。

## Dockerってなんだ？
まずDockerとはそもそも何なのか、
なんとなく下記のようなイメージを抱いていました。

* いわゆるコンテナ的なもの？
* アプリケーションの実行がお手軽にできる
* 開発環境構築が楽になるらしい
* ***何もわからん！！！***

### Dockerとは
Docker社の開発したオープンソースのコンテナ型プラットフォーム。
Dockerを使用すると一台のPCで複数のコンテナ（アプリケーションの実行環境パッケージ）を立てて実行することができるようになるらしい。
つまり、お手軽にアプリケーションの実行環境を用意できるもの？

### Dockerの用語たち

#### Dockerエンジン
色々な処理を行うDockerの本体のようなもの。
CLIとREST API、Dockerデーモンの3つで構成されていて
CLIでコマンドを打つとREST APIが叩かれてサーバー（Dockerデーモン）に処理内容を伝えてくれるらしい。

#### Dockerコンテナ
Dockerが実行するコンテナのこと。
そのままの名前。

#### Dockerイメージ
Dockerコンテナを実行するためのテンプレートのようなもの。
Dockerイメージを実行することでDockerコンテナを生成し、アプリケーションを実行させることができる。
Dockerレジストリ（後述）に登録されているイメージをPullして用意するほか、Dockerfile（後述）からビルドすることで自身でカスタマイズしたDockerイメージを用意することも可能。

つまり、流れとしてはこんな感じ？
DockerHub → （Pull） → Dockerイメージ → （実行） → Dockerコンテナ
Dockerfile → （ビルド） → Dockerイメージ → （実行） → Dockerコンテナ

#### Dockerレジストリ
作成したDockerイメージを保管するサービス。
Docker Hub（GitHub的なイメージ？）やAWS Elastic Container Registry等があり、公開されているイメージをPullして利用することもできる。

#### Dockerfile
Dockerイメージの設計ファイル。
Dockerfileを書くことで自身でカスタマイズしたDockerイメージを作成することができる。

#### Docker Compose
複数のDockerコンテナを操作するためのツール。
docker-compose.ymlという設定ファイルを定義することで複数コンテナの起動や停止、ビルド等をまとめて行うことができて便利らしい。


## 動かしてみよう
なんとな～～～くDockerについて理解を深めた（つもりになった）ところで実際に動かしてみることにします。
まずはDockerのインストールから。

Docker DesktopなるソフトウェアはGUIで色々操作ができるほか、インストールするとDockerエンジンやCLI、Docker Compose等をまとめてインストールしてくれるそうなのでDocker Desktopをインストールします。
利用にはDocker Hubのアカウントを作る必要があるそうなのでまずはアカウントを作成。
今回は個人利用目的なのでPersonalプランでアカウントを作り、早速Docker Desktop on Windowsをインストール。

### nginxをDockerで動かしてみる

インストールが完了したら早速Dockerを動かしてみようということで、
nginxのDockerイメージからDockerコンテナを動かしてみます。

↓作業するディレクトリ構造はこんな感じです。

```
root
┣ index.html
┣ Dockerfile
┗ docker-compose.yml
```

#### Dockerfile

今回はとりあえず動かすだけなのでDockerfileをわざわざ作る必要もないですが、Dockerfileから作る流れを知るためにDockerfileから作ってみます。

```Dockerfile 
# ベースとなるイメージを指定
# nginxが提供している最新バージョンのイメージを指定している
FROM nginx:latest

# 作業を行うディレクトリを指定
# nginxのイメージはこのディレクトリで色々作業するらしい
WORKDIR /usr/share/nginx

# ローカルのindex.htmlを/usr/share/nginx/html配下にコピーする
COPY ./index.html ./html
```

<details>
<summary>index.htmlの中身はどうでもいいので折りたたみます。</summary>

```index.html
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8" />
    <title>nginx × Dockerサンプル</title>
</head>
<body>
    <h1>nginxをDockerで動かしてみるよ！！</h1>
</body>
</html>
```

</details>

Dockerfile（とindex.html）が書けたらDockerfileからイメージをビルドしてコンテナを実行します。

```sh
# Dockerfileから`nginx-sample-image`という名前でイメージをビルド
docker image build ./ -t nginx-sample-image

# nginx-sample-imageから`nginx-sample`という名前でコンテナを実行する
# -p 8080:80でホストPCの8080ポートとコンテナ上の80ポートを紐づけ（ポートフォワード）
docker run --name nginx-sample -p 8080:80 nginx-sample-image
```

http://localhost:8080 へアクセスしてみると・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/a88e9de6-7650-0f04-bea4-a1140aed8779.png)

無事コンテナを動かすことができました。

### Docker Compose
Docker Composeでも動かしてみようということで、
docker-compose.ymlを作成します。

```docker-compose.yml
# docker-composeファイルのバージョン
version: '3'

# アプリケーションを構成するサービスの設定
services:
  web:
    # Dockerビルドの設定
    build: 
      context: .
      dockerfile: ./Dockerfile
    # サービスで使用するポートを指定する
    ports:
      - 8080:80
```

docker-compose.ymlを作成したらイメージをビルドして実行します。

```sh
# docker-composeでDockerイメージをビルドする
docker-compose build

# Dockerコンテナを実行する
docker-compose up
```

アクセスしてみると・・・
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/a88e9de6-7650-0f04-bea4-a1140aed8779.png)

Docker Composeでもコンテナを動かすことができました。

## 開発環境を作る

さて、Docker初心者がとりあえず触ってみた編を終えたところで
いよいよDockerを用いた開発環境を作っていきます。


### DevContainer
VSCodeの拡張機能「Remote Development」を利用すると開発用DockerコンテナにVSCodeからアクセスし、コンテナ内のファイルを直接編集することができるようになります。
DevContainerを使うことでローカルPC上にライブラリや各種拡張機能等々が入り混じってごちゃついてしまう現象を解消することが可能となるらしく、便利そうなので利用してみます。


```
root
┣ frontend
┗ backend
```

#### フロントエンド

frontendディレクトリにひな型を作成します。
本当はDevContainerの中で作業を行うようにしたかったのですが、
DevContainerの中でうまいことcreate-react-appができなかったのであきらめてローカルで作りました。。。

```sh
# frontendのひな型を作成
cd frontend

npx create-react-app . --template typescript

# ローカルに不要なnode_modulesを消す
rm -r node_modules
```


#### バックエンド
backendディレクトリにひな型を作成します。

```sh
# backendのひな型を作成
cd backend

npm init -y

# 必要なパッケージをインストール
npm i -D typescript nodemon ts-node

npx tsc --init

mkdir src

touch src/index.ts

touch nodemon.json

# ローカルに不要なnode_modulesを消す
rm -r node_modules
```

tsconfing.jsonを適当に弄り、
```tsconfig.json
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "rootDir": "./src",
    "outDir": "./dist",
    "strict": true,
    "skipLibCheck": true
  }
}
```

ホットリロードの設定を行います。

```package.json
{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "devDependencies": {
    "nodemon": "^3.0.1",
    "ts-node": "^10.9.1",
    "typescript": "^5.2.2"
  },
  "scripts": {
    "dev": "npx nodemon"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}
```

```nodemon.json
{
    "watch": ["src"],
    "ext": "ts",
    "exec": "npx ts-node ./src/index.ts"
}
```

### DevContainer

各Dockerコンテナ用のDevContainer設定ファイルを追加します。

```frontend/.devcontainer.json
{
    "name": "Frontend",
    "dockerComposeFile": ["../docker-compose.yml"],
    "service": "frontend",
    "shutdownAction": "none",
    "workspaceFolder": "/workspace/frontend"
}
```

```backend/.devcontainer.json
{
    "name": "Backend",
    "dockerComposeFile": ["../docker-compose.yml"],
    "service": "backend",
    "shutdownAction": "none",
    "workspaceFolder": "/workspace/backend"
}
```

### DockerFile

各Dockerfileの設定を行います。

```
root
┣ frontend
┃  ┣ src
┃  ┣ public
┃  ┣ package.json
┃  ┣ tsconfig.json
┃  ┗ .devcontainer.json
┣ backend
┃  ┣ src
┃  ┣ nodemon.json
┃  ┣ package.json
┃  ┣ tsconfig.json
┃  ┗ .devcontainer.json
┗ docker
   ┣ frontend.Dockerfile
   ┣ backend.Dockerfile
   ┗ db.Dockerfile
```

```frontend.Dockerfile
FROM node:20

WORKDIR /workspace/frontend

COPY ../frontend/ ./

RUN npm install

# コンテナ起動時に実行するコマンド
CMD [ "npm", "start" ]
```

```backend.Dockerfile
FROM node:20

WORKDIR /workspace/backend

COPY ../backend/ ./

RUN npm install

# コンテナ起動時に実行するコマンド
CMD ["npm", "run", "dev"]
```

dbはとりあえずMySQLの実行環境のみ用意（正直Dockerfileは要らない）。
```db.Dockerfile
FROM mysql:latest
```

### docker-compose.yml

```docker-compose.yml
version: '3'

services:
  # frontendの設定
  frontend:
    container_name: docker-sample-frontend
    build: 
      context: .
      dockerfile: ./docker/frontend.Dockerfile
    environment:
      # Reactのホットリロードを有効化するための設定
      - WATCHPACK_POLLING=true
    volumes:
      # ルートディレクトリ配下をworkspace配下にマウントする
      - .:/workspace:cached
      # node_modulesは別ボリュームでマウントする
      - frontend_node_modules:/workspace/frontend/node_modules
    ports:
      - '8080:3000'
  # backendの設定
  backend:
    container_name: docker-sample-backend
    build:
      context: .
      dockerfile: ./docker/backend.Dockerfile
    environment:
      # nodemonでホットリロードを有効化するための設定
      - CHOKIDAR_USEPOLLING=true
    volumes:
      # ルートディレクトリ配下をworkspace配下にマウントする
      - .:/workspace:cached
      # node_modulesは別ボリュームでマウントする
      - backend_node_modules:/workspace/backend/node_modules
    ports:
      - '3000:3000'
  # dbの設定
  db:
    container_name: docker-sample-mysql
    build:
      context: .
      dockerfile: ./docker/db.Dockerfile
    ports:
      - '3306:3306'
    environment:
      MYSQL_ROOT_PASSWORD: mysql
      MYSQL_USER: user
      MYSQL_PASSWORD: password
      TZ: Asia/Tokyo
    volumes:
      # MySQLのデータをボリュームにマウントする
      - db-store:/var/lib/mysql

# ボリュームの設定、実行時に作ってくれる
volumes:
  frontend_node_modules: 
  backend_node_modules:
  db-store:
```


最終的に以下のようなディレクトリ構造になりました。
```
root
┣ frontend
┃  ┣ src
┃  ┣ public
┃  ┣ package.json
┃  ┣ tsconfig.json
┃  ┗ .devcontainer.json
┣ backend
┃  ┣ src
┃  ┣ nodemon.json
┃  ┣ package.json
┃  ┣ tsconfig.json
┃  ┗ .devcontainer.json
┣ docker
┃  ┣ frontend.Dockerfile
┃  ┣ backend.Dockerfile
┃  ┗ db.Dockerfile
┗ docker-compose.yml
```

### 開発用Dockerコンテナにアクセスする
さて、ここまで出来たら開発用コンテナにアクセスして開発してみましょう。
VSCodeからfrontendプロジェクトを開き、F1 >「Dev Containers: Reopen in Container」を選択するとfrontendコンテナにVSCodeからアクセスすることができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/e2029eb7-8491-528c-fdd6-25572b01c1df.png)

開発コンテナ frontendにアクセスできました。
これでコンテナ内のファイルを直接編集することができるようになります。
また、ソースコードはホストPCのディレクトリとマウントされているため、DevContainer内での変更内容はホストPCにもしっかり反映されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3125818/eb744731-9c00-cb52-e870-4e5a4c7cc5b9.png)

更に、DevContainerを起動すると、.devcontainer.jsonで指定したdocker-compose.ymlに基づいて（？）、関連するコンテナをまとめて起動してくれます。
今回であればfrontendのDevContainerの起動に合わせてbackend、dbのDockerコンテナも起動してくれます。


## おわりに

さて、何とかDockerとDevContainerを用いてReact×TypeScript×MySQLの開発環境構築ができました。


・・・が、結局Dockerについて詳しくなれた気がしませんね。
調べれば調べるほどわからないことが増えていく不思議。
なんとかLv.1.1くらいにはなれたと信じて今後も勉強していきたいです。


## 参考
https://docs.docker.com/reference/

https://docs.docker.jp/get-started/overview.html

https://containers.dev/implementors/json_reference/

https://qiita.com/SatoshiSobue/items/a612ebbb3a9242c09db5

https://qiita.com/yuta-ushijima/items/d3d98177e1b28f736f04

https://qiita.com/ttnt1006/items/0b7cbbba401a61c54945