🐳

Next.js + Rails プロジェクトのセットアップ手順

2021/02/26に公開

フロントエンド Next.js + バックエンド Rails のセットアップ手順を整理します。

1. リポジトリの作成

GitHub 上に新規リポジトリを作成し、ローカルにクローンします。

git clone [GITHUB_URL]

プロジェクト名は project とします。

cd project

2. Next.js の作成

project/project-frontend として Next.js アプリを作成します。

  • Node 15.8.0
npx create-next-app project-frontend

yarn を実行します。

cd project-frontend
yarn

3. Rails の作成

project/project-backend として Rails アプリを作成します。

  • Ruby 3.0.0
  • Rails 6.1.2
rails new project-backend --skip-spring --api

4. Git の調整

自動生成される Next.js, Rails の .git を削除します。

cd project-frontend
rm -rf .git
cd project-backend
rm -rf .git

.gitignore の統合

ルート層に .gitignore を作成し、Next.js, Rails の .gitignore 内容を統合します。(それぞれの内容に /project-frontend/ もしくは /project-backend を付け加えます。)

統合後、Next.js, Rails の .gitignore を削除します。

5. 開発環境の Docker 化

開発環境を Docker 化していきます。

Next.js の Docker 化

project-frontend の設定です。

Dockerfile

Next.js 用の Dockerfile を追加します。

project-frontend/Dockerfile
FROM node:15.8

ENV APP /usr/src/app
RUN mkdir $APP
WORKDIR $APP

COPY package.json yarn.lock $APP/
RUN yarn

COPY . $APP/

CMD ["yarn", "dev"]

Rails の Docker 化

project-backend の設定です。

MySQL の設定

DB は MySQL を使うため、Gemfile を更新します。

project-backend/Gemfile
# Use sqlite3 as the database for Active Record
# gem 'sqlite3', '~> 1.4'
gem 'mysql2'

続いて、database.yml の設定を次のように更新します。

project-backend/config/database.yml
default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000
  charset: utf8mb4
  encoding: utf8mb4
  collation: utf8mb4_general_ci

local: &local
  socket: /var/run/mysqld/mysqlx.sock
  port: <%= ENV['DB_PORT'] %>
  host: <%= ENV['DB_HOST'] %>
  username: <%= ENV['DB_USERNAME'] %>
  password: <%= ENV['DB_PASSWORD'] %>

development:
  <<: *default
  <<: *local
  database: project_development
# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  <<: *local
  database: project_test

production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>
  socket: /tmp/mysql.sock

起動スクリプト

開発サーバー用の起動スクリプトを bin/server-dev に追加します。

project-backend/bin/server-dev
#!/bin/bash -i

# Wait for MySQL
until nc -z -v -w30 $DB_HOST $DB_PORT; do
 echo 'Waiting for MySQL...'
 sleep 1
done
echo "MySQL is up and running!"

# If the database exists, migrate. Otherwise setup (create and migrate)
bundle exec rails db:migrate 2>/dev/null || bundle exec rails db:create db:migrate db:seed

# If the container has been killed, there may be a stale pid file
# preventing rails from booting up
rm -f tmp/pids/server.pid

bundle exec rails server -p 8080 -b 0.0.0.0

同ファイルに実行権限がない場合があるので、必要に応じて権限を更新します。

chmod +x bin/server-dev

Dockerfile

Rails 用の Dockerfile を追加します。

project-backend/Dockerfile
FROM ruby:3.0.0

ENV BUNDLE_VERSION 2.2.11
ENV APP /usr/src/app

# netcat is for nc command
RUN apt-get update && apt-get install -y netcat && RUN apt-get install -y vim
RUN gem install bundler --version "$BUNDLE_VERSION" 

RUN mkdir $APP
WORKDIR $APP

COPY Gemfile* $APP/
RUN bundle install

COPY . $APP/

CMD ["./bin/server-dev"]

6. docker-compose でアプリ起動

docker-compose.yml の作成

ルート層に docker-compose.yml を作成します。

docker-compose.yml
version: '3'

services:
  db:
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password --sql_mode=""
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3307:3306'

  backend:
    build:
      context: project-backend
    volumes:
      - ./project-backend:/usr/src/app
      - ./project-backend/vendor/bundle:/usr/local/bundle
    ports:
      - '8080:8080'
    depends_on:
      - db
    stdin_open: true
    tty: true
    environment:
      DB_USERNAME: root
      DB_PASSWORD: password
      DB_PORT: 3306
      DB_HOST: db
      RAILS_MAX_THREADS: 5
      RAILS_ENV: development

  frontend:
    build:
      context: project-frontend
    volumes:
      - ./project-frontend:/usr/src/app
    ports:
      - '3000:3000'

.gitignore の更新

bundle の保存先に指定したローカルのボリュームパスを .gitignore に追加します。

.gitignore
# Ignore bundler config.
/project-backend/.bundle
+ /project-backend/vendor/bundle

動作確認

アプリを起動してみます。

docker-compose up

localhost:3000 で Next.js アプリが表示されることを確認します。

Next.js アプリが表示されることを確認

localhost:8080 で Rails アプリが表示されることを確認します。

Rails アプリが表示されることを確認

7. Next.js と Rails の連携

Docker での開発環境で Next.js から Rails API を叩けるようにします。

ngrok のインストール

getInitialProps など SSR 時に Rails API を叩くと ECONNREFUSED のエラーが出るので、ここでは ngrok で発行した URL を Rails アプリに割り当てることで対処します。

インストール後、ポート 8080 で起動します。

ngrok http 8080

Rails: 環境変数の設定

上記 ngrok で準備した URL を Rails の環境変数として dotenv-rails で管理します。

project-backend/Gemfile
group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
+   gem 'dotenv-rails'
end
docker-compose run backend bundle

環境変数として BACKEND_HOST を設定します。

project-backend/.env
BACKEND_HOST='xxxxxxxxxxxx.ngrok.io'

.gitignore.env を追加します。

.gitignore
.env

Rails: ホスト名の追加

開発環境の設定ファイルに開発サーバーのホスト名を追加します。

project-backend/config/environments/development.rb
Rails.application.configure do
  # .
  # .
  # .
  # Hostname
  config.hosts << 'localhost'
  config.hosts << '127.0.0.1'
  config.hosts << ENV['BACKEND_HOST']
end

Rails: CORS の設定

オリジン間リソース共有 (CORS) の設定をします。

Gemfile
# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'
docker-compose run backend bundle

CORS の設定ファイルを更新します。

project-backend/config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '127.0.0.1:3000', 'localhost:3000'

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

Rails: エンドポイントの作成

起動している backend のプロセス名を確認し、コンテナ内に入ります。

docker ps
docker exec -it project_backend_1 sh

モデルの作成

サンプルとして、リソース posts を作成します。

bundle exec rails g model Post title:string body:text
bundle exec rails db:migrate

JSONPlaceholder を使って、コンソールから初期データを投入します。

bundle exec rails c
require 'net/http'
uri = URI('https://jsonplaceholder.typicode.com/posts')
data = Net::HTTP.get(uri)
posts = JSON.parse(data)
posts.each { |post| Post.create!(title: post['title'], body: post['body']) } 

コントローラーの作成

GET /posts でDBに投入したデータを JSON 形式で取得できるようにします。

project-backend/config/routes.rb
Rails.application.routes.draw do
  resources :posts, only: :index
end
project-backend/app/controllers/posts_controller.rb
class PostsController < ApplicationController
  def index
    render json: { posts: Post.limit(50) }
  end
end

Next.js: API データの取得

ngrok で準備した URL をバックエンドの URL として環境変数に設定します。

project-frontend/.env.local
NEXT_PUBLIC_BACKEND_URL='https://xxxxxxxxxxxx.ngrok.io'

getInitialProps で Rails API からリソース posts を取得し、タイトルを一覧として表示させます。

project-frontend/pages/index.js
import Error from 'next/error';

const Home = ({ posts, statusCode }) => {
  if (statusCode) {
    return <Error statusCode={statusCode} />;
  }
  
  return (
    <div>
      <h1>POSTS</h1>
      <ul>
        {!posts
          ? null
          : posts.map((post, index) => {
              return <li key={index}>{post.title}</li>;
            })}
      </ul>
    </div>
  );
};

Home.getInitialProps = async (_ctx) => {
  try {
    const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/posts`);
    const data = await res.json();
    return {
      posts: data.posts,
    };
  } catch (error) {
    return {
      statusCode: error.response ? error.response.status : 500,
    };
  }
};

export default Home;

ブラウザから localhost:3000 を確認します。

localhost:3000 で確認

一覧が表示されていれば設定は完了です。

8. Redis の設定

Rails でバックグラウンドジョブを利用するために Redis を導入します。

docker-compose.ymlredis サービスを追加し、backendREDIS_URL を追加します。

docker-compose.yml
version: '3'

volumes:
  backend-sync-volume:
    external: true

services:
  db:
    image: mysql:8.0.20
    command: --default-authentication-plugin=mysql_native_password --sql_mode=""
    environment:
      MYSQL_ROOT_PASSWORD: password
    ports:
      - '3307:3306'

+   redis:
+     image: redis:alpine
+     command: ['redis-server', '--bind', '0.0.0.0', '--port', '6379']
+     ports:
+       - '6379:6379'

  backend:
    build:
      context: project-backend
    volumes:
      - ./project-backend:/usr/src/app
    ports:
      - '8080:8080'
    depends_on:
      - db
    stdin_open: true
    tty: true
    environment:
      DB_USERNAME: root
      DB_PASSWORD: password
      DB_PORT: 3306
      DB_HOST: db
      RAILS_MAX_THREADS: 5
      RAILS_ENV: development
      CORS_ORIGINS: 'localhost:3000,127.0.0.1:3000'
+       REDIS_URL: redis://redis:6379

  frontend:
    build:
      context: project-frontend
    volumes:
      - ./project-frontend:/usr/src/app
    ports:
      - '3000:3000'

9. Active Job の設定

ここでは Active Job のアダプタに Sidekiq を利用します。

インストール

まずは gem をインストールします。Foreman は開発環境でワーカーを立ち上げるのに利用します。

project-backend/Gemfile
+  gem 'sidekiq'

group :development do
  gem 'listen', '~> 3.3'
+   gem 'foreman'
end
docker-compose run backend bundle

Sidekiq の設定

続いて、Sidekiq の設定を追加します。

application.rb

project-backend/config/application.rb
+ config.active_job.queue_adapter = :sidekiq

routes.rb

バックグラウンドジョブのステータスを管理画面で見れるようにします。

project-backend/config/routes.rb
+ require 'sidekiq/web'

Rails.application.routes.draw do
+   mount Sidekiq::Web => '/sidekiq'

  resources :posts, only: :index
end

sidekiq.rb

project-backend/config/initializers/sidekiq.rb
Sidekiq.configure_client do |config|
  config.redis = { size: 1 }
end

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

sidekiq.yml

sidekiq -C config/sidekiq.yml で起動できるようにします。

project-backend/config/sidekiq.yml
:concurrency: 3
:pidfile: ./tmp/pids/sidekiq.pid
:logfile: ./log/sidekiq.log
:queues:
  - default
  - mailers
:daemon: true

Foreman の設定

Procfile.dev を作成し、開発環境でサーバーとワーカーを立ち上げる処理を書きます。

project-backend/Procfile.dev
web: bundle exec rails server -p 8080 -b 0.0.0.0
worker: bundle exec sidekiq -C config/sidekiq.yml

開発環境の起動スクリプトで Foreman を使うように修正します。

project-backend/bin/server-dev
- bundle exec rails server -p 8080 -b 0.0.0.0
+ bundle exec foreman start -f Procfile.dev

ジョブの作成

サンプルとして、PingJob を作成します。

project-backend/app/jobs/ping_job.rb
class PingJob < ApplicationJob
  queue_as :default

  def perform
    puts '-- PingJob performed'
  end
end

動作確認

Ctrl + C で docker-compose のプロセスを停止し、起動し直します。

docker-compose up

webworker が起動していることを確認します。

web と worker の起動を確認

管理画面の表示

localhost:8080/sidekiq で Sidekiq の管理画面が表示されることを確認します。

Sidekiq の管理画面

ジョブの実行

起動している backend のプロセス名を確認し、コンテナ内に入ります。

docker ps
docker exec -it project_backend_1 sh

コンソールから PingJob を実行してみます。

bundle exec rails c
PingJob.perform_later

ログでプロセスが実行されたことを確認します。

プロセスの実行を確認

次のステップ

デプロイ

フロントエンドを Vercel に、バックエンドを Heroku にデプロイする手順をまとめています。

https://zenn.dev/kei178/articles/1b79e48e676329

GraphQL 化

バックエンド GraphQL + フロントエンド Apollo Client の構築手順をまとめています。

https://zenn.dev/kei178/articles/2f4ffc6b89618c

https://zenn.dev/kei178/articles/8c6ad6fd91c9de

Discussion