Nginx+Next.js+Rails+PostgreSQLを使用した開発環境構築

# 概要

※当記事は所属している[株式会社コンピュータテクノブレイン](https://computer-tb.co.jp/2022/06/22/nginxnext-jsrailspostgresql%e3%82%92%e4%bd%bf%e7%94%a8%e3%81%97%e3%81%9f%e9%96%8b%e7%99%ba%e7%92%b0%e5%a2%83%e6%a7%8b%e7%af%89/)にて投稿した内容を転載したものです。

再度環境構築の話となりますが、タイトルにある通り`Nginx`のリバースプロキシを使用し`Next.js`、`Ruby on Rails`の`Docker`を使用した開発環境の構築についての手順を紹介します。

データベースは`PostgreSQL`を使用しようと思います。

# 構成

2022年6月22日時点での環境を示します。

- macOS Monterey 12.4
- Docker 20.10.14
- Docker Compose v2.5.1
- Nginx 1.21.6
- Node.js v18.4.0
- Next.js 12.1.6
- Ruby 3.1.2
- Ruby on Rail 7.0.3
- PostgreSQL 14.3

ディレクトリ構造

```
プロジェクトルート
├── docker-compose.yml
├── container
│   ├── backend
│   │   └── Dockerfile
│   │   └── entrypoint.sh
│   ├── db
│   ├── frontend
│   │   └── Dockerfile
│   └── nginx
│       └── default.conf
│       └── nginx.conf
├── backend
│   ├── Gemfile
│   └── Gemfile.lock
└── frontend
```

# プロジェクトの作成

## Next.js（フロントエンド側）のプロジェクト作成

- docker-compose.yml

```yaml
version: "3"

services:
  frontend:
    container_name: frontend
    build:
      context: .
      dockerfile: container/frontend/Dockerfile
    volumes:
      - ./frontend:/usr/src/app
    command: yarn dev
    ports:
      - "3000:3000"
```

- container/frontend/Dockerfile

```dockerfile
FROM node:18-alpine3.15

# 環境変数の設定
ENV APP_PATH /usr/src/app

WORKDIR $APP_PATH
```

### Next.jsのプロジェクトを作成する

```sh
docker compose run --rm frontend npx create-next-app . --ts
```

## Ruby on Rails（バックエンド側）のプロジェクト作成

- docker-compose.yml（修正）

```diff_yaml
version: "3"

services:
  frontend:
    container_name: frontend
    build:
      context: .
      dockerfile: container/frontend/Dockerfile
    volumes:
      - ./frontend:/usr/src/app
    command: yarn dev
    ports:
      - "3000:3000"
+    depends_on:
+      - backend

+  backend:
+    container_name: backend
+    build:
+      context: .
+      dockerfile: container/backend/Dockerfile
+    volumes:
+      - ./backend:/app
+    environment:
+      TZ: Asia/Tokyo
+      RAILS_ENV: development
+    ports:
+      - "3001:3000"
+    depends_on:
+      - db

+  db:
+    image: postgres:14.3-alpine
+    container_name: postgres
+    environment:
+      - TZ=Asia/Tokyo
+      - PGTZ=Asia/Tokyo
+      - POSTGRES_PASSWORD=password
+    volumes:
+      - ./container/db/data:/var/lib/postgresql/data
+    ports:
+      - "5432:5432"
```

- container/backend/Dockerfile

```dockerfile
FROM ruby:3.1.2-alpine

ENV ROOT="/app"
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo

WORKDIR ${ROOT}

COPY ./backend/Gemfile ./backend/Gemfile.lock ${ROOT}

RUN apk update && \
    apk add \
    alpine-sdk \
    build-base \
    sqlite-dev \
    postgresql-dev \
    tzdata \
    git \
    gcompat

RUN gem install bundler
RUN bundle install

COPY ./container/backend/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

CMD ["rails", "server", "-e"]
```

- container/backend/entrypoint.sh

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

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

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

- backend/Gemfile

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

ruby "3.1.2"

# Bundle edge Rails instead: gem "rails", github: "rails/rails", branch: "main"
gem "rails", "~> 7.0.3"
```

- backend/Gemfile.lock

```
空ファイル
```

### Ruby on Railsのプロジェクトを作成する

```sh
docker compose run --rm backend bundle exec rails _7.0.3_ new . -d postgresql -f
```

## コンテナをビルドする

```sh
docker compose build
```

### データベース情報を設定する

- backend/config/database.yml

```diff_yaml
 default: &default
   adapter: postgresql
   encoding: unicode
   # For details on connection pooling, see Rails configuration guide
   # https://guides.rubyonrails.org/configuring.html#database-pooling
   pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
+  username: postgres
+  password: password
+  host: db
```

## データベースを作成する

```sh
docker compose run --rm backend rails db:create
```

## コンテナを立ち上げる

```sh
docker compose up -d
```

## `http://localhost:3000/`へアクセスし、フロントエンド側の画面が表示されることを確認する

![57809f476bd9b40e887f909b6c721ec5-1024x486.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152063/f8debcc3-2625-e527-1f53-17117f9020a4.png)

## `http://localhost:3001/`へアクセスし、バックエンド側の画面が表示されることを確認する

![536721958ea42bc1f3ba9ca40c0a65f8-1024x485.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152063/f9a4cc8e-93ff-a01c-c469-c90e269fcdf3.png)

## コンテナを落とす

```sh
docker compose down
```

## Nginxの設定を追加

- docker-compose.yml（再修正）

```diff_yaml
version: "3"

services:
+  web:
+    image: nginx:1.21.6
+    container_name: web
+    environment:
+      TZ: Asia/Tokyo
+    volumes:
+      - ./container/nginx/default.conf:/etc/nginx/conf.d/default.conf
+      - ./container/nginx/log:/var/log/nginx
+      - ./backend/public:/backend/public
+      - ./backend/tmp:/backend/tmp
+    ports:
+      - "80:80"
+    depends_on:
+      - frontend
+      - backend

  frontend:
    container_name: frontend
    build:
      context: .
      dockerfile: container/frontend/Dockerfile
    volumes:
      - ./frontend:/usr/src/app
    command: yarn dev
    ports:
      - "3000:3000"
    depends_on:
      - backend

  backend:
    container_name: backend
    build:
      context: .
      dockerfile: container/backend/Dockerfile
    volumes:
      - ./backend:/app
    environment:
      TZ: Asia/Tokyo
      RAILS_ENV: development
    ports:
      - "3001:3000"
    depends_on:
      - db

  db:
    image: postgres:14.3-alpine
    container_name: postgres
    environment:
      - TZ=Asia/Tokyo
      - PGTZ=Asia/Tokyo
      - POSTGRES_PASSWORD=password
    volumes:
      - ./container/db/data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
```

- container/nginx/default.conf

```conf
upstream backend {
  server unix:///backend/tmp/sockets/puma.sock fail_timeout=30s;
}

server {
  listen 80;
  # ドメインもしくはIPを指定
  server_name localhost;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log;

  ########################################
  # リバースプロキシ関連の設定
  ########################################
  # 公開側（フロントエンド）
  location / {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://frontend:3000/;
  }

  # 管理側（バックエンド）
  location /manage/ {
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $http_host;
    proxy_pass http://backend;
  }
}
```

- container/nginx/nginx.conf

```conf
user  root;
worker_processes  auto;

error_log  /var/log/nginx/error.log notice;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';
    access_log  /var/log/nginx/access.log  main;
    sendfile        on;
    keepalive_timeout  65;
    include /etc/nginx/conf.d/*.conf;
}
```

## バックエンド側のPumaの設定を変更する

- backend/config/puma.rb

```diff_ruby
# Puma can serve each request in a thread from an internal thread pool.
# The `threads` method setting takes two numbers: a minimum and maximum.
# Any libraries that use thread pools should be configured to match
# the maximum value specified for Puma. Default is set to 5 threads for minimum
# and maximum; this matches the default thread size of Active Record.
#
max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count }
threads min_threads_count, max_threads_count

# Specifies the `worker_timeout` threshold that Puma will use to wait before
# terminating a worker in development environments.
#
worker_timeout 3600 if ENV.fetch("RAILS_ENV", "development") == "development"

# Specifies the `port` that Puma will listen on to receive requests; default is 3000.
#
- port ENV.fetch("PORT") { 3000 }
+ #port ENV.fetch("PORT") { 3000 }
+ bind "unix://#{Rails.root}/tmp/sockets/puma.sock"

# Specifies the `environment` that Puma will run in.
#
environment ENV.fetch("RAILS_ENV") { "development" }

# Specifies the `pidfile` that Puma will use.
pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" }

# Specifies the number of `workers` to boot in clustered mode.
# Workers are forked web server processes. If using threads and workers together
# the concurrency of the application would be max `threads` * `workers`.
# Workers do not work on JRuby or Windows (both of which do not support
# processes).
#
# workers ENV.fetch("WEB_CONCURRENCY") { 2 }

# Use the `preload_app!` method when specifying a `workers` number.
# This directive tells Puma to first boot the application and load code
# before forking the application. This takes advantage of Copy On Write
# process behavior so workers use less memory.
#
# preload_app!

# Allow puma to be restarted by `bin/rails restart` command.
plugin :tmp_restart
```

## バックエンド側のルート設定を変更する

- backend/config/environments/development.rb

```diff_ruby
require "active_support/core_ext/integer/time"

Rails.application.configure do
  # Settings specified here will take precedence over those in config/application.rb.

  # In the development environment your application's code is reloaded any time
  # it changes. This slows down response time but is perfect for development
  # since you don't have to restart the web server when you make code changes.
  config.cache_classes = false

  # Do not eager load code on boot.
  config.eager_load = false

  # Show full error reports.
  config.consider_all_requests_local = true

  # Enable server timing
  config.server_timing = true

  # Enable/disable caching. By default caching is disabled.
  # Run rails dev:cache to toggle caching.
  if Rails.root.join("tmp/caching-dev.txt").exist?
    config.action_controller.perform_caching = true
    config.action_controller.enable_fragment_cache_logging = true

    config.cache_store = :memory_store
    config.public_file_server.headers = {
      "Cache-Control" => "public, max-age=#{2.days.to_i}"
    }
  else
    config.action_controller.perform_caching = false

    config.cache_store = :null_store
  end

  # Store uploaded files on the local file system (see config/storage.yml for options).
  config.active_storage.service = :local

  # Don't care if the mailer can't send.
  config.action_mailer.raise_delivery_errors = false

  config.action_mailer.perform_caching = false

  # Print deprecation notices to the Rails logger.
  config.active_support.deprecation = :log

  # Raise exceptions for disallowed deprecations.
  config.active_support.disallowed_deprecation = :raise

  # Tell Active Support which deprecation messages to disallow.
  config.active_support.disallowed_deprecation_warnings = []

  # Raise an error on page load if there are pending migrations.
  config.active_record.migration_error = :page_load

  # Highlight code that triggered database queries in logs.
  config.active_record.verbose_query_logs = true

  # Suppress logger output for asset requests.
  config.assets.quiet = true

  # Raises error for missing translations.
  # config.i18n.raise_on_missing_translations = true

  # Annotate rendered view with file names.
  # config.action_view.annotate_rendered_view_with_filenames = true

  # Uncomment if you wish to allow Action Cable access from any origin.
  # config.action_cable.disable_request_forgery_protection = true

+  Rails.application.config.relative_url_root = "/manage"
end
```

- backend/config.ru

```diff_ruby
# This file is used by Rack-based servers to start the application.

require_relative "config/environment"

- run Rails.application
+ map ActionController::Base.config.relative_url_root || "/" do
+   run Rails.application
+ end
Rails.application.load_server
```

## コンテナを再度立ち上げる

```sh
docker compose up -d
```

# 動作確認

## `http://localhost/`へアクセスしフロントエンド側が表示されることを確認する

![57809f476bd9b40e887f909b6c721ec5-1024x486.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152063/6d95ad97-97e4-5453-c2ba-b86c98881623.png)

## `http://localhost/manage/`へアクセスしバックエンド側が表示されることを確認する

![536721958ea42bc1f3ba9ca40c0a65f8-1024x485.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152063/a4cbc5d4-8cc8-9c55-47e6-726a23b31f7f.png)

# 終わりに

弊社にお仕事を依頼したいお客様がいらっしゃいましたら、以下のフォームもしくはメールにてお気軽にお問い合わせ下さい。

[お問合せ](https://computer-tb.co.jp/contact/)

システム開発部 森岡（morioka_tatsuaki@computer-tb.co.jp）