😸

Next.js × Railsアプリケーションの環境構築【Docker使用】

2024/05/10に公開

はじめに

本記事は、Dockerを使用してNext.jsとRuby on Railsで構築したアプリケーションをVercelとFly.ioにデプロイした際のプロセスを記録しています。プロジェクトは、モノリポ構成を採用しており、フロントエンドとバックエンドのコードを同一のリポジトリで管理するようになっています。
また、RedisやRailsキューイングバックエンドSolid Queueの設定方法についても含みます。
同様のアーキテクチャを検討している方はぜひ読んでみてください。

前提

Vercel、Fly.ioの登録方法については記載しませんので、事前に登録しておいてください。

使用技術のバージョン

Ruby: 3.2.2
Rails: 7.1.3
React: 18.2.0
Next.js: 14.1.0

リポジトリ作成

  • GitHub上にリポジトリを作成して、ローカルにクローンします。
git clone <GitHubのURL>
  • ディレクトリを移動
    (ここでは、test というリポジトリ名にしています)
cd test
  • test ディレクトリ配下に frontback ディレクトリを作成
mkdir front back

雛形作成

compose.yml

ルートディレクトリに compose.yml を作成します。
また、他にもRedisやバックグラウンドワーカーの設定を後ほど追加していくので、暫定版です。

compose.yml
services:
  front:
    build:
      context: ./front
      dockerfile: Dockerfile
    environment:
      TZ: Asia/Tokyo
    volumes:
      - ./front:/app
      - front_node_modules:/app/node_modules
    command: yarn dev -p 4000
    ports:
      - "4000:4000"
  back:
    build:
      context: ./back
      dockerfile: Dockerfile
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - ./back:/app
    depends_on:
      - db
    ports:
      - "3000:3000"
    tty: true
    stdin_open: true
  db:
    image: postgres:16.2
    environment:
      POSTGRES_USER: root
      POSTGRES_PASSWORD: password
      POSTGRES_DB: app_development
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
volumes:
  front_node_modules:
  postgres_data:

フロントエンド準備

Next.jsアプリケーションを作成

node -v で使用したいNode.jsのバージョンになっているか確認します。今回使用したバージョンは、20.11.0です。
1. front ディレクトリにて以下のコマンドを実行し、Next.jsアプリケーションを作成

yarn create next-app .

以下のような質問がされると思います。今回は、以下のように回答して作成しました。

✔ Would you like to use TypeScript? … Yes
✔ Would you like to use ESLint? … Yes
✔ Would you like to use Tailwind CSS? … No
✔ Would you like to use `src/` directory? … Yes
✔ Would you like to use App Router? (recommended) … Yes
✔ Would you like to customize the default import alias (@/*)? … No

2. ルートディレクトリに .gitignore ファイルを作成し、frontディレクトリの .gitignore の内容を移動
先頭に front をつけるようにしてください。終わったら、 front ディレクトリの .gitignore は削除してください。

例)
- /node_modules
+ front/node_modules

Docker

front ディレクトリ直下に以下のDockerfileを作成します。

Dockerfile
FROM node:20.11.0

ENV TZ Asia/Tokyo

WORKDIR /app

COPY package.json yarn.lock /app/
RUN yarn install

COPY . /app

CMD ["yarn", "dev", "-p", "4000"]

バックエンド準備

Docker

1. back ディレクトリに以下の Dockerfile を作成

Dockerfile
FROM ruby:3.2.2

WORKDIR /app

RUN apt-get update -qq && \
    apt-get install -y build-essential nodejs postgresql-client vim

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 3000

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

2. back ディレクトリに、entrypoint.sh を作成
entrypoint.sh は、コンテナが開始された時に実行されるスクリプトです。
サーバー起動時にプロセスIDが server.pid に書き込まれ、終了する際に削除されますが、異常終了などでこのファイルが残ったままになってしまうと、サーバーが既に起動中であると誤認されてしまうため、プロセスIDファイルを削除する処理を行っています。

entrypoint.sh
#!/bin/bash
set -e

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

exec "$@"

Railsアプリケーションを作成

1. back ディレクトリに以下の Gemfile と、空の Gemfile.lock を作成

Gemfile
source "https://rubygems.org"
ruby "3.2.2"

gem "rails", "~> 7.1.3"

2. イメージのビルド

docker compose build

3. 以下のコマンドを実行し、Railsアプリケーションを作成
--rm: コンテナを終了時にコンテナを自動的に削除
--api: RailsアプリケーションをAPIモードで作成

docker compose run --rm back bundle exec rails new . --api
  • Gemfileがコンフリクトするので、 Overwrite /app/Gemfile? (enter "h" for help) [Ynaqdhm] と表示されます。上書きするので、 Y を入力します。

  • Rails7.1から、新規アプリケーション作成時に Dockerfile が生成されるようになったため、Dockerfileも Overwrite /app/Dockerfile? (enter "h" for help) [Ynaqdhm] と表示されます。ここは n を入力してください。理由としては、自動生成される Dockerfile は本番環境用のもので、開発用のものではないからです。また、Fly.ioのデプロイに最適な Dockerfile を後ほど自動生成するので、本番環境用の Dockerfile はそちらをベースに作ります。
    現状あるDockerfileは Dockerfile.dev にリネームし、開発用にしておきましょう。また、 compose.yml の以下の部分を修正しておきます。

compose.yml
  back:
    build:
      context: ./back
-       dockerfile: Dockerfile
+       dockerfile: Dockerfile.dev

4. 自動生成された.git は削除

5. 自動生成された .gitignore の内容をルートの .gitignore に移動
先頭に back をつけるようにしてください。終わったら、 back ディレクトリの .gitignore は削除してください。

6. 基本設定
タイムゾーンとロケールの設定を application.rb に記載します。

config/application.rb
module App
  class Application < Rails::Application
  
    # Set timezone
    config.time_zone = "Tokyo"
    config.active_record.default_timezone = :local

    # Set locale
    config.i18n.default_locale = :ja
    config.i18n.available_locales = [:en, :ja]
  end
end

7. DB作成

  • database.ymlの修正
config/database.yml
default: &default
  adapter: postgresql
  encoding: utf8
  port: 5432
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  username: root
  password: password
  database: app_development
  host: db

本番環境用の設定は不要です。なぜなら、Fly.ioで後ほどPostgreSQLサーバーを作成する際に、 DATABASE_URL がSecretsとして自動で設定され、それを通じてDBにアクセスできるためです。

  • Gemfile に指定するDBをPostgreSQLに変更
Gemfile
- gem "sqlite3", "~> 1.4"
+ gem 'pg', '~> 1.1'
  • bundle install 実行
docker compose run --rm back bundle install

開発環境起動確認

1. イメージをビルド

docker compose build

2. コンテナ起動

docker compose up

3. http://localhost:3000 にアクセスし、Railsの初期ページが表示されることを確認

4. http://localhost:4000 にアクセスし、Next.jsの初期ページが表示されることを確認

デプロイ

ここまでの内容をGitHubにプッシュして、デプロイしていきます。

フロントエンドデプロイ

1. https://vercel.com/dashboard へアクセス

2. 「Add New」の「Project」をクリック

3. 「Import Git Repository」から対象のリポジトリの「Import」をクリック

4.「Configure Project」の「Root Directory」を「front」に変更し、「Continue」

5. 「Deploy」をクリック

Vercelは今後 main ブランチにpushされると自動でデプロイしてくれます。

バックエンドデプロイ

1. back ディレクトリに移動

2. Fly CLIをインストール

brew install flyctl

3. Fly.ioにログイン

fly auth login

4. Fly.ioへのデプロイ設定

fly launch

? Do you want to tweak these settings before proceeding? (y/N)と質問されますので、「y」を入力します。すると、ダッシュボードがブラウザ上で開かれますので、そちらで設定していきます。
今回は以下のような設定にしました。Redisが必要な方はここで Upstash for Redis を選択しておきましょう。

設定が完了したら、「Confirm Settings」をクリックして、ターミナルに戻ります。

特に自分で下記の接続情報を用いなければならない場面は私はなかったのですが、念の為、ここでターミナルに表示されているものを控えておくと安心です。

  • Username
  • Password
  • Hostname
  • Flycast
  • Proxy port
  • Postgres port
  • Connection string
  • DATABASE_URL

.dockerignore ファイルがコンフリクトし、Overwrite .dockerignore? (enter "h" for help) [Ynaqdhm] hと表示されています。「d」を入力すると差分が見れるので、確認してみます。

+ # Ignore assets.
+ /node_modules/
+ /app/assets/builds/*
+ !/app/assets/builds/.keep
+ /public/assets

アセット関連のファイルやディレクトリをDockerイメージビルドコンテキストから除外するための記述のようです。今回は、CSSやJavaScriptはNext.js側で扱いますので、.dockerignoreに追記されたものによる影響はないと思いますが、仮にRails側でアセットを管理していた場合、この追記によりDockerイメージのビルドが効率化されるようになるので、とりあえず自動生成されたもので上書きます(「Y」を入力してください)。

docker-entrypoint はFly.ioの自動生成ファイルにはほとんど何も書かれていないので、上書きせず、既存のものを使います。

5. bundle install実行
fly launch 実行時に、Gemfileに自動で追加が入っているので、一度 back コンテナに入って bundle install を実行しておきます。

6. デプロイ

fly deploy

※ Fly.ioのデプロイ先URLにアクセスしても、今回はAPIモードなので、Railsの初期ページは表示されません。開発環境ではRailsが特別に表示させてくれるだけだそうです。後ほどサンプルアプリを作成しますので、そこで動作確認をまとめて行います。

バックエンドデプロイ用のGitHub Actions作成

Fly.ioはVercelのようにGitHubの変更を自動で検知してデプロイするしくみがないので、GitHub Actionsを作成して、デプロイを自動化します。

1. Fly.ioのトークンを生成

  • Fly.ioのダッシュボードから、Account > Access Tokensをクリック
  • 「Create token」にトークン名を入力し、「Create」をクリック

2. GitHubのリポジトリにトークンを設定

  • GitHubリポジトリの Settings > Secrets and variables > Repository secrets > New repository secretを選択
  • 「Name」には FLY_API_TOKENを、「Secret」には、1.で作成したトークンを設定し、「Add secret」をクリック

3. GitHub Actions用ファイル作成
ルート直下に .github/workflows/fly.yml を以下のように作成します。

.github/workflows/fly.yml
name: Fly Deploy
on:
  push:
    branches:
      - main
    paths:
      - 'back/**'
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy back/ --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

mainブランチの back ディレクトリに対しての変更がpushされたときのみ実行されるようにしています。

動作確認

CORS設定

  • docker compose exec back bashback コンテナに入る
  • Gemfilegem 'rack-cors'のコメントアウトを外して、 bundle install
  • config/initializers/cors.rb を以下のように変更し、フロントエンドからのアクセスができるように設定
config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '127.0.0.1:4000', 'localhost:4000', '<Vercel URL>'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
  • docker compose down でコンテナを停止し、 docker compose up --build で再度立ち上げ

サンプルアプリ作成

簡易的なサンプルアプリを作成します。

Rails

1. Post モデル作成

  • docker compose exec back bashback コンテナに入る
  • 以下実行してリソース Post を作成
bundle exec rails g scaffold Post title:string body:text
bundle exec rails db:migrate

2. seedデータ作成

  • seeds.rb に以下追加
db/seeds.rb
Post.create!(
  [
    { title: "秋の始まり", body: "秋の涼しい風が心地よい季節が始まりました。木々の葉が色づき、秋の収穫が待ち遠しいです。この時期には、散歩や読書に最適な時間が増え、心が落ち着きます。" },
    { title: "テクノロジーの最新トレンド", body: "テクノロジーの世界では、AIと機械学習の進化が止まりません。これらの技術は、仕事から日常生活まで、私たちの生活を根本的に変えています。最新のトレンドを追いかけることは、これからの未来を形作る上で欠かせません。" },
    { title: "健康的な生活のための簡単なヒント", body: "健康的な生活を送ることは、多忙な日々の中でも可能です。バランスの取れた食事、定期的な運動、十分な睡眠が鍵となります。小さな変更から始めて、徐々にライフスタイルに組み込んでいきましょう。" },
  ]
)
  • 以下のコマンド実行
bundle exec rails db:seed

3. http://localhost:3000/posts にアクセスすると、先ほど作成したテストデータがJSON形式で表示されることを確認

Next.js

1. 投稿のタイトルと内容を取得し、表示するコードを作成 (CSSは省略)

src/app/page.tsx
page.tsx
import CreatePost from "@/app/features/components/CreatePost/CreatePost";
import { getAllPosts } from "@/app/features/lib/fetchPost";

type Post = {
  id: number;
  title: string;
  body: string;
};

export default async function Home() {
  const posts: Post[] = await getAllPosts();

  return (
    <main>
      <CreatePost />
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <span>タイトル: {post.title}</span>
            <span>内容: {post.body}</span>
          </li>
        ))}
      </ul>
    </main>
  );
}
src/features/components/CreatePost/CreatePost.tsx
CreatePost.tsx
"use client";

import { useRef } from "react";
import { createPost } from "@/app/features/lib/fetchPost";

export default function CreatePost() {
  const ref = useRef<HTMLFormElement>(null);

  return (
    <form
      ref={ref}
      action={ async (formData) => {
        await createPost(formData)
        ref.current?.reset()
      }}
    >
      <div>
        <label htmlFor="title">タイトル</label>
        <input type="text" name="title" id="title" required />
      </div>
      <div>
        <label htmlFor="body">内容</label>
        <textarea name="body" id="body" required />
      </div>
      <button>投稿</button>
    </form>
  )
}
src/lib/fetchPost.ts
fetchPost.ts
"use server";

import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";

export const getAllPosts = async () => {
  const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`, {
    method: "GET",
    headers: {
      "Content-Type": "application/json",
    },
    cache: "no-store",
  });
  return res.json();
}

export const createPost = async (formData: FormData) => {
  const title = formData.get("title");
  if (!title) {
    throw new Error("タイトルが入力されていません");
  }

  const body = formData.get("body");
  if (!body) {
    throw new Error("本文が入力されていません");
  }

  await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ title, body }),
  });

  revalidatePath("/");
  redirect("/");
}

2. APIサーバーのURLを設定

  • 開発環境用

先程の page.tsx で使用している環境変数 NEXT_PUBLIC_API_URL を設定します。

まず、back コンテナのコンテナIDを確認します。

docker ps

コンテナのIPアドレスを取得します。

docker inspect <コンテナID> | grep IPAddress

.env.local ファイルを作成し、以下のように記入します。

env.local
NEXT_PUBLIC_API_URL=http://<コンテナIPアドレス>:3000

コンテナのIPアドレスを固定するため、 compose.yml を修正します。

compose.yml
例)
services:
  front:
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.2
  back:
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.3
  db:
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.4
networks:
  fixed_compose_network:
    ipam:
      driver: default
      config:
        - subnet: 172.27.0.0/24

http://localhost:4000 にアクセスすると、画像のように投稿が表示されます。

試しに以下のようなデータを追加してみます。

追加した投稿が表示されることが確認できました。

  • 本番環境用

Vercelのプロジェクトダッシュボード Project Settings > Environment Variablesを開きます。
Keyに NEXT_PUBLIC_API_URL を、ValueにFly.ioのデプロイ先URLを入力し、環境変数として登録します。

Vercelのデプロイ先URLを開くと、seedデータは投入していないのでデータは表示されませんが、投稿フォームが表示されていることが確認できます。フォームから投稿して、開発環境と同様フォームの下に表示されていれば成功です。

Solid Queue

バックエンドで非同期処理を行うためにSolid Queueを導入します。
同じくDBを使用するバックエンドワーカーの Delayed Job ではなく、 Solid Queue を採用した理由は以下です。

  • DHH氏がRailsリポジトリのisssueで、Rails8ではデフォルトActive Jobのバックエンドとして Solid Queue を採用することを提案しているようだということ
    https://github.com/rails/rails/issues/50442
  • Delayed Job より更新が頻繁に行われていること

設定

1. Gemfileに以下を追加し、 bundle install

gem "solid_queue"

2. マイグレーション

  • マイグレーションファイルの生成
bundle exec rails solid_queue:install:migrations
  • マイグレーションの実行
bundle exec rails db:migrate

3. Active Jobアダプタの設定

config/application.rb
config.active_job.queue_adapter = :solid_queue

4. ジョブの作成

  • 以下のコマンドを実行してジョブファイルを生成
bundle exec rails g job PostLogsJob
  • ジョブファイル修正
app/jobs/post_logs_job.rb
class PostLogsJob < ApplicationJob
  queue_as :default

  def perform(post)
    puts "*** PostLogsJob performed (title: #{post.title}, body: #{post.body}) ***"
  end
end
  • コントローラーを修正
    投稿を追加する際にジョブのキューに登録するようにします。
app/controllers/posts_controller.rb
def create
  @post = Post.new(post_params)

  if @post.save
+   # モデルのインスタンスを渡してジョブをキューに登録する
+   PostLogsJob.perform_later(@post)
    render json: @post, status: :created, location: @post
  else
    render json: @post.errors, status: :unprocessable_entity
  end
end

5. compose.yml にバックグラウンドワーカーサービスを追記

compose.yml
services:
  ・・・
  worker:
    build:
      context: ./back
      dockerfile: Dockerfile.dev
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
    command: bash -c "bundle exec rake solid_queue:start"
    volumes:
      - ./back:/app
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.5

6. fly.tomlに追記

fly.toml
[processes]
  app = "bin/rails server"
  worker = "bundle exec rake solid_queue:start"

また、デフォルトでは auto_stop_machines がtrueに設定されているため、自動でマシンが停止するようになっています。わたしは停止させないように以下falseに変更しました。

fly.toml
[http_service]
  auto_stop_machines = false

動作確認

  • 開発環境

一度サーバーを停止させ、イメージのビルドをし直した上で再度サーバーを立ち上げてください。
http://localhost:4000 にアクセスし、なにかデータを追加してみると、workerのログにジョブで定義した出力内容が表示されていることを確認できれば成功です。

  • 本番環境

Vercelのデプロイ先URLにアクセスします。
開発環境と同様に、データを追加した際に、Fly.ioのMachinesログに想定したメッセージが出力されていればOKです。

nrt [info] *** PostLogsJob performed (title: Solid Queueテスト, body: Solid Queueテスト) ***

Redis

SidekiqやResqueはRedisを必要としますが、Solid QueueはRDBを使用するので、バックグラウンドジョブ用にはRedisは不要です。ですが、私が作成していたアプリではActionCableを使用したかったので、サブスクリプションアダプタとしてRedisを用意しました。また、私は極力開発と本番で環境を同じにしたかったので、開発環境でもRedisを導入しました。

設定

  • 本番環境用

fly lauch 実行時にRedisを選択して、使える準備は整っているはずなので、確認のためにRedisアクセス用URLを見てみます。
Fly.ioのダッシュボードからUpstash for Redisの使用を有効化した際に、 REDIS_URL は自動でfly secretsに追加されているはずなので、以下のコマンドで確認します。

fly ssh console -C "printenv REDIS_URL"

redis://default:secretpassword@my-apps-redis-host.internal:6379 のような表示が見つかると思います。これがRedisアクセスURLです。
Fly.ioにシークレットとして登録済みなので、 credentials.yml でこれを設定しなくても大丈夫です。

  • 開発環境用

1. compose.yml に以下を追加

compose.yml
services:
 ...
  back:
    build:
      context: ./back
      dockerfile: Dockerfile.dev
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
+     REDIS_URL: "redis://redis:6379"
    command: bash -c "rm -f tmp/pids/server.pid && bundle exec rails s -b '0.0.0.0'"
    volumes:
      - ./back:/app
+   depends_on:
+     - redis
      - db
    ports:
      - "3000:3000"
    tty: true
    stdin_open: true
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.3
 ...
+ redis:
+   image: "redis:alpine"
+   ports:
+     - "6379:6379"
+   volumes:
+     - ./redis/data:/data
+   networks:
+     fixed_compose_network:
+       ipv4_address: 172.27.0.5

2. コントローラーの修正
正常に設定できているか確認するため、Redisに値を格納し、その値を取得した結果を出力するテストをしてみます。

app/controllers/posts_controller.rb
def create
  @post = Post.new(post_params)

  if @post.save
-   # モデルのインスタンスを渡してジョブをキューに登録する
-   PostLogsJob.perform_later(@post)
+   # Redisに接続
+   redis = Redis.new(url: ENV["REDIS_URL"])
+
+   # PostのIDをRedisに保存
+   redis.set("post:#{@post.id}", @post.to_json)
+
+   # 保存した値をRedisから取得
+   value = redis.get("post:#{@post.id}")
+   puts value

    render json: @post, status: :created, location: @post
  else
    render json: @post.errors, status: :unprocessable_entity
  end
end

3. .gitignore に追記
Redisがデータのスナップショットを自動で保存すると、redis/data/dump.rdb ファイルができますが、これをGit管理しないようにします。

.gitignore
redis/data/*

4. サーバー再起動

動作確認

  • 開発環境

http://localhos:4000 にアクセスし、データを追加してみます。

Redisに格納されたPostデータの内容が出力されていることが確認できます。

  • 本番環境

Vercelのデプロイ先URLにアクセスし、同様にデータを追加します。

Fly.ioのログの出力が確認できれば成功です。

nrt [info] {"id":5,"title":"Redisテスト","body":"Redisテスト","created_at":"2024-04-29T19:43:02.890+09:00","updated_at":"2024-04-29T19:43:02.890+09:00"}

おまけ1: Sidekiq

私は初めバックグラウンドジョブの実行に、Sidekiq を使用しようとしていました。
しかし、なんのジョブも実装していない状態でも、バックグラウンドでSidekiqがRedisに行うコールが大量にあり、数時間で1日の利用制限に達してしまう状態でした。調べた結果、Upstash for Redisの無料枠の範囲で運用するのは厳しいと判断し、今回は導入を見送りました。(念の為問い合わせもしてみましたが、無料枠では難しいようだと回答をもらいました)
[参考]
https://community.fly.io/t/managing-redis-rate-limits-on-sidekiq-and-rails/6741/19
https://community.fly.io/t/pay-as-you-go-pricing-for-redis-databases/16253/15

一旦Sidekiqを使用したテストデプロイはできましたので、従量課金制のプランに移行するなどして、Sidekiqを利用したい方は、以降ご参考ください。

Redis

Sidekiqの実行にはRedisが必要ですので、導入します。
こちらで設定した内容と同じです。

Sidekiq

設定

1. Gemfileに以下を追加し、 bundle install

gem 'sidekiq'

2. Active Jobのキューアダプターに sidekiq を設定

config/application.rb
config.active_job.queue_adapter = :sidekiq

3. compose.yml にバックグラウンドワーカーサービスを追記

compose.yml
services:
  ・・・
  worker:
    build:
      context: ./back
      dockerfile: Dockerfile.dev
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
      REDIS_URL: "redis://redis:6379"
    command: bash -c "bundle exec sidekiq"
    volumes:
      - ./back:/app
    depends_on:
      - redis
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.5

Sidekiqの場合、 worker サービスに必ずRedisへのアクセスが必要なので、depends_onenvironmentsREDIS_URL の追加を忘れないようにしてください。

4. Sidekiq から接続するRedisのURLとLoggerを設定

config/initializers/sidekiq.rb

Sidekiq.configure_client do |config|
  config.redis = { url: ENV['REDIS_URL'] }
end

Sidekiq.configure_server do |config|
  config.redis = { url: ENV['REDIS_URL'] }
  config.logger = Sidekiq::Logger.new($stdout)
end

5. Sidekiqの状態を管理画面から参照できるように設定
第三者がアクセスできないよう、認証つきの管理画面が開けるようにします。

routes.rb

+require 'sidekiq/web'

+Sidekiq::Web.use ActionDispatch::Cookies
+Sidekiq::Web.use Rails.application.config.session_store, Rails.application.config.session_options

Rails.application.routes.draw do
+ mount Sidekiq::Web, at: '/sidekiq'

+ Sidekiq::Web.use Rack::Auth::Basic do |username, password|
+   username == Rails.application.credentials.sidekiq_user[:username] &&
+   password == Rails.application.credentials.sidekiq_user[:password]
+ end

  resources :posts
  get "up" => "rails/health#show", as: :rails_health_check
end

6. credentials.yml にSidekiq用のusenameとpasswordを設定

credentials.yml
sidekiq_user:
  username: <ユーザー名>
  password: <パスワード>

7. ジョブの作成

  • 以下のコマンドを実行してジョブファイルを生成
bundle exec rails g job PostLogsJob
  • ジョブファイル修正
app/jobs/post_logs_job.rb
class PostLogsJob < ApplicationJob
  queue_as :default

  def perform(post)
    logger.info "*** PostLogsJob performed (title: #{post.title}, body: #{post.body}) ***"
  end
end
  • コントローラーを修正
    投稿を追加する際にジョブのキューに登録するようにします。
app/controllers/posts_controller.rb
def create
  @post = Post.new(post_params)

  if @post.save
+   # モデルのインスタンスを渡してジョブをキューに登録する
+   PostLogsJob.perform_later(@post)
    render json: @post, status: :created, location: @post
  else
    render json: @post.errors, status: :unprocessable_entity
  end
end

8. fly.toml に追記

fly.toml
+ [processes]
+ app = "bin/rails server"
+ worker = "bundle exec sidekiq"

9. 高度な設定が必要であれば、 sidekiq.yml も追加
https://github.com/sidekiq/sidekiq/wiki/Advanced-Options

動作確認

  • 開発環境

サーバーを停止させ、イメージをビルドし直し、再度サーバを起動させたら、http://localhost:4000 にアクセスします。
データを追加して、ログに出力されていることを確認できたらOKです。


次に、管理画面にアクセスできるか確認します。こちらは、 http://localhost:3000/sidekiq です。認証に成功すると、管理画面が表示されます。

  • 本番環境

Vercelのデプロイ先URLにアクセスし、データを追加します。

Fly.ioの workerマシンログに想定するログが表示されていれば成功です。

nrt [info] I, [2024-02-04T17:24:35.127213 #305] INFO -- : [ActiveJob] [PostLogsJob] [8c869072-7e97-408f-8640-c834a751a009] *** PostLogsJob performed (title: Sidekiqテスト, body: Sidekiqテスト) ***

管理画面は、<fly.ioデプロイ先URL>/sidekiqで確認できます。

おまけ2: Delayed Job

Solid Queueを導入する前にDelayed Jobも試しましたので、そちらの記録も残しておきます。

設定

1. Gemfileに以下を追加し、 bundle install

gem 'delayed_job_active_record'

2. マイグレーション

  • Delayed Jobがジョブを保存するために使用する delayed_jobs テーブル用のマイグレーションファイルの生成
bundle exec rails g delayed_job:active_record
  • マイグレーションの実行
bundle exec rails db:migrate

3. Active Jobのキューアダプターに delayed_job を設定

config/application.rb
config.active_job.queue_adapter = :delayed_job

4. ジョブの作成

  • 以下のコマンドを実行してジョブファイルを生成
bundle exec rails g job PostLogsJob
  • ジョブファイル修正
app/jobs/post_logs_job.rb
class PostLogsJob < ApplicationJob
  queue_as :default

  def perform(post)
    puts "*** PostLogsJob performed (title: #{post.title}, body: #{post.body}) ***"
  end
end
  • コントローラーを修正
    投稿を追加する際にジョブのキューに登録するようにします。
app/controllers/posts_controller.rb
def create
  @post = Post.new(post_params)

  if @post.save
+   # モデルのインスタンスを渡してジョブをキューに登録する
+   PostLogsJob.perform_later(@post)
    render json: @post, status: :created, location: @post
  else
    render json: @post.errors, status: :unprocessable_entity
  end
end

5. ログの出力設定
ログがバッファリングされて、ログメッセージが即時に表示されないような事象を防ぐために、以下を追加します。

config/environments/development.rb
$stdout.sync = true

config.logger = ActiveSupport::Logger.new($stdout)
config.log_level = :info
config/environments/production.rb
$stdout.sync = true 

# Log to STDOUT by default
  config.logger = ActiveSupport::Logger.new(STDOUT)
    .tap  { |logger| logger.formatter = ::Logger::Formatter.new }
    .then { |logger| ActiveSupport::TaggedLogging.new(logger) }

config.log_level = ENV.fetch("RAILS_LOG_LEVEL", "info")

6. compose.yml にバックグラウンドワーカーサービスを追記

compose.yml
services:
  ・・・
  worker:
    build:
      context: ./back
      dockerfile: Dockerfile.dev
    environment:
      RAILS_ENV: development
      TZ: Asia/Tokyo
    command: bash -c "bundle exec rails jobs:work"
    volumes:
      - ./back:/app
    networks:
      fixed_compose_network:
        ipv4_address: 172.27.0.5

7. fly.tomlに追記

toml
+ [processes]
+   app = "bin/rails server"
+   worker = "bundle exec rails jobs:work"

動作確認

  • 開発環境

一度サーバーを停止させ、イメージのビルドをし直した上で再度サーバーを立ち上げてください。
http://localhost:4000 にアクセスし、なにかデータを追加してみて、ログにその内容が表示されていることを確認できれば成功です。

  • 本番環境

Vercelのデプロイ先URLにアクセスします。
開発環境と同様に、データを追加した際に、Fly.ioのworkerマシンのログに想定したメッセージが出力されていればOKです。

nrt [info] *** PostLogsJob performed (title: テスト, body: テスト) ***

参考

https://zenn.dev/kei178/articles/43172ba33eece4
https://qiita.com/ippei_jp/items/1163a40a86d07fa691b2
https://fly.io/docs/reference/redis/
https://fly.io/docs/rails/the-basics/sidekiq/#provision-a-redis-server
https://github.com/sidekiq/sidekiq/wiki/Logging

Discussion