【Ruby on Rails】Railsをdocker-composeで動かすときのミニマムな構成

# まえがき

本記事は、**[DMM WEBCAMP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/infratop)** 2日目記事です。

Ruby on Railsを中心に、DWCメンター・卒業生が記事を投稿しておりますので、是非他の記事もご確認ください！

# はじめに

こんにちは、DMM WEBCAMP メンターの @ukwhatn です。

私は業務／プライベートの双方でRailsを書いているのですが、
特にRails7について、Docker(docker compose)で動かすための構成を調べてもあまり情報がなく、頭を抱えたことが何度かあります。

テンプレートリポジトリとしてRyan Williamsさんの[rails7-on-docker](https://github.com/ryanwi/rails7-on-docker)などはありますが、全部盛りすぎて少々使いにくい部分も....

そこで今回は、私が個人的に構築したRails7をDockerで動かすためのテンプレートリポジトリを紹介しつつ、その内容を解説します。

# 結論

作ったのが↓です。

https://github.com/ukwhatn/rails7-template

本記事では、このテンプレートリポジトリから重要な部分をかいつまんで解説します。

# 解説

## Docker関連

まずは「Dockerで動かす」ための部分から見ていきます

### compose.yml

<details><summary>コード</summary>

```yaml
services:
  web:
    build:
      context: .
      dockerfile: ./deployment/dockerfiles/web/Dockerfile
      args:
        RUBY_VERSION: 3.2.2
    image: web-dev
    entrypoint: [ "bin/docker-entrypoint.sh" ]
    command: bash -c "rm -f tmp/pids/server.pid && bin/rails s -p 3000 -b '0.0.0.0'"
    volumes:
      - .:/usr/src/app
      - bundle:/usr/local/bundle
    ports:
      - "127.0.0.1:59998:3000"
    environment:
      - HISTFILE=/usr/src/app/log/.bash_history
      - RAILS_ENV=development
      - DB_HOST=${DEV_DB_HOST}
      - DB_PORT=${DEV_DB_PORT}
      - DB_USER=${DEV_DB_USER}
      - DB_PASSWORD=${DEV_DB_PASSWORD}
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:15.4
    environment:
      - POSTGRES_USER=${DEV_DB_USER}
      - POSTGRES_PASSWORD=${DEV_DB_PASSWORD}
    volumes:
      - pg_data:/var/lib/postgresql/data
    restart: always
    healthcheck:
      test: pg_isready -U postgres
      interval: 2s
      timeout: 5s
      retries: 30

  redis:
    image: redis:7.2.0
    volumes:
      - redis_data:/data
    healthcheck:
      test: redis-cli ping
      interval: 2s
      timeout: 5s
      retries: 30

volumes:
  pg_data:
  redis_data:
  bundle:
```

</details>

↑は正確に言えば`compose.dev.yml`の中身です。
他に`compose.stg.yml`と`compose.prod.yml`がありますが、環境変数名のprefixが違うだけなので割愛します。

このcompose.ymlでは、以下のコンテナが動作します。

- **web**
    - railsアプリ本体が動作するコンテナです
        - Dockerfileは後述
    - `ポイント`
        - build引数としてRUBY_VERSIONを定義し、Dockerfileから参照することでベースイメージのバージョンを切り替えられるようにしています
        - environmentでRAILS_ENVやDB接続情報を渡しており、これを切り替えることでdev/stg/prodを分けています
        - depends_onでpostgresやredisの立ち上がり(`service_healthy`)を待ってからwebコンテナを上げています
- **db**
    - みんな大好きpostgresのオフィシャルイメージです
    - `ポイント`
        - environmentで渡している`POSTGRES_USER`と`POSTGRES_PASSWORD`はrailsに渡しているものと共通です
        - `pg_isready -U postgres`でhealthcheckを行うことで、`service_healthy`コンディションによるdepends_onを有効化しています
- **redis**
    - セッションストアとして利用するためのredisコンテナです
    - `ポイント`
        - `redis-cli ping`でhealthcheckを行うことで、`service_healthy`コンディションによるdepends_onを有効化しています


#### Dockerfile(web)

`deployment/dockerfiles/web/Dockerfile` にあります

<details><summary>コード</summary>


```dockerfile
ARG RUBY_VERSION

FROM ruby:${RUBY_VERSION}-slim

# OS Level Dependencies
RUN --mount=type=cache,target=/var/cache/apt \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  --mount=type=tmpfs,target=/var/log \
  rm -f /etc/apt/apt.conf.d/docker-clean; \
  echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache; \
  apt-get update -qq \
  && apt-get install -yq --no-install-recommends \
    build-essential \
    gnupg2 \
    less \
    git \
    libpq-dev \
    postgresql-client \
    libvips \
    curl

ENV LANG=C.UTF-8 \
  BUNDLE_JOBS=4 \
  BUNDLE_RETRY=3

RUN gem update --system && gem install bundler

WORKDIR /usr/src/app

ENTRYPOINT ["./bin/docker-entrypoint.sh"]

EXPOSE 3000
```
</details>

あんまり複雑なことはしてないです。

DBをmysqlとかに変えたい場合はここで`libpq-dev`や`postgresql-client`のかわりに適切なものをinstallしてください。


### docker-entrypoint.sh

`bin/docker-entrypoint.sh`にあります。
compose.ymlの`entrypoint`に設定しているものです。

<details><summary>コード</summary>

```sh
#!/bin/bash
set -e

echo -e "----------------------\nInitializing Rails App\nMODE: $RAILS_ENV\n----------------------\n"

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

echo "rails: installing gems..."
bundle check || bundle install --jobs 4

echo "rails: creating database..."
bundle exec rake db:create
echo "rails: migrating database..."
bundle exec rake db:migrate

# productionモードの場合はassetsをコンパイルする
if [ "$RAILS_ENV" = 'production' ]; then
  echo "rails: precompiling assets..."
  bundle exec rake assets:precompile
fi

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


やっていることは単純で、
1. server.pidの削除
1. bundle install
1. dbのマイグレーション
1. アセットプリコンパイル(prodモード時のみ)

だけです。
コンテナ作成時にやらせたい処理があったらここに追記しましょう。

---

Docker周りで大事なのはこのくらいです
総じてあまり複雑なことはしていないので、必要に応じて書き換えつつ利用してください。


## Rails関連

ここからは、中で動くRailsの設定ファイル等を見ていきます。


### Gemfile

<details><summary>コード</summary>

```rb
# frozen_string_literal: true

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.2.2"

# Rails 7
gem "rails", "~> 7.0.7", ">= 7.0.7.2"

# For asset pipeline
gem "sprockets-rails"

# Postgresql driver
gem "pg", "~> 1.1"

# Web server
gem "puma", "~> 6.3"

# Use import maps
gem "importmap-rails"

# Use Turbo
gem "turbo-rails"

# Stimulus Framework
gem "stimulus-rails"

# JSON Builder
gem "jbuilder"

# Redis
gem "redis", "~> 4.8.1", "< 5"
gem "redis-rails"

# Create password hash
# gem "bcrypt", "~> 3.1.7"

# Timezone for Windows
gem "tzinfo-data", platforms: %i[mingw mswin x64_mingw jruby]

# Reduces boot times through caching
gem "bootsnap", require: false

# Use Sass to process CSS
gem "dartsass-rails"
gem "sassc-rails"

# Use Active Storage variants
gem "image_processing", "~> 1.2"

# for localize
gem "i18n_generators"
gem "rails-i18n"

# for read .env
# gem "dotenv-rails"

# for discord oauth2
# gem "omniauth-discord"

# for google oauth2
# gem "omniauth-google-oauth2"

# for github oauth2
# gem "omniauth-github"

# csrf protection for omniauth
# gem "omniauth-rails_csrf_protection"

group :development, :test do
  # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem
  gem "brakeman"
  gem "bundler-audit"
  gem "debug", "1.8.0", platforms: %i[mri mingw x64_mingw]
  gem "rspec-rails"
  gem "rubocop"
  gem "rubocop-performance"
  gem "rubocop-rails"
  gem "rubocop-rspec"
end

group :development do
  # Use console on exceptions pages [https://github.com/rails/web-console]
  gem "web-console"

  # Add speed badges [https://github.com/MiniProfiler/rack-mini-profiler]
  gem "rack-mini-profiler"

  # Speed up commands on slow machines / big apps [https://github.com/rails/spring]
  # gem "spring"
end

group :test do
  # Use system testing [https://guides.rubyonrails.org/testing.html#system-testing]
  gem "capybara"
  gem "selenium-webdriver"
  gem "webdrivers"
end
```
</details>

ここもテンプレートから逸脱したことはあまりしていません。
OAuth2を使いたい場合は`omniauth`系のGemのコメントアウトを外してください。

### .rubocop.yml

<details><summary>コード</summary>

```yml
require:
  - rubocop-rails
  - rubocop-rspec
  - rubocop-capybara
  - rubocop-performance

AllCops:
  TargetRubyVersion: 3.2.2
  TargetRailsVersion: 7.0.1
  DisabledByDefault: true
  DisplayCopNames: true
  NewCops: enable
  Exclude:
    - "bin/**/*"
    - "vendor/**/*"
    - "public/**/*"
    - "node_modules/**/*"
    - "db/schema.rb"

# ----- bundler -----
Bundler:
  Enabled: true

# ----- layout -----

Layout:
  Enabled: true

# ----- lint -----

Lint:
  Enabled: true

Lint/EmptyBlock:
  Enabled: false
  Exclude:
    - "config/routes.rb"

# ----- naming -----

Naming:
  Enabled: true

# ----- security -----

Security:
  Enabled: true

# ----- performance -----

Performance:
  Enabled: true

# ----- style -----

# ブロックの終了部分が適切にスタイル指定されているか
Style/EndBlock:
  Enabled: true
# ハッシュの構文が適切か
Style/HashSyntax:
  Enabled: true
# 文字列リテラルが適切にスタイル指定されているか
Style/StringLiterals:
  Enabled: true
  EnforcedStyle: "double_quotes"
  Exclude:
    - "config/**/*.rb"
    - Rakefile
# caseの等価性が適切にチェックされているか
Style/CaseEquality:
  Enabled: true
# クラスメソッドが適切に定義されているか
Style/ClassMethods:
  Enabled: true
# クラス変数が適切に使用されているか
Style/ClassVars:
  Enabled: true
# collectionメソッドが適切に使用されているか
Style/CollectionMethods:
  Enabled: true
  PreferredMethods:
    collect: "map"
    collect!: "map!"
    inject: "reduce"
    detect: "find"
    find_all: "select"
```
</details>

テンプレートリポジトリなので、rubocopは必要な部分だけtrueにしています。
特に`Style/*`については、用途に応じて増やしたり減らしたりしてください。

### session_store.rb

`config/initializers/session_store.rb`にあります

<details><summary>コード</summary>

```ruby
secure = Rails.env.production?
key = Rails.env.production? ? "_app_session" : "_app_session_#{Rails.env}"
domain = ENV.fetch("APP_DOMAIN", "localhost")

Rails.application.config.session_store :redis_store,
                                       servers: %w(redis://redis:6379/0/session),
                                       expire_after: 90.minutes,
                                       key:,
                                       domain:,
                                       secure:,
                                       httponly: true,
                                       threadsafe: true
                                    
```
</details>

設定値は [redis-railsの公式README](https://github.com/redis-store/redis-rails)を確認してください。
多分変える必要があるのは`expire_after`くらいだと思います。

----

Rails周りはこのくらいです。
必要に応じてカスタムしてください。

## その他

### Makefile

dockerコマンド打つのめんどくさいので、Makefileにまとめました

<details><summary>コード</summary>

```make
.PHONY: test build-dev

DIR_NAME := $(shell basename $(CURDIR))

init:
	cp .env.example .env

dev:
	make build-dev
	make up-dev
	make log-dev

build-dev:
	docker compose -f compose.dev.yml build

up-dev:
	docker compose -f compose.dev.yml up -d

down-dev:
	docker compose -f compose.dev.yml down

clean-dev:
	docker compose -f compose.dev.yml down --rmi all --volumes --remove-orphans

log-dev:
	docker compose -f compose.dev.yml logs -f

prod:
	make build-prod
	make up-prod
	make log-prod

build-prod:
	docker compose -f compose.prod.yml build

up-prod:
	docker compose -f compose.prod.yml up -d

down-prod:
	docker compose -f compose.prod.yml down

log-prod:
	docker compose -f compose.prod.yml logs -f

redis-cli:
	docker compose -f compose.dev.yml exec redis redis-cli
 ```
</details>

### .env.example

DEV/TEST/PRODで使い分けられるようにしています

<details><summary>コード</summary>

```
# -- DEVELOPMENT --
DEV_DB_HOST="db"
DEV_DB_PORT="5432"
DEV_DB_USER="postgres"
DEV_DB_PASSWORD="changeme"

# -- STAGING --
STG_DB_HOST="db"
STG_DB_PORT="5432"
STG_DB_USER="postgres"
STG_DB_PASSWORD="changeme"

# -- TEST --
TEST_DB_HOST="db"
TEST_DB_PORT="5432"
TEST_DB_USER="postgres"
TEST_DB_PASSWORD="changeme"

# -- PRODUCTION --
PROD_DB_HOST="db"
PROD_DB_PORT="5432"
PROD_DB_USER="postgres"
PROD_DB_PASSWORD="changeme"
```
</details>

### dependabot.yml

`.github/depandabot.yml`にあります

<details><summary>コード</summary>

```yaml
version: 2
updates:
- package-ecosystem: docker
  directory: "/"
  schedule:
    interval: daily
- package-ecosystem: bundler
  directory: "/"
  schedule:
    interval: daily
  ignore:
    - dependency-name: "rails"
      update-types: ["version-update:semver-major", "version-update:semver-minor"]
- package-ecosystem: github-actions
  directory: "/"
  schedule:
    interval: daily
```
</details>

Gemfileを読みに行って、アップデートがあればPRを作ってくれます。
便利なので有効化しておきましょう。

---

以上です。

割と雑に作ったテンプレートなので、気になる部分があればコメントやPRでご指摘ください。

[アドベントカレンダー](https://qiita.com/advent-calendar/2023/infratop)は明日からも投稿が続きますので、お楽しみに！