既存のRails(APIモード) × React × Nginx × MySQLで作成したSPAをDocker化

# 概要

新規にDockerで開発環境を構築するのではなく、既存のローカルのSPAをDocker化するために自分が行ったことをご紹介します。
また、本番環境ではWebサーバーにNginxを使用しているため、環境ごとの差分を少なくするために、開発環境でもフロントエンド側にNginxを使用しています。

# バージョン
|| バージョン |
|:-:|:-:|
| OS | macOS Catalina  |
| ruby | 2.7.0 |
| Rails(APIモード) | 6.0.4 |
| React | 17.0.2  |
|  Node | 14.4.0  |
|  MySQL | 8.0.28  |
| Docker | 20.10.6 |
|Nginx|1.20.0|

# ディレクトリ構成

```bash:myapp
.
├── app
├── config
├── frontend
│   ├── Dockerfile
│   ├── nginx.conf
│   ├── node_modules
│   ├── package-lock.json
│   ├── package.json
│   ├── src
│   └── yarn.lock
├── entrypoint.sh
├── Dockerfile
├── docker-compose.yml
├── Gemfile
├── Gemfile.lock

```


# 手順1： Docker Engineのインストール
まずはdockerコマンドを使えるようにするためにDocker Engineをインストールしていきます。Docker Engineのインストールについては[公式のインストール概要](https://docs.docker.com/get-docker/)が参考になるかと思います。

# 手順2: Dockerfile等の作成

## API側のDockerfileと関連するファイルの設定

#### Dockerfile(API)
```dockerfile:Dockerfile(API)
FROM ruby:2.7.0

RUN apt-get update && apt-get install -y curl apt-transport-https wget && \
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
apt-get update && apt-get install -y yarn

RUN apt-get update -qq && apt-get install -y nodejs yarn

WORKDIR /myapp

COPY ./Gemfile /myapp/Gemfile
COPY ./Gemfile.lock /myapp/Gemfile.lock

RUN gem install bundler
RUN bundle install

VOLUME /tmp

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

CMD ["rails", "server"]

```

ここでのポイントは、VOLUMEで/tmpにマウントポイントを作成していることです。
これを記述することで、後に設定していくフロントエンド側のコンテナにpuma.sockがマウントされ、API側のコンテナとソケット通信することが可能になります。

#### entrypoint.sh
```bash:entrypoint.sh
#!/bin/bash
set -e

# Remove a potentially pre-existing server.pid for Rails.
rm -f /myapp/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
```
コンテナが起動されるたびに実行されるスクリプトです。特定のserver.pidファイルが事前に存在する場合にサーバーが再起動しないRails固有の問題があるそうで、それを修正するためにこのスクリプトを実行します。
（参考：[Quickstart: Compose and Rails](https://docs.docker.com/samples/rails/)）

## フロントエンド側のDockerfileと関連するファイルの設定

#### Dockerfile(フロントエンド)
```Dockerfile:Dockerfile(フロントエンド)
FROM node:14.4.0 as build
WORKDIR /home/node/app
COPY ./ /home/node/app
RUN npm install
RUN npm run build

FROM nginx:1.20.0
RUN rm -f /etc/nginx/conf.d/*
COPY --from=build /home/node/app/build /var/www
COPY ./nginx.conf /etc/nginx/conf.d/
CMD /usr/sbin/nginx -g 'daemon off;' -c /etc/nginx/nginx.conf
```

ここでのポイントはマルチステージビルドを行なっていることです。
１〜５行目でローカルのReactのソースコードをビルド。ここで生成されたものをnginxコンテナの/var/wwwにコピーすることで、Nginx上でReactを動かすことができるようになります。

#### Nginxの設定
```nginx:nginx.conf
upstream api {
  server unix:/myapp/tmp/sockets/puma.sock;
}
server {
  listen       80;
  server_name  localhost:3000;
  charset      utf-8;

  root /var/www/;
  index index.html index.htm;

  location / {
    try_files $uri /index.html;
  }

  location /api/v1 {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://api;
  }

  error_page 500 502 503 504 /500.html;
  client_max_body_size 4G;
  keepalive_timeout 10;

}
```
frontendコンテナの/etc/nginx/conf.d/に配置されるNginxの設定ファイルです。
Rails側のURLが/api/v1なので、`http://localhost:3000/api/v1` にリクエストが来たときにはバックエンドへリクエストを流すように設定しています。

#### Pumaの設定
```ruby:config/puma.rb
# ソケット通信を行うためコメントアウト
# port        ENV.fetch("PORT") { 3000 }

# こちらを新しく記述
bind "unix:///myapp/tmp/sockets/puma.sock"
```


# 手順３：docker-compose.ymlの作成と関連するファイルの設定

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

services: 
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - db-data:/var/lib/mysql
  api:
    build: 
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/myapp
      - bundle:/usr/local/bundle
    depends_on:
      - db
  frontend:
    build:
      context: ./frontend/
      dockerfile: Dockerfile
    ports:
      - "3000:80"
    volumes_from:
      - api
    depends_on:
      - api

volumes:
  db-data:
  bundle:
```

ここでのポイントはfrontendコンテナの中の

```docker:docker-compose.yml
    volumes_from:
      - api
```
です。
これを記述することによって、API側DockerfileのVOLUMEで指定した/tmpディレクトリがfrontendコンテナにマウントされ、apiコンテナとソケット通信ができるようになります。

#### database.yml
```yml:database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password: password
  host: db

development:
  <<: *default
  database: myapp_development

test:
  <<: *default
  database: myapp_test

```

docker-compose.ymlでコンテナの名前を`db` 、 `MYSQL_ROOT_PASSWORD`を `password` としているため、defaultの欄に以下の内容を記述しています。

```yml
  username: root
  password: password
  host: db
```

# 手順４：イメージ、コンテナの作成と実行
ここまで準備が整ったら次はコンテナの元となるイメージを作成していきます。

```bash
$ docker compose build
```

`docker compose build` で 、 `docker-compose.yml` を元にイメージを作成します。

```bash
$ docker compose up
```
イメージが作成できたら、次に `docker compose up` でコンテナの作成と実行を行います。

```bash
$ docker compose ps
```

コンテナを立ち上げたら `docker compose ps` でコンテナの状態を確認します。 `docker-compose.yml` で設定した３つのコンテナの `STATUS` が `running` になっていればOKです。

コンテナが `running` であることが確認できたら次にデータベースの作成とマイグレーションを行います。

```bash
$ docker compose exec api rails db:create
```

```bash
$ docker compose exec api rails db:migrate
```

最後に `http://localhost:3000`にアクセスして、アプリがローカルと同じように動作すればDocker化が完了です。

# まとめ
RailsとNginxや、ReactとNginxでDocker環境を構築する記事はそれなりに見つけられたものの、この記事と同じような構成で作られたアプリの記事をなかなか見つけることができず苦労しました。なので、自分と同じような構成でアプリケーションを作成しようとしている方の参考になれば幸いです。

拙い文章でしたが最後まで読んでいただき、本当にありがとうございます。

# 参考文献
- [さわって学ぶクラウドインフラ　docker基礎からのコンテナ構築](https://www.nikkeibp.co.jp/atclpubmkt/book/20/279230/)
- [マルチステージビルドの利用](https://matsuand.github.io/docs.docker.jp.onthefly/develop/develop-images/multistage-build/)
- [Nginx + Rails (Puma) on Docker のいくつかの実用パターン](https://qiita.com/na-o-ys/items/1a863419e1f6c3063ace)
- [【docker】db:createすると、Plugin caching_sha2_password could not be loaded...のエラーハマった話](https://qiita.com/tomo-IR/items/224d33f14561e759dd16)
- [nginxについてまとめ(設定編)](https://qiita.com/morrr/items/7c97f0d2e46f7a8ec967)