【個人開発】Rails API × React アプリの環境を構築しました

## はじめに
こんにちは！[MaTTa](https://x.com/RckLVnPtRv61824)と申します。プログラミングスクールRunteq50期生です。先日、生成AIを用いた習慣化支援RPGアプリ「3日目に魔王がいる」をMVPリリースしました。その技術要素を細かく切り出して順に備忘録として残していこうと思います。

今回は最初の行程である**環境構築**についてです。Tailswind CSSの導入もおまけで実施します。

## 参考
### アプリ紹介記事
https://qiita.com/MaTTA_RUNTEQ50B/items/c8eac9251f704164dc9c

### サービスURL
https://mao-the-3rd-day-front-e8eb3156d3f2.herokuapp.com/

### Githubリポジトリ
https://github.com/MaTTalv001/MAO_the_3rd_Day

# 本編ここから
## 作業環境
- MacBook Air 2020 (Apple M1)
- macOS Sonoma 14.4.1

## 構成
このアプリでは、バックエンドにRails（APIモード）、フロントエンドにReactを採用しており、それぞれ個別に開発環境を構築しています。つまりアプリが二つあるような状態です。


![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f313138323338302f37663633383033622d643234332d383134622d306336392d3164663761376139636165662e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/2444679d-c57a-7c70-2039-7a02092050c4.png)

一方で、開発の管理は楽したいので、リポジトリとしては一つの統合アプリとして管理しています。

```txt:ディレクトリ構成
app
├backend/
│
├frontend/
│
└docker-compose.yml

# これらをまとめてリポジトリとする
```

## バックエンド側の環境構築
Rails APIのDocker開発環境を構築します。リポジトリのルートディレクトリから操作を開始します。ファイル操作はGUIで行っても問題ありません。

```bash
touch docker-compose.yml
```
touch docker-compose.ymlに下記のように記述します。後にフロントエンド部分も追記するのですが、先に書くとエラーを起こすのでひとまずバックエンドのみです。なお、データベースはMySQLを用いています。
```yml:docker-compose.yml
version: "3"
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
  back:
    build:
      context: ./backend
      dockerfile: Dockerfile
    env_file:
      - ./backend/.env.local
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - ./backend:/app
    ports:
      - "3000:3000"
    depends_on:
      - db
    tty: true
    stdin_open: true
volumes:
  mysql_data:

```
<details><summary>補足</summary>


| 項目 | 説明 |
|------|------|
| db:`volumes:` | コンテナ内のデータを永続化するためのボリューム。`mysql_data`というボリュームが`/var/lib/mysql`にマウントされます。 |
| `build:` | アプリケーションのビルド設定を指定します。<br>- `context: ./backend`: ビルドコンテキスト（Dockerfileの場所）を指定します。|
| `env_file:` | 環境変数をファイルから読み込むことを指定します。ここでは、`./backend/.env.local`ファイルが使用されます。 |
| `command:` | コンテナ起動時に実行されるコマンドを指定します。ここでは、`server.pid`ファイルを削除し、Railsサーバーを起動します。Railsは `server.pid`ファイルを使用してサーバープロセスのPIDを保存します。古いPIDファイルが存在すると、サーバーの起動に問題が発生する可能性があるため、起動前にこのファイルを削除しています。|
| db:`volumes:` | `./backend`ディレクトリがコンテナ内の`/app`ディレクトリにマウントされます。 |
| `depends_on:` | サービス間の依存関係を定義します。`back`サービスは`db`サービスに依存しているため、`db`サービスが起動してから`back`サービスが起動します。 |
| `tty: true`と`stdin_open: true` | これらのオプションは、コンテナとのインタラクティブなセッションを可能にします。 |
| `volumes:` | 最後の`volumes:`セクションでは、名前付きボリューム（Docker によって管理される永続的なデータ保存領域）を定義します。ここでは、`mysql_data`というボリュームが定義されています。 |

</details>

バックエンドのワークスペースを作成し移動。必要な空ファイルを作成する。
```bash
mkdir backend
cd backend
touch Dockerfile Gemfile Gemfile.lock entrypoint.sh .env.local
```

Dockerfileに下記の通り記述します。

:::note warn
注意
一行目の`--platform=linux/amd64`について、M1以降のARMアーキテクチャを採用しているMacの場合に必要です。それ以外の開発環境の場合は削除してください。
:::

```:Dockerfile
FROM --platform=linux/amd64 ruby:3.2.2
RUN apt-get update -qq && apt-get install -y nodejs default-mysql-client

WORKDIR /app

COPY Gemfile /app/Gemfile
COPY Gemfile.lock /app/Gemfile.lock

RUN gem install bundler
RUN bundle install

COPY . /app

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

EXPOSE 3002

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

<details><summary>補足</summary>

| Dockerfile命令 | 説明 |
|---------------|------|
| `FROM --platform=linux/amd64 ruby:3.2.2` | このイメージは、Ruby 3.2.2を使用します。`--platform=linux/amd64`は、プラットフォームを指定し、Dockerイメージが特定のアーキテクチャ（この場合はAMD64）で構築されることを保証します。 |
| `RUN apt-get update -qq && apt-get install -y nodejs default-mysql-client` | `apt-get update`を実行して、パッケージリストを更新します。`apt-get install`を使用して、NodeJSとMySQLクライアントをインストールします。これらのパッケージは、Railsアプリケーションの実行に必要です。 |
| `WORKDIR /app` | コンテナ内の作業ディレクトリを`/app`に設定します。 |
| `COPY Gemfile /app/Gemfile` <br> `COPY Gemfile.lock /app/Gemfile.lock` | `Gemfile`と`Gemfile.lock`をコンテナ内の`/app`ディレクトリにコピーします。これらのファイルは、アプリケーションの依存関係を定義します。 |
| `RUN gem install bundler` <br> `RUN bundle install` | `bundler`をインストールします。`bundle install`を実行して、GemfileとGemfile.lockに基づいて依存関係をインストールします。 |
| `COPY . /app` | カレントディレクトリ（`.`）の内容全体をコンテナ内の`/app`ディレクトリにコピーします。これにより、アプリケーションのソースコードがコンテナ内に配置されます。 |
| `COPY entrypoint.sh /usr/bin/` <br> `RUN chmod +x /usr/bin/entrypoint.sh` <br> `ENTRYPOINT ["entrypoint.sh"]` | `entrypoint.sh`ファイルを`/usr/bin/`ディレクトリにコピーします。`chmod +x`を使用して、`entrypoint.sh`ファイルに実行権限を付与します。`ENTRYPOINT`命令を使用して、コンテナの起動時に`entrypoint.sh`スクリプトが実行されるように設定します。 |
| `CMD ["rails", "server", "-b", "0.0.0.0"]` | `CMD`命令を使用して、コンテナの起動時に実行されるデフォルトのコマンドを指定します。この場合、`rails server -b 0.0.0.0`が実行され、Railsサーバーがすべてのネットワークインターフェイス（`0.0.0.0`）で起動します。 |


</details>

続いてentrypoint.shに下記のように記述します。`entrypoint.sh`は、Dockerコンテナが起動する際に実行されるシェルスクリプトです。このスクリプトは、コンテナの起動プロセスをカスタマイズするために使用されます。
```:entrypoint.sh
#!/bin/bash
set -e

rm -f /app/tmp/pids/server.pid

exec "$@"
```
<details><summary>補足</summary>

| スクリプトの行 | 説明 |
|---------------|------|
| `#!/bin/bash` | スクリプトがBashシェルを使用して実行されることを指定します。 |
| `set -e` | スクリプト内のコマンドがエラーを返した場合、スクリプトの実行を即座に終了するように指示します。 |
| `rm -f /app/tmp/pids/server.pid` | 古いRailsサーバーのPIDファイルを削除します。 |
| `exec "$@"` | スクリプトに渡された引数を使用して新しいプロセスを実行し、現在のシェルプロセスを置き換えます。これにより、コンテナのメインプロセスがスクリプトから実行されたプロセスになります。 |

</details>

さらに`Gemfile`にRailsのインストールについて記述します。
```Gemfile
source 'https://rubygems.org'
gem 'rails', '~> 7.1.2'
```

`Gemfile.lock`, `.env.local`ファイルは空のままで構いません。

#### Railsアプリを作成する
ターミナルで下記の通り実行します。
```bash
docker-compose run back rails new . --force --database=mysql --skip-docker --api
```
<details><summary>補足</summary>
- --skip-docker: rails new時にDockerfileが自動作成されるのですが今回は不要なのでskip
- --api : RailsをAPIモードで立ち上げます
</details>

Railsアプリのファイルがインストールされたら`config/database.yml`を一部修正します
```config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  # 下記2 行を修正
  password: password
  host: db
```

準備が整ったのでコンテナを立ち上げます。
```bash
docker compose build
docker compose up
```



データベースの作成
```bash
docker compose exec back bash
rails db:create 
```

:::note info
アプリが起動できたら`http://localhost:3000/`にアクセスして確認します
:::

![スクリーンショット 2024-06-03 22.01.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/f38a7c95-bb55-4a6d-7301-af26b97791e0.png)


#### .gitフォルダの削除
`rails new`時に`.gitフォルダ`がbackend内に自動作成されるのですが、親ディレクトリでバック・フロント一括git管理しようとしている今回のような場合、backendにも.gitがあるとサブモジュールという扱いになり扱いが面倒になります。backendフォルダ内の.gitフォルダを削除しましょう。
```bash
rm -rf backend/.git
```

#### その他重要なファイルの格納
`.env.local`と`.gitignore`はbackendフォルダ内に移す(ない場合は作成)。
`.gitignore`には`.env.local`の記述を忘れないようにする。

## フロントエンド側の環境構築
ReactのDocker開発環境を構築します。`create-react-app`でfrontendディレクトリとReact初期ファイル類を揃えることから開始します。

```
# コンテナのルートで操作します。　backendにいるときは　cd ../
npx create-react-app frontend
```
<details><summary>補足</summary>

ここで、npxは、npm（Node Package Manager）によって提供されるコマンドランナーで、ローカルもしくはリモートのnpmパッケージを実行するために使用されます。create-react-appは、Reactアプリケーションのプロジェクトを素早くセットアップするためのツールです。
</details>

次に、ワークスペースに移動して必要な空ファイルを作成します。
```
cd frontend
touch Dockerfile yarn.lock
```
frontend/Dockerfileに下記のように記述します。
```:frontend/Dockerfile
FROM --platform=linux/amd64 node:21.5.0
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
RUN yarn build
RUN yarn global add serve
EXPOSE 3000
CMD sh -c 'serve -s build -l tcp://0.0.0.0:$PORT'
```

<details><summary>それぞれの意味</summary>

| コマンド                           | 説明                                                                                  |
|------------------------------------|---------------------------------------------------------------------------------------|
| `FROM --platform=linux/amd64 node:21.5.0` | ベースイメージとしてNode.jsのバージョン21.5.0を使用し、amd64プラットフォームで動作するように指定します。 |
| `WORKDIR /app`                     | コンテナ内の作業ディレクトリを`/app`に設定します。                                       |
| `COPY package.json yarn.lock ./`   | `package.json`と`yarn.lock`をコンテナの作業ディレクトリにコピーします。                       |
| `RUN yarn install`                 | 必要なパッケージをインストールします。                                                  |
| `COPY . .`                         | 現在のディレクトリのすべてのファイルをコンテナの作業ディレクトリにコピーします。                  |
| `RUN yarn build`                   | アプリケーションをビルドします。                                                       |
| `RUN yarn global add serve`        | `serve`パッケージをグローバルにインストールします。                                       |
| `EXPOSE 3000`                      | コンテナがリッスンするポート3000を公開します。                                            |
| `CMD sh -c 'serve -s build -l tcp://0.0.0.0:$PORT'` | `serve`コマンドを使用してビルドされたアプリケーションを起動し、指定されたポートでリッスンします。 |

</details>


ルートディレクトリの`docker-compose.yml`にフロントエンド分を追記します
```yml:docker-compose.yml
version: "3"
services:
  db:
    image: mysql:8.0
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - "3306:3306"
    volumes:
      - mysql_data:/var/lib/mysql
  back:
    build:
      context: ./backend
      dockerfile: Dockerfile
    env_file:
      - ./backend/.env.local
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - ./backend:/app
    ports:
      - "3000:3000"
    depends_on:
      - db
    tty: true
    stdin_open: true
  # ここから追記
  front:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    volumes:
      - ./frontend:/app
    command: yarn start
    ports:
      - "8000:3000"
  # ここまで追記
volumes:
  mysql_data:

```

コンテナを立ち上げます
```bash
docker-compose up --build
```

:::note info
アプリが起動できたらlocalhost:8000にアクセスして動作確認します。
:::

![スクリーンショット 2024-06-03 22.06.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1182380/2adad14e-68cd-d25b-e205-e84be487a438.png)



#### その他重要なファイルの格納
こちらも`.env.local`と`.gitignore`はfrontendフォルダ内に移す(ない場合は作成)。
`.gitignore`には`.env.local`の記述を忘れないようにしましょう。


## CORS対応
Rails APIとReactの組み合わせだとCORSによるエラーが発生するのであらかじめその対応をとっておきます。
<details><summary>CORSとは？</summary>
<strong>CORS（Cross-Origin Resource Sharing)</strong>は、ウェブブラウザが異なるオリジン間でリソースを共有する際のセキュリティメカニズムです。デフォルトでは、ブラウザはセキュリティ上の理由から、スクリプトが異なるオリジンのリソースにアクセスすることを禁止しています。CORSは、サーバーがブラウザに対して特定のオリジンからのリクエストを許可するための仕組みです。

Rails APIとReactのフロントエンドが異なるオリジンにホストされている場合、CORSの設定が必要です。例えば、Rails APIがhttp://api.example.comにあり、Reactアプリがhttp://frontend.example.comにある場合、ReactアプリからRails APIにリクエストを送ると、CORSエラーが発生する可能性があります。
</details>

`Gemfile`に下記記述します。
```:Gemfile
gem "rack-cors"
```
```bash
# docker compose exec back bash
bunsle install
```

Railsの`config/initializers/cors.rb`に下記のとおり記述することでリクエスト元を明示的に許可するようにします。
```ruby:config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "localhost:8000", "https://www.本番環境のURLがあるならここにも追加"

    resource "*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

```

## (おまけ)Tailswind CSSとDaisyUIを使えるようにする
React側でデザインのハンドリング性を高めるためにTailwindとDaisyUIは自分的には欠かせない存在なので、環境構築の段階でインストールしてしまいます。

公式

https://tailwindcss.com/

https://daisyui.com/

フロントのコンテナに入りインストールします。
```bash
docker compose exec front bash
yarn add tailwindcss@^3
yarn add postcss autoprefixer -D
npx tailwindcss init
yarn add -D daisyui@latest
```

`frontend/src/index.css`にTailwindを記述します。先頭に追記する形で構いません。
```CSS:frontend/src/index.css
@tailwind base;
@tailwind components;
@tailwind utilities;
```
`tailwind.config.js` ファイルを開き、content セクションを編集して、Tailwindがスタイルを適用するファイルを指定します。また、DaisyUIプラグインを有効にします。


```js:tailwind.config.js
module.exports = {
  content: [
    "./src/**/*.{js,jsx,ts,tsx}", // すべてのJS、JSX、TS、TSXファイルをスキャン
  ],
  future: {
  },
  purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [require("daisyui")],
  daisyui: {
    themes: ["light", "dark", "cupcake", "aqua", "pastel"],
  },
};
```

最後のほうにある下記はDaisyUIのテンプレートテーマです。任意ですが使えるようにしておくと非常に便利です。

```js
daisyui: {
    themes: ["light", "dark", "cupcake", "aqua", "pastel"],
  },
```
DaisyUIのテーマ

https://daisyui.com/docs/themes/

`frontend`直下に`postcss.config.js`を作成し、以下の内容を追加します。
```js:postcss.config.js
module.exports = {
    plugins: [require("tailwindcss"), require("autoprefixer")],
};
```
<details><summary>PostCSS</summary>
CSSを解析し、プラグインを通じて変換するツールです。多くの機能を持つプラグインを利用することで、CSSの構文チェックや最適化、新しいCSS仕様のサポートなどが行えます。

pluginsプロパティ
pluginsプロパティは、PostCSSが使用するプラグインを指定します。この例では、tailwindcssとautoprefixerの2つのプラグインを使用しています。

require("tailwindcss")
Tailwind CSSは、ユーティリティファーストのCSSフレームワークです。これにより、簡単にレスポンシブデザインやカスタマイズが可能なCSSクラスを使うことができます。require("tailwindcss")は、Tailwind CSSのプラグインを読み込み、PostCSSで使用できるようにします。

require("autoprefixer")
Autoprefixerは、ベンダープレフィックスを自動的に追加するPostCSSのプラグインです。これにより、異なるブラウザ間でのCSSの互換性を確保できます。require("autoprefixer")は、Autoprefixerのプラグインを読み込み、PostCSSで使用できるようにします。
</details>


dockerを再ビルドすればTailwind CSS, DaisyUIが使えるようになります。
```bash
docker compose build
docker compose up
```

環境構築は以上です。

## 締めくくり
Rails APIとReactで構成するDocker環境構築の例でした。個人的にはこの構成で2,3個アプリを作っているので安定感はあるのですが、もっとこうした方がいいといったことがあればぜひコメントでご指摘ください。

今回はここまでです。引き続き、MVPアプリの技術要素を切り出してアウトプットしていきたいと思います。

## この記事を書いた人
友達募集中ですw

https://x.com/RckLVnPtRv61824