React アプリを小さなサイズの Docker イメージにして運用

## 要約

* React アプリを Dockerize するなら、Docker のマルチステージビルドを利用して nginx イメージに載せることで、イメージサイズを 1GB -> 22MB にできる。

## はじめに

React アプリを作ってみようと思い立ち、ローカルの開発環境では動かせるようになった。だが、これを本稼働させるにはどうするのがスマートなのか調べてみた。

[React の公式ドキュメント][react-deployment]では、第一の方法として、次のように [serve][npm-serve] を使うのが簡単だよと書いてあるが、これだと実行環境にも Node.js が必要になる。

[react-deployment]: https://create-react-app.dev/docs/deployment
[npm-serve]: https://www.npmjs.com/package/serve

```shell
npm install -g serve
npm run build
serve -s build
```

## Dockerize

運用しているほかの Web アプリは Docker 上で稼働させているので、React アプリも Docker イメージ化(Dockerize)して稼働させてみることにした。

まずは、DockerHub の node.js イメージから[公式ドキュメント][node-dockerhub]をたどってみたが、実践的な Dockerfile の書き方については情報なし。

[node-dockerhub]: https://github.com/nodejs/docker-node/blob/master/README.md#how-to-use-this-image

そんな中、まさに的中の次の記事を見つけた。

> [Dockerizing a React App - Michael Herman](https://mherman.org/blog/dockerizing-a-react-app/)

## 方法

React アプリを Docker イメージ化して稼働させる方法として次の3つが考えられる。

1. 開発デバッグ用と同様に [npm start][react-scripts] で手っ取り早く稼働させる。
2. [React の公式ドキュメント][react-deployment]の通り、[serve][npm-serve] を使って稼働させる。
3. 先の記事にある [Production](https://mherman.org/blog/dockerizing-a-react-app/#production) のように、nginx イメージにビルド結果を載せて稼働させる。

[react-scripts]: https://create-react-app.dev/docs/available-scripts

1.の方法では、js ファイルの minify もされないため、開発デバッグ用ならともかく、本番環境用とは言い難い。

2.の方法では、本番環境と同様にパッケージ化されて稼働するが、その Docker イメージには Node.js の実行環境に加えて、packages.json の dependencies に指定されているパッケージ(node_modulesフォルダ)も含まれてしまう。そのため、イメージサイズが大きい。

3.の方法ならイメージサイズもコンパクトになると期待できる。

## 実装

* React アプリのルートフォルダに下記 Dockerfile と docker-compose.yml を用意し、`docker-compose up -d` コマンドを実行することでアプリ稼働。

* 方法3 のポイントは Docker のマルチステージビルド。詳細は次のドキュメント参照。

    > <https://docs.docker.com/develop/develop-images/multistage-build/>

* Dockerfile では、`npm install` と `npm run build` を別々に実行してレイヤを分けることで、依存パッケージに変更なければイメージビルド時のキャッシュが働き、ビルド時間を短縮。

### Dockerfile

```dockerfile:Dockerfile

ARG NODE_TAG=12
ARG NGINX_TAG=alpine
ARG APP_HOME=/home/node/app

# build stage
FROM node:${NODE_TAG} as build
ARG NODE_TAG
ARG APP_HOME

WORKDIR ${APP_HOME}
COPY package*.json ${APP_HOME}/
RUN echo -n 'node '; node -v; \
    echo -n 'npm '; npm -v ; \
    npm install --production

COPY src ${APP_HOME}/src/
COPY public ${APP_HOME}/public/
RUN npm run build

# deploy stage
FROM nginx:${NGINX_TAG}
ARG NGINX_TAG
ARG APP_HOME
RUN echo "nginx:${NGINX_TAG}" > /docker-image-tag && cat /docker-image-tag
COPY --from=build ${APP_HOME}/build /usr/share/nginx/html

WORKDIR /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
```

### docker-compose.yml

```yml:docker-compose.yml

version: '3'

services:
    app:
        container_name: reactapp
        image: reactapp:latest
        build:
            context: .
            args:
              - NODE_TAG=12.8
              - NGINX_TAG=alpine
        environment:
            - NODE_ENV=production
        ports:
            - 8080:80
        restart: always
```

## 試行・比較

(方法1) 1.03 GB

<https://github.com/tajimak/reactapp-dockerized/tree/start-app/npm-start>

```
$ docker-compose build
$ docker images | grep reactapp
reactapp    latest    f951bf73549b    About a minute ago    1.03GB
```

(方法2) 1.03 GB

<https://github.com/tajimak/reactapp-dockerized/tree/start-app/node-serve>

```
$ docker-compose build
$ docker images | grep reactapp
reactapp    latest    8abace7aab5c    About a minute ago    1.03GB
```

(方法3) 22 MB

<https://github.com/tajimak/reactapp-dockerized/tree/start-app/on-nginx>

```
$ docker-compose build
$ docker images | grep reactapp
reactapp    latest    d07dcfbb1e54    About a minute ago   21.7MB
```

## SSL/TLS 対応

* 上記の Dockerfile では React アプリのポートを直接公開してしまっているが、実際には、SSL/TLS 対応のリバースプロキシ配下に配置するなどして、セキュリティへの考慮も必要。

* リバーズプロキシ配下で Context Path(Relative Path)付き URL とする場合は、[このドキュメント][react-contextpath]に従って packages.json に `"homepage": ".",` を追加しておく。

```json
{
  "name": "reactapp-dockerized",
  "homepage": ".",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
  ...
}
```

[react-contextpath]: https://create-react-app.dev/docs/deployment#serving-the-same-build-from-different-paths

## 参考：試行環境

| 項目 | バージョン |
|----|----|
|node.js| 12.8.1 |
|npm | 6.10.2 |
| Docker | 19.03.1 |
| docker-compose | 1.24.0 |
| Docker Host OS | Debian 9.9 |