Ruby on Rails x Docker環境を超シンプルに構築してみた

以前、個人でRailsプロジェクトを作成したことはありましたが、ローカルMacにパッケージやMySQLをインストールしたりと、プロジェクト作成前の面倒な工程の印象がありました。

今では、Dockerを使うとローカルマシーンにインストールすることなく、仮想環境でアプリケーションを作れるので、現時点(2024年4月)の最新バージョンでRailsプロジェクトを作ってみました。

また、なるべくDockerfileやcompose.yml(docker-compose.yml)ファイル内の記述をシンプルに書いています。

# 使用環境・バージョン

- Mac
- Ruby 3.3
- Rails 7.1.3
- MySQL 8.3

# 完成形

後述の、環境構築の進め方記載の2ステップで進めた結果、完成形のディレクトリ構成、Dockerfile、compose.ymlは以下になります。

<details><summary><b>ディレクトリ構成<b></summary><div>

```sh
|- service # Railsプロジェクトを格納するディレクトリ
|- db_data # MySQLのボリュームディレクトリ
|- compose.yml
|- Dockerfile
|- Gemfile # Railsのパッケージバージョンを記載
|- Gemfile.lock
```
</div></details>

<details><summary><b>Dockerfile<b></summary><div>

```sh
FROM ruby:3.3

WORKDIR /service

COPY Gemfile* /service/

RUN bundle install

EXPOSE 3000

CMD ["rails", "server", "-b", "0.0.0.0"]
```
</div></details>

<details><summary><b>compose.yml<b></summary><div>

```yml
version: '3.8'

services:
  web:
    build: .
    environment:
      MYSQL_HOST: db
      MYSQL_USER: root
      MYSQL_PASSWORD: password
    ports:
      - '3000:3000'
    volumes:
      - type: bind
        source: ./service
        target: /service
    depends_on:
      - db

  db:
    image: mysql:8.3
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
    volumes:
      - type: bind
        source: ./db_data
        target: /var/lib/mysql
```
</div></details>

ディレクトリは、他の記事ではDockerfile/compose.ymlと同階層にプロジェクトファイルが混在するのを見かけたりしましたが、個人的に整理しておきたいなと考え、Docker関連のファイルと同階層に、プロジェクトディレクトリを配置するようにしています。

# 環境構築の進め方

## ステップ1: コンテナセットアップ・Railsプロジェクト作成

### 準備
以下のディレクトリ構成、Dockerfile、compose.yml、Gemfileファイルの中身を以下のようにします。

__ディレクトリ構成__
```sh
|- service # 空ディレクトリを用意
|- compose.yml
|- Dockerfile
|- Gemfile
```

__Gemfile__
```sh
source 'https://rubygems.org'
gem 'rails', '~> 7.1.3'
```

__Dockerfile__
```sh
FROM ruby:3.3

WORKDIR /service

# GemfileとGemfile.lockファイルを
# イメージのserviceディレクトリ内にコピー
COPY Gemfile* /service/

RUN bundle install

EXPOSE 3000

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

__compose.yml__
```yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - '3000:3000'
    volumes:
      - type: bind
        source: ./service
        target: /service
```

compose.ymlのvolumesは、他の記事では1行に書いているのを見ますが、[docker-compose の bind mount を1行で書くな](https://zenn.dev/sarisia/articles/0c1db052d09921)という記事を参考に、long syntaxで書いています。シンプルな記述ならshort syntaxが良いのかもしれないけど、このくらいならシンプルの範疇だし厄介ごとはできれば減らしたいという意図でした。

### 手順

1. `docker compose build` でDockerfileのイメージをビルド
2. `docker compose run web rails new . --database=mysql` のコマンドを実行して、serviceディレクトリ配下をRailsプロジェクトにする
3. serviceディレクトリ内にあるGemfile, Gemfile.lockファイルをserviceディレクトリと同じ階層にコピー *元々あるGemfileをそのまま上書きする
4. 再度 `docker compose build` を実行
5. `docker compose up` でコンテナを起動

ここまで終わったら、webコンテナが起動した状態で、ブラウザから http://localhost:3000 のURLでアクセスすると、以下の画面が表示されます。

![error_mysql.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3572572/7393720b-d50b-ae18-9e27-f47c90b4360b.png)

<details><summary>エラーメッセージ</summary><div>

```sh
ActiveRecord::ConnectionNotEstablished
Can't connect to local server through socket '/run/mysqld/mysqld.sock' (2)
```
</div></details>

Railsプロジェクトは起動したのですが、MySQLのデータベース環境を準備する必要があります。

## ステップ2: MySQL環境のセットアップ・データベースの接続設定

ステップ1でコンテナを起動していたら、`docker compose down` コマンドで一度コンテナを終了しましょう。

### 準備
ステップ1から、以下のように変更します。

__ディレクトリ構成__
```sh
|- service
|- db_data # 空ディレクトリを用意、DBのマウントに使用
|- compose.yml
|- Dockerfile
|- Gemfile
|- Gemfile.lock # ステップ1で追加された
```

__compose.yml__
```yml
version: '3.8'

services:
  web:
    build: .
    ports:
      - '3000:3000'
    # ステップ2で追記、database.ymlで参照するため
    environment:
      MYSQL_HOST: db
      MYSQL_USER: root
      MYSQL_PASSWORD: password
    volumes:
      - type: bind
        source: ./service
        target: /service
    # ステップ2で追記、dbサービスと依存関係があり指定
    depends_on:
      - db

  # ステップ2で追記、MySQLのイメージを使用
  db:
    image: mysql:8.3
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3306:3306'
    volumes:
      - type: bind
        source: ./db_data
        target: /var/lib/mysql
```

また、serviceディレクトリ内にある `config/database.yml` も該当箇所を以下のように更新します。

__config/database.yml__
```yml
# AS-IS
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

# TO-BE
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV['MYSQL_USER'] %> # 変更
  password: <%= ENV['MYSQL_PASSWORD'] %> #変更
  host: <%= ENV['MYSQL_HOST'] %> # 変更
```

### 手順
1. `docker compose build` で再度イメージをビルド
2. `docker compose up` でコンテナ起動 (*)
3. `docker compose run web rails db:create` で、データベースを作成
4. 再度画面をリロードで読み込み直したら正しく表示されるようになります

![complete.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3572572/d250cf4b-6166-24c1-89ce-b10da079f2de.png)


*この時、http://localhost:3000 にアクセスすると、以下のようなエラー画面が表示されます。
![error_db.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3572572/a5fc03e5-6008-07dd-ae88-b51e127ba9f1.png)
<details><summary>エラーメッセージ</summary><div>

```sh
We could not find your database: service_development. Available database configurations can be found in config/database.yml.
To resolve this error:
- Did you not create the database, or did you delete it? To create the database, run:
bin/rails db:create
- Has the database name changed? Verify that config/database.yml contains the correct database name.
```
</div></details>

# さいごに

RailsとDockerを組み合わせた環境構築の記事をみつつ、自分なりにDocker周りの記述をシンプルにしようと整理してみました。ただ、わたし自身バックエンドの実経験がなく、この手順だと考慮するべきポイントもあるかと思います。

今回は、最初つまづきそうな環境構築にフォーカスして書いてみましたが、
ここからはまったポイントとか、コントローラ、データベース疎通の進め方含め知見がたまったら記事にしていこうと思います。

# 参考記事

- [【爆速】RailsとDockerで環境構築〜立ち上げまで](https://qiita.com/3Zz8Xx/items/3341a795e1c6fa24bf33)
- [docker-compose の bind mount を1行で書くな](https://zenn.dev/sarisia/articles/0c1db052d09921)