【Docker】Create React Appプロジェクトの開発・本番環境へのDocker導入

これまで開発環境でのDockerの使用について学んできました。

https://qiita.com/suzuki0430/items/b2527cfba7602f9fe3bc

https://qiita.com/suzuki0430/items/fcf57968365d15419601

https://qiita.com/suzuki0430/items/e07890c5d83596fe4787

Docker Composeまでなんとなくわかってきたので、今回は実際のプロジェクトにおけるDockerの使い方を学ぶことにしました。
私がReactを使っているということもあり、Create React Appのプロジェクトを開発および本番環境で実行することを目標にしました。

#開発環境用Dockerfileの作成
`npx create-react-app [プロジェクト名]`で適当なプロジェクトを作ります。

開発環境と本番環境ではサーバが異なります。そのため、Dockerfileも開発用と本番用で2つ必要になります。
ファイル名がかぶらないように、開発用は`Dockerfile.dev`という名前にします。

```dockerfile
FROM node:alpine

WORKDIR '/app'

COPY package.json .
RUN npm install

COPY . .

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

イメージをビルドするときは、`docker build -f Dockerfile.dev .` で実行することができます(ファイル名に`.dev`とつくため、`docker build .`ではビルドできません)。
ビルドの過程でコンテナのファイルシステム上に`node_modules`がつくられます(その後にコンテナは停止してファイルシステムのスナップショットがコピーされたイメージがつくられます)。
そのため、プロジェクトフォルダ直下にある`node_modules`は削除して問題ありません。

`-p`フラグをつけることで、localhostのポート(3000)とコンテナのポート(3000)を関連付けて、コンテナを起動することができます。
`docker run -it -p 3000:3000 [image id]`

これでブラウザからプロジェクトを開けるようになりました。

##ボリューム
無事に開発環境ができたのでプロジェクト作成を進めようとすると、ここで思わぬ落とし穴にハマります。
それは、**ファイルの変更内容がブラウザに反映されないということです。**

何故このようなことが起こるかというと、**コンテナのファイルシステムはローカルのファイルから独立しているためです。**
つまり、ファイルの変更を反映させるためには、再度イメージをビルドしてコンテナを立ち上げる必要があります。...超面倒くさい。

この問題を解決するのが**ボリューム**です。

ボリュームというのは、コンテナの外にあるデータの置き場のことです。コンテナ内部にデータ(ファイル)を保存してもコンテナを破棄すると消えてしまうため、永続化したいデータがある場合にボリュームは使われます。

ボリュームはコンテナにマウントされる(-v)ことで、コンテナからのアクセスが可能になります。
また、ボリューム(データの置き場)には以下の2つがあります。

- ホストのディレクトリ(ローカル)
- Docker管理下のストレージ領域

ホストのディレクトリをボリュームとしてコンテナにマウントすることで、`/src`や`/puclic`フォルダ内の変更がコンテナにも反映されるようになります。
![スクリーンショット 2021-03-09 21.31.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/a80c91e3-239a-b6c6-1af9-3408383254be.png)

また、`node_modules`についてはビルドの過程でつくられるため、ホストのディレクトリには置く必要がありません。その代わりにDocker管理化のリソースとして扱います。

コンテナの起動とボリュームのマウントを同時に行うには以下のコマンドを実行します。
`docker run -p 3000:3000 -v /app/node_modules -v $(pwd):/app [image id]`

`-v $(pwd):/app`の部分で、ローカルのカレントディレクトリをコンテナの`/app`にマウントします。
これによってコンテナからローカルのディレクトリを参照できるようになります。

つぎに、`-v /app/node_modules`で`node_modules`をDockerのリソースとして扱います。
`-v $(pwd):/app`でローカルの`node_modules`はすでに削除しているため、こちらの記述がないとエラーになります。


docker-compose.ymlは以下のように書くことができます。

```docker-compose.yml
version: '3'
services:
  web: //起動するコンテナ名
    stdin_open: true
    build: //ビルドするDockerfileの指定
      context: .
      dockerfile: Dockerfile.dev //ファイル名も指定する
    ports: //ローカルとコンテナのポートの関連付け
      - '3000:3000'
    volumes: //マウントするボリューム
      - /app/node_modules
      - .:/app
```

##テスト用コンテナの作成
Create React AppでつくられたプロジェクトにはすでにJestが含まれています。
そのため、`docker run -it [image id] npm run test`とすることでテストを実行することができます。
対話型になっているため、`-it`でターミナルとコンテナ内のJest CLIの標準入力を接続する必要があります。

```
Watch Usage
 › Press a to run all tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
```

また、docker-compose.ymlにtestsコンテナの記述を加えると、upしたときにwebコンテナとtestsコンテナが同時に立ち上がります。
testsコンテナではファイルの変更を反映(watch)しながらテストを実行します。

testsもwebと同じ`Dockerfile.dev`を素にしているので、実行コマンドを`command: ['npm', 'run', 'test']`として上書きしてあげる必要があります。

```docker-compose.yml
version: '3'
services:
  web:
    stdin_open: true
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - '3000:3000'
    volumes:
      - /app/node_modules
      - .:/app
  tests:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - /app/node_modules
      - .:/app
    command: ['npm', 'run', 'test'] //コマンドを上書きする
```

ついでに、ターミナルとコンテナの標準入出力を接続する方法には、`attach`と`exec`の2種類あります。

例えば、`docker-compose up`でコンテナを起動した後、ターミナルの別タブを開いて`docker ps`でtestsコンテナのid(container id)を調べ、`docker attach [testsのcontainer id]`または`docker exec -it [testsのcontainer id] sh`接続することができます。

どちらもコンテナへ接続することに変わりはないのですが、`attach`では、コンテナで起動しているPID=1のプロセスの標準入出力に接続し、`exec`ではコンテナで任意のコマンドを実行させるという違いがあります。


#本番環境用Dockerfileの作成
本番環境でコンテナを使う場合、nginxのようなWEBサーバが必要となります。
そのため、Dockerfileの中身もそれに対応させる必要があります。
![スクリーンショット 2021-03-10 7.24.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/a79326dc-7436-edae-4199-b1bec7919f08.png)

nginxで作成したプログラムを動かすためには、プログラムを実行できるような形に変換してあげる必要があり、これをビルドといいます。
つまり、Dockerfileにこのビルドのフェーズとビルドしたプログラムをnginxに配置する実行のフェーズを書いてあげれば、本番環境をたちあげることができます。

```dockerfile
// ビルドフェーズ
FROM node:alpine
WORKDIR '/app'
COPY package.json ./
RUN npm install
COPY . .
RUN npm run build

//実行フェーズ
FROM nginx
COPY --from=0 /app/build /usr/share/nginx/html
```

ビルドフェーズで、`npm run build`が実行されると、実行可能なプログラムがまとめられたbuildフォルダがコンテナの`/app`につくられます。
実行フェーズにおいては、`COPY --from=0 /app/build /usr/share/nginx/html`でbuildフォルダを`/usr/share/nginx/html`にコピー(ホスティング)しています。
`--from=0`では別のフェーズ(ビルドフェーズ)を指定しています。

`docker build .`でイメージをビルドし、`docker run -p 8080:80 [image id]`でローカルホストのポート8080とコンテナnginxのデフォルトポート番号80をマッピングし、コンテナを起動します。

`localhost:8080`にブラウザからアクセスすると、nginxに配置されたプログラムを開くことができるようになりました。
![スクリーンショット 2021-03-08 22.10.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569054/132da9a7-2fae-7520-d040-61b6f1f6e4df.png)

#おわりに
次はGitHub ActionsかCircle CIでCI/CDの構築に挑戦してみたいと思います。

#参考資料
https://www.udemy.com/share/101WjMCEEZd11VRno=/

https://qiita.com/gounx2/items/23b0dc8b8b95cc629f32

https://qiita.com/RyoMa_0923/items/9b5d2c4a97205692a560

https://qiita.com/leomaro7/items/649732faf2f632419f11