パーフェクトRuby on Rails 10章 メモ・雑感(Docker, buildkit, docker-compose, Rollbar)

# 10 コンテナを利用した Rails アプリケーションの運用

## 雑感

この章はRailsアプリケーションのDockerコンテナ環境での運用についての内容でした。すでにDockerは個人開発から業務に至るまで毎日のように使っているものの、本番と開発環境でのソースコードの反映のさせ方やマウントキャッシュなど、ガッツリ理解はできていなかったので改めて理解が深まったように感じています。以下、忘備録となります。

## Dockerfile の作成とコンテナの起動

#### ・Dockerfile

以下の形式のファイルをアプリケーションのルートディレクトリに配置する。
以下は本書の状態から環境変数の記法などを最新へアップデートし`ruby: 2.7.8`用の設定に書き換えています。

```diff:Dockerfile
# Node.jsダウンロード用ビルドステージ
FROM ruby:2.7.8 AS nodejs

WORKDIR /tmp

# Node.jsダウンロード
RUN curl -LO https://nodejs.org/dist/v14.21.3/node-v14.21.3-linux-x64.tar.xz
RUN tar xJf node-v14.21.3-linux-x64.tar.xz
RUN mv node-v14.21.3-linux-x64 node

# Railsプロジェクトインストール
+FROM ruby:2.7.8

# nodejsをインストールしたイメージからnode.jsをコピー
COPY --from=nodejs /tmp/node /opt/node
+# ENVの表記を最新の書き方に変更
+ENV PATH=/opt/node/bin:$PATH

# アプリケーション起動用のユーザを追加
RUN useradd -m -u 1000 rails
RUN mkdir /app && chown rails /app
USER rails

# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
+ENV PATH=/home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH

+# 2.7以上のrubyでrails newを実行するための設定
+# ruby2.7.8との互換性を考慮して2.4.22を指定
+RUN gem install bundler -v 2.4.22

WORKDIR /app

# Dockerのビルドステップでキャッシュを使うためにGemfileを先にコピーしbundle install
# アプリケーションの内容変更時にGemfileの内容に変化がなかった場合はbundle installの工程までキャッシュを利用できる
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/

RUN bundle install
RUN yarn install

# Gemfileに変化がなければここから再ビルドされる
COPY --chown=rails . /app

RUN bin/rails assets:precompile

VOLUME /app/public

# 実行時にコマンドを指定しなかった場合に実行されるコマンド
CMD ["bin/rails", "s", "-b", "0.0.0.0"]
```

#### ・コンテナ起動

以下コマンドで上記ファイルからコンテナイメージを作成する

```
# -t でイメージにタグ(名前)を付ける(下記の場合はmyrailapp)
# . でカレントディレクトリのDockerfileを使用
# Dockerfileとそこから参照される全てのファイルは指定ディレクトリ内にある必要がある
% docker build -t myrailapp .
```

このイメージから以下でコンテナを起動する

```
# -p で、ホストマシンのポスト : コンテナのポスト の形式で転送するポートを指定
% docker run -p 3000:3000 myrailapp
```

## buildkit を利用してイメージビルドを高速化

buildkit のマウントキャッシュを利用することでさらにビルドを高速化することができる。ただし、非標準の機能であるため有効化するにはオプションを指定する必要がある。

マウントキャッシュはイメージビルド中に書き込んだファイルをホストマシン上にキャッシュして保存する。これにより`bundle install`後の状態を保持することができ、ホストマシン上で`bundle install`した場合と同じ体験を維持できる。また、レイヤーキャッシュのみでは難しい`asset:precompile`の実行時間をキャッシュを利用して短縮可能。

#### ・Dockerfile-buildkit

利用するには作成済の Dockerfile を下記の Dockerfile-buildkit へコピーし、先頭と後半を編集する。

```diff:Dockerfile-buildkit
+# マウントキャッシュの利用を有効化するコメント
+# syntax = docker/dockerfile:experimental

# Node.jsダウンロード用ビルドステージ
FROM ruby:2.7.8 AS nodejs

略

# Dockerのビルドステップでキャッシュを使うためにGemfileだけ先にコピー
COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/

+RUN bundle config set app_config .bundle
+RUN bundle config set path .cache/bundle

+# マウントキャッシュはビルドステップが変わると失われる
+# キャッシュ用のディレクトリにbundle installしそこから通常のインストール先にコピー
+RUN --mount=type=cache,uid=1000,target=/app/.cache/bundle bundle install && \
+  mkdir -p vendor && \
+  cp -ar .cache/bundle vendor/bundle
+RUN bundle config set path vendor/bundle
+
+RUN --mount=type=cache,uid=1000,target=/app/.cache/node_modules yarn install --modules-folder .cache/node_modules && \
+  cp -ar .cache/node_modules node_modules
+
+COPY --chown=rails . /app
+
+RUN --mount=type=cache,uid=1000,target=/app/tmp/cache bin/rails assets:precompile
+
+USER root
+
+# 不要なファイルを削除を条件付きで実行
+RUN rm -r /app/tmp/cache || true
+
+USER rails

VOLUME /app/public

# 実行時にコマンドを指定しなかった場合に実行されるコマンド
CMD ["bin/rails", "s", "-b", "0.0.0.0"]
```

#### ・コンテナ起動

以下コマンドで上記ファイルからイメージをビルドする

```
# DOCKER_BUILDKIT環境変数に何かしらの値を入れる
# -f でビルドで参照するファイルを指定する
% DOCKER_BUILDKIT=1 docker build -t myrailsapp -f Dockerfile-buildkit .
```

## 開発環境用の Docker 設定(ソースコードをコピーではなくマウントする)

開発環境においてはソースコードを編集した結果が即座に稼働中のサーバに反映されてほしいが、ビルド時にソースコード全体をコピーしている場合は、編集のたびにイメージをビルドし直しコンテナを再起動しなければならない。
そのため、一般的には開発環境での Docker 利用ではソースコード全体をコピーしてイメージ内部に含めるのではなく、ホストマシン上のディレクトリをコンテナ上にマウントさせる方式をとる。

#### ・開発環境用の Dockerfile

基本的には変更ないが、ユーザ指定をせず root ユーザのまま起動、およびソースコードのコピー、インストール手順の削除などを行う。

```diff:Dockerfile
# Node.jsダウンロード用ビルドステージ
FROM ruby:2.7.8 AS nodejs

WORKDIR /tmp

# Node.jsダウンロード
RUN curl -LO https://nodejs.org/dist/v14.21.3/node-v14.21.3-linux-x64.tar.xz
RUN tar xJf node-v14.21.3-linux-x64.tar.xz
RUN mv node-v14.21.3-linux-x64 node

# Railsプロジェクトインストール
FROM ruby:2.7.8

# nodejsをインストールしたイメージからnode.jsをコピー
COPY --from=nodejs /tmp/node /opt/node
# ENVの表記を最新の書き方に変更
ENV PATH=/opt/node/bin:$PATH

-# アプリケーション起動用のユーザを追加
-RUN useradd -m -u 1000 rails
-RUN mkdir /app && chown rails /app
-USER rails

# yarnのインストール
RUN curl -o- -L https://yarnpkg.com/install.sh | bash
ENV PATH=/home/rails/.yarn/bin:/home/rails/.config/yarn/global/node_modules/.bin:$PATH

# 2.7以上のrubyでrails newを実行するための設定
# ruby2.7.8との互換性を考慮して2.4.22を指定
RUN gem install bundler -v 2.4.22

WORKDIR /app

-# Dockerのビルドステップでキャッシュを使うためにGemfileを先にコピーしbundle install
-# アプリケーションの内容変更時にGemfileの内容に変化がなかった場合はbundle installの工程までキャッシュを利用できる
-COPY --chown=rails Gemfile Gemfile.lock package.json yarn.lock /app/
-
-RUN bundle install
-RUN yarn install
-
-# Gemfileに変化がなければここから再ビルドされる
-COPY --chown=rails . /app
-
-RUN bin/rails assets:precompile
-
-VOLUME /app/public
-
-# 実行時にコマンドを指定しなかった場合に実行されるコマンド
-CMD ["bin/rails", "s", "-b", "0.0.0.0"]

+RUN bundle config set path vendor/bundle
+
+# 実行時にコマンドを指定しなかった場合に実行されるコマンド
+CMD ["bash"]
```

#### ・ホストマシン上にコンテナへマウント用のディレクトリを作成

```
% docker volume create myurailsapp_bundle
% docker volume create myurailsapp_node_mojules
```

#### ・コンテナ起動

```
% docker run -i -t \ # -i -t でコンテナ上の/appでbashを起動する
  -v $(pwd):/app \ # カレントディレクトリをコンテナの/appへマウント
  -v myrailsapp_bundle:/app/vendor/bundle \
  -v myrailsapp_node_modules:/app/node_modules \
  -p 3000:3000 \
  myrailsapp
```

:::note info

macOS の場合は Docker を Linux の VM を経由して動作させる必要がある関係でファイルアクセスのパフォーマンスが低下する場合がある。その場合は上記コマンドを下記のように修正するとある程度改善する。

```
-v $(pwd):/app:cached
```

:::

#### ・サーバ起動

上記で起動した bash 内で下記コマンドを実行して rails サーバを起動する。

```
bundle install, yarn install, bin/rails s
```

## docker-compose で複数コンテナを組み合わせた開発環境を構築

Rails 単体以外に MySQL や Redis などのコンポーネントが必要になるケースの場合、複数コンテナのイメージを一度に起動してくれる`docker-compose`を使用することでそれらのミドルウェアのインストールや環境構築を大きく簡略化できる。

下記は Rails と一緒に MySQL コンテナを動作させるための設定手順。

#### 1. docker-compose.yml の作成

こちらのファイルも Dockerfile 同様、ルートディレクトリに配置する。

```docker-compose.yml
version: "3"
services:
  mysql:
    image: mysql:5.7
    command: ["--bind-address=0.0.0.0"]
    environment:
      MYSQL_ROOT_PASSWORD: password
    # portsの公開設定は動作させるだけなら必要ではないが、ホスト側からMYSQLに接続できるようにしておくと管理ツールが使いやすくなる
    ports:
      - "3306:3306"
    volumes:
      - ./mysql-data:/var/lib/mysql

  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      MYSQL_USERNAME: app
      MYSQL_PASSWORD: password
      MYSQL_HOST: mysql
    ports:
      - "3000:3000"
    depends_on:
      - mysql
    volumes:
      - .:/app
      - docker_app_bundle:/app/vendor/bundle
      - docker_app_node_modules:/app/node_modules

volumes:
  mysql-data:
    driver: local
  docker_app_bundle:
    driver: local
  docker_app_node_modules:
    driver: local
```

#### 2. MySQL の設定

MySQL コンテナを先に起動して DB 接続ユーザの設定をする。

```
% docker-compose up -d mysql

# -u root でルートユーザでログインし、 -p でパスワード入力を促す
% docker compose exec mysql mysql -u root -p

# DB作成
# CHARACTER SET でDBの文字セットを指定
# COLLATE でDBの照合順序（文字の比較方法）を指定
mysql > CREAE database appdb CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin;
mysql > CREAE database appdb_test CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin;

# ユーザー作成と権限付与、パスワードの設定
mysql > GRANT ALL on appdb.* TO app@'%' IDENTIFIED BY 'password';
mysql > GRANT ALL on appdb_test.* TO app@'%';

mysql > exit
```

#### 3. Railsの設定

Mysql使用のためのgemを追加して`bundle install`

```Gemfile
gem 'mysql2', '>= 0.4.4'
```

さらに`docker-compose.yml`に合わせて下記ファイルを編集する。

```diff:config.database.yml
default: &default
+  adapter: mysql2
+  encoding: utf8mb4
+  charset: utf8mb4
+  collation: utf8mb4_bin
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+  timeout: 5000
+  username: <%= ENV["MYSQL_USERNAME"] || "app" %>
+  password: <%= ENV["MYSQL_PASSWORD"] || "password" %>
+  host: <%= ENV["MYSQL_HOST"] || "127.0.0.1" %>
+  port: <%= ENV["MYSQL_PORT"] || 3306 %>

development:
  <<: *default
+  database: appdb

test:
  <<: *default
+  database: appdb_test
```

#### 4. コンテナ内で設定・サーバ起動

下記コマンドで、docker-composeが自動でmysqlコンテナを立ち上げた上でappコンテナでbashが立ち上がる。

```
# runコマンドはポートマッピング無効のため、ホストマシンから接続するためにオプションを指定
% docker compose run --service-ports app

# コンテナ環境のセットアップ
/app# bundle install
/app# bundle exec db:migrate

# サーバ起動
# 全てのインターフェースをバインドしてDockerのポート転送を有効化
# オプションを指定しないとコンテナ内からしかappへアクセスできない
/app# bundle exec rails s -b 0.0.0.0
```

:::note info

上記サーバ起動コマンドは`docker-compose.yml`へ記述することで通常のupコマンドで全コンテナを一括起動できる。

```diff:docker-compose.yml
略
  app:
    build:
      context: .
      dockerfile: Dockerfile
+   command: ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"]
    environment:
      MYSQL_USERNAME: app
略
```

```
# 全てのコンテナが一括起動
% docker compose up 
```

:::

## Railsログを標準出力へ変更し、コンテナ外から参照可能にする

Railsはデフォルトの設定では、logディレクトリのファイルへログ出力を保存する。コンテナでRailsを運用する場合は標準出力へ出力を変更しコンテナ外からログを参照できるようにしておく。

```config/environments/production.rb
# コンテナでRailsを運用するためにログを標準出力に出力する
  config.logger = ActiveSupport::Logger.new($stdout)
  $stdout.sync = true # syncを有効にすることで、標準出力にリアルタイムで出力
```

## Railsコンテナのエラーをrollbarでトラッキングする

アプリケーションのエラーを素早く検知するためにはテキストログだけでなく、外部エラートラッカーを利用すると良い。
以下はRollbarを利用した設定方法。

#### 1. Rollbarのセットアップ

[Rollbar](https://app.rollbar.com/)へアクセスし、GitHubアカウントを連携して登録し、プロジェクトを作成する。

#### 2. Railsの設定

プロジェクト作成後のRollbarの案内に従い、以下の順序で設定する。

- `rollbar`gemを追加し、`bundle install`
- `rails g rollbar`コマンドで設定ファイルを作成する(環境変数で設定したいのでトークンは指定しない)
- `docker-compose.yml`を編集

```diff:docker-compose.yml
  app:
    build:
      context: .
      dockerfile: Dockerfile
    environment:
      MYSQL_USERNAME: app
      MYSQL_PASSWORD: password
      MYSQL_HOST: mysql
+     ROLLBAR_ACCESS_TOKEN: 29ae333dc8d04629bf0f7e7bad2e2586
```

#### 3. テストコマンドもしくは適当な例外を発生させる

下記コマンドでトラッキングのテストが可能。

```
% docker compose exec app bundle exec rake rollbar:test
```

上記コマンド実行後、もしくは例外発生後に[Rollbar](https://app.rollbar.com/)のプロジェクトページへアクセスするとトラッキングされたitemを確認できる。

![スクリーンショット 2025-02-16 13.00.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790072/ecd36a2e-e624-42ef-abfb-4bf7ec0edc6b.png)

![スクリーンショット 2025-02-16 12.59.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3790072/c582fe30-18cf-4f58-a59e-89ce1f082bd4.png)