Rails7 Mysql8 React TypeScriptで作る Docker環境

## はじめに
近しい記事はあれど、欲しい環境の記事を見つけられなかった
妥協して近しい記事をコピペで使おうとしたらエラーで動かなかった。
この際勉強にもなるし、自分で組んでみようということで作成しました。

## 前提
M1 Mac
基本的なDockerコマンドはわかる
エラーでハマった箇所を中心に少しだけ説明も入れる

## 求めた条件
Rails7 API
Mysql8 (認証プラグインを旧バージョンに変更)
React TypeScriptを使用する
`backend` `frontend` `docker-compose.yml`のシンプルな構成
`docker compose run <サービス> /bin/sh`等でコンテナに入って直接作業するのはなし
最近発生しているbundler特有のエラーを抑制する記載もいれたい


## 構成

```
backend
 - Dockerfile
frontend
 - Dockefile
docker-compose.yml
```

### バックエンド
*Dockerfile*
```Dockerfile
FROM ruby:3.1
ARG RUBYGEMS_VERSION=3.3.20

# WORKDIRは指定したディレクトリがなければ自動で作成する
WORKDIR /backend
COPY Gemfile* /backend/

# Bundlerのversionが起因してエラーが発生するのを防いでいる (結構苦労した)
# 過去には問題なく動いていた環境で急に動かなくなった人もいたみたい
RUN gem update --system ${RUBYGEMS_VERSION} && \
    bundle install
COPY . /backend

# rails特有のバグを避けるために必要
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
```

```Dockerfile:frontent/Dockerfile
FROM node:18.7.0-alpine3.16
WORKDIR /frontend
RUN npm i -g create-react-app
```
*Gemfile*
```Gemfile
source 'https://rubygems.org'
gem 'rails', '~>7.0.3'
```
*Gemfile.lock*
```shell
# 空で良い
```
*entrypoint.sh*
```shell
#!/bin/bash
set -e

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

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"
```

### フロントエンド
*Dockerfile*
```Dockerfile
FROM node:18.7.0-alpine3.16
WORKDIR /frontend

# create-react-appのinstallはdocker compose run時にしても良いが
# 今回はImageの時点で入れている
# ここは変更してもよさそう
RUN npm i -g create-react-app
```

## docker-compose.yml
```yml
version: '3.8'
services:
  db:
    image: mysql:8.0.29
    command: --default-authentication-plugin=mysql_native_password
    volumes:
      - ./tmp/db:/var/lib/mysql
    environment:
      MYSQL_ROOT_PASSWORD: root
    ports:
      - "3306:3306"

  backend:
    build: ./backend/
    volumes:
      - ./backend:/backend
    ports:
      - "3001:3000"
    depends_on:
      - db

  frontend:
    build: ./frontend/
    volumes:
      - ./frontend:/frontend
    command: sh -c "npm start"
    ports:
      - "3000:3000"
```

## コマンド

### バックエンド
```shell
# コンテナ内で railsアプリを作成
docker compose run --rm backend rails new . --force --no-deps -d mysql --api
```

```yml
# config/database.ymlを編集してコンテナのmysqlに接続する
password: root
host: db
```
```shell
# Gemfileが更新されたので再度buildする
docker compose build

# DBを作成
docker compose run --rm backend rails db:create
```

### フロントエンド

```shell
# Dockerfileをrootに移す アプリを作成 Dockerfileを元に戻す作業を行なっている
# create-react-app . する際余計なファイルが存在すると作成してくれないのでこの形にしている(苦労した)
docker compose run --rm frontend sh -c "mv Dockerfile .. && npx create-react-app . && mv /Dockerfile /frontend/Dockerfile"
```

### 全てのコンテナを立ち上げる
```shell
docker compose up -d
```

*`http://localhost:3000`* → `rails`
*`http://localhost:3001`* → `react`

## 知識系 小ネタ
`docker comose run <サービス> <コマンド>`で複数のコマンドを実行したい時
`sh -c "A && B"`とする必要がある
`A && B`とすると2個目以降はホストで実行されてしまう。

`yml`に`command`が設定されている場合
`docker compose run`や`docker compose up`では`Dockerfile`の`CMD`は無視される
`docker container <Image>`名で起動する場合は`CMD`
`docker compose run <コマンド>`とする場合は設定したコマンドを優先する

## ちょっと脱線
最近はバックエンドとフロントエンドのリポジトリを分けて開発することも多いようです。
バックエンドのAPIだけDocker化して別々のプロジェクトとして連携したほうがCI/CDの構築が簡単
コミットにバックエンドとフロントエンドが混ざることがなくなる。等利点があるようですので
どちらを利用するか考えないとですね。
自分はこの環境を作っている際に知ったのですが、最後まで完成させたくて時間を使ってしまった・・・

## 参考
今回は10サイト以上見ましたが、その中でも特に参考にしたものを貼っておきます。

https://qiita.com/Shuhei_Nakada/items/b59964e56482ac911522

https://subaru-hello.hateblo.jp/entry/2022/01/07/231912

https://qiita.com/P-man_Brown/items/32fdba14e88219f8d2f0