ローカル環にて、Docker + Docker compose + Rails8 + Nginx + MySQL8の環境を構築

# 前書き
ローカル環境にて、表題の通り環境を構築したので、その構築方法について記載します。

:::note
`Rails 8` では、`Thruster`と言う`HTTP/2`プロキシサーバーが、構築時から導入されている為、必ずしも`Nginx`等のリバースプロシキを導入する必要はないです。それを、理解した上で今回は構築を行なっております。

**用語説明**

| 用語 | 説明 |
| :-: | - |
| TCP | インターネットでデータを確実に届ける為の通信プロトコル（規則）。YouTubeの動画視聴、Webページの閲覧、メールの送受信等、正確なデータ転送を行う際に使用する重要な仕組み。 |
| HTTP/2 | 1つの`TCP`接続を使って複数のリクエストを同時に処理が出来る為、`HTTP/1.1`で発生していた「リクエストの順番待ち（ヘッドオブラインブロッキング）」の問題を解消。<br>更に、データ圧縮 や サーバープッシュ（サーバーからクライアントへ事前にデータを送る仕組み） により、Webページの表示速度が向上する。 |
| Thruster | `Rails 8`に導入された `HTTP/2`対応のリバースプロキシサーバー。従来`Nginx`が担っていた`HTTP/2`処理や負荷分散を`Rails`環境に最適化。静的ファイルの配信や`HTTPS`の終端処理 (`SSL/TLS`の処理) も可能。 |
| プロキシサーバー | クライアント（ブラウザなど）とサーバーの間に入り、リクエストを中継するサーバー。通信の最適化やキャッシュ、セキュリティ向上を目的とする。**フォワードプロキシ**と**リバースプロキシ**の2種類がある。 |
| リバースプロキシ | クライアントからのリクエストを受け取り、適切なバックエンドサーバー（`Puma`等）へ転送するプロキシ。負荷分散 (ロードバランシング) や`HTTP/2`対応、静的ファイルの配信を効率化出来る。 |
| Nginx | 高速な`HTTP`サーバー兼リバースプロキシ。静的ファイルの配信、`HTTP/2`対応、ロードバランシング、`SSL/TLS`終端処理等に優れる。`Rails`では`Puma`の前段に置いて、処理の最適化を行うのが一般的だったが、`Rails 8`の`Thruster`により不要になるケースも増える。 |
:::


# 構築手順
## 必要ファイルの作成
- 任意のディレクトリを作成後、そのディレクトリー内で下記のコマンドを実行する

```sh
mkdir -p backend/web1/settings backend/nginx/conf backend/nginx/error_page db/conf phpmyadmin/conf && \
touch backend/web1/settings/.env && \
touch backend/web1/settings/entrypoint.sh && \
touch backend/web1/Dockerfile.dev && \
touch backend/web1/Gemfile && \
touch backend/web1/Gemfile.lock && \
touch backend/nginx/conf/web1.conf && \
touch backend/nginx/error_page/404.html && \
touch backend/nginx/error_page/422.html && \
touch backend/nginx/error_page/500.html && \
touch backend/nginx/Dockerfile && \
touch db/conf/.env && \
touch phpmyadmin/conf/.env && \
touch compose.yml && \
touch README.md && \
touch .gitignore
```

- 上記コマンドを実行した後のディレクトリー構成は下記の状態と
```sh
.
├── backend/
│   ├── web1/
│   │   ├── settings/
│   │   │   ├── env
│   │   │   └── entrypoint.sh
│   │   ├── Dockerfile.dev
│   │   ├── Gemfile
│   │   └── Gemfile.lock
│   └── nginx/
│       ├── conf/
│       │   └── web1.conf
│       ├── error_page/
│       │   ├── 404.html
│       │   ├── 422.html
│       │   └── 500.html
│       └── Dockerfile
├── db/
│   └── conf/
│       └── .env
├── phpmyadmin/
│   └── conf/
│       └── .env
├── compose.yml
├── .gitignore
└── README.md
```

## .gitignore
- `.gitignore`に、`git`へ含めたくないファイルの情報を記載する

```.gitignore
# db関連
mysql_volume

# .env関連
db/conf/.env
phpmyadmin/conf/.env
backend/web1/settings/.env

# その他
.DS_Store
```

## `Gem`関連の設定

- Gemfileに下記を記載
```sh
source "https://rubygems.org"
# 新規で立ち上げる際は、最新バージョンにする
gem "rails", "~> 8.0.1"
```

- `Gemfile.lock`は、何も記載しない
```sh
# 何も記載しない
```

- `entrypoint.sh`に下記を記載
```settings/entrypoint.sh
#!/bin/bash
set -e

# 古いPIDファイルやソケットファイルを削除
rm -rf /web1/tmp/{pids,sockets}

# 必要なディレクトリを作成
mkdir -p /web1/tmp/{pids,sockets}

# マイグレーションを実行
bundle exec rails db:migrate

exec "$@"
```

- `.env`下記を記載
- 下記の設定は、`DB`を接続する際に必要
```settings/.env
HOST=db
DATABASE=web1_development
USER_NAME=root
PASSWORD=password
```

## Docker関連の構築
- `Rails`を起動する`Dockerfile`に、下記を記載
```Dockerfile
# ベースイメージを指定
ARG RUBY_VERSION=3.4.1
FROM ruby:${RUBY_VERSION}-slim-bookworm

# 環境変数の設定
ENV LANG=C.UTF-8 \
    TZ=Asia/Tokyo \
    RUBY_YJIT_ENABLE=1 \
    RAILS_ENV=development \
    APP_ROOT_PATH=/web1

# 必要なパッケージをインストール
RUN apt-get update && apt-get install -y \
    build-essential \
    libgmp-dev \
    libssl-dev \
    libmariadb-dev-compat \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

# 作業ディレクトリを設定
WORKDIR ${APP_ROOT_PATH}

# Gemfile と Gemfile.lock をコピーし、依存関係をインストール
COPY Gemfile Gemfile.lock ${APP_ROOT_PATH}/
RUN bundle install

# プロジェクトのソースコードをコピー
COPY . ${APP_ROOT_PATH}/

# エントリポイントスクリプトをコピーし、実行可能にする
COPY settings/entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
```

## Docker composeの構築
-  `Rails`の環境は、デバッグが出来る様に構築
-  `phpmyadmin`は、`DB`の中身を可視化する為に入れています
```compose.yml
services:
  nginx:
    container_name: nginx
    image: nginx:latest
    build: ./backend/nginx
    ports:
      - "80:80"
    volumes:
      - tmp_volume:/web1/tmp
    depends_on:
      - web1
  web1:
    container_name: web1
    image: web1:latest
    build:
      context: ./backend/web1
      dockerfile: Dockerfile.dev
    env_file:
      - ./backend/web1/settings/.env
    volumes:
      - ./backend/web1:/web1
      - tmp_volume:/web1/tmp
    ports:
      - 1111:1111  
    depends_on:
      - db
    tty: true
    stdin_open: true
    command: >
      bash -c "
        bundle install &&
        rails db:migrate &&
        rdbg -n --open --host 0.0.0.0 --port 1111 -c -- bundle exec pumactl start
      "
  db:
    container_name: mysql
    image: mysql:latest
    env_file: ./db/conf/.env
    volumes:
      - ./db/mysql_volume:/var/lib/mysql
    ports:
      - "3306:3306"
    restart: always
  phpmyadmin:
    container_name: phpmyadmin
    image: phpmyadmin:latest
    restart: always
    depends_on:
      - db
    env_file:
      - ./phpmyadmin/conf/.env  
    ports:
      - "8080:80"

volumes:
  tmp_volume:
    driver: local
    driver_opts:
      type: tmpfs
      device: tmpfs
```

### volumes内で使用しているメソッドと説明

| メソッド | 説明 |
| :-: | - |
| volumes | `Docker`コンテナで使用するボリュームを定義する。ボリュームを指定する事で、コンテナが停止・削除されてもデータを保持できる。ただし、一時的なボリューム（今回の場合は、`tmpfs`）を指定すると、コンテナ停止時にデータが消える。 |
| tmp_volume | `volumes`で定義されたボリュームの名前。コンテナ内で、この名前のボリュームをマウント(接続)できる。この設定では、一時的なメモリ上のストレージとして機能する。 |
| driver | ボリュームの管理方法（ドライバ）を指定する。`local`を指定すると、ホストマシンのストレージ（ディスクやメモリ）を利用する。特に、`tmpfs`と組み合わせるとメモリ上にデータを保持し、高速アクセスが可能になる。 |
| driver_opts | ボリュームの詳細な設定を行う為のオプション。ここで指定した内容により、ボリュームの動作が変わる。例えば、`type`=`tmpfs`を指定すると、ボリュームがメモリ上に作成され、ディスクを使用しない一時的なストレージとして機能する。 |
| type | ボリュームの種類を指定する。`tmpfs`を指定すると、ボリュームがRAM（メモリ）上に作成され、データの読み書き速度が高速になる。ただし、コンテナが停止するとデータは消える。 |
| device | 使用するストレージのデバイスを指定する。ここでは、`tmpfs`を指定している為、ディスクではなくメモリをストレージとして扱う事になる。これにより、ディスク`I/O`を減らし、高速なデータ処理が可能になる。 |


## Nginxの構築
- `Nginx`用の`Dockerfile`に、下記の内容を記載
```Dockerfile
FROM nginx:latest

RUN apt-get update && \ 
    apt-get upgrade -y && \ 
    apt-get install -y \ 
    curl && \ 
    rm -rf /var/lib/apt/lists/*
RUN rm -f /etc/nginx/conf.d/default.conf

COPY ./conf/web1.conf /etc/nginx/conf.d/web1.conf
COPY ./error_page /usr/share/nginx/html

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

```

- `conf/web1.conf`に、下記の内容を記載
```nginx
# unixドメインソケット通信でpumaとnginxを接続する
upstream web1 {
	server unix:///web1/tmp/sockets/puma.sock;
}

server {
	listen 80;
	server_name localhost;

  location / {
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_redirect off;
    proxy_pass http://web1;
    proxy_intercept_errors on;
  }

  # エラーページの設定
  error_page 404 /404.html;
  error_page 422 /422.html;
  error_page 500 502 503 504 /500.html;

  # 静的エラーページの内部設定
  location ~ ^/(404|422|500)\.html$ {
    root /usr/share/nginx/html;
    internal;
  }
}
```
- `404.html`、`422.html`、`500.html`を作成する
```404.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>404 - ページが見つかりません</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 50px;
        }
        h1 {
            font-size: 50px;
        }
        p {
            font-size: 20px;
        }
        a {
            color: #007BFF;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h1>404</h1>
    <p>お探しのページは見つかりませんでした。</p>
</body>
</html>
```

```422.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>422 - 処理できない要求</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 50px;
        }
        h1 {
            font-size: 50px;
        }
        p {
            font-size: 20px;
        }
        a {
            color: #007BFF;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h1>422</h1>
    <p>リクエストを処理できませんでした。</p>
</body>
</html>
```

```500.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>500 - サーバーエラー</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            text-align: center;
            padding: 50px;
        }
        h1 {
            font-size: 50px;
        }
        p {
            font-size: 20px;
        }
        a {
            color: #007BFF;
            text-decoration: none;
        }
        a:hover {
            text-decoration: underline;
        }
    </style>
</head>
<body>
    <h1>500</h1>
    <p>内部サーバーエラーが発生しました。</p>
</body>
</html>
```

## db(MySQL)の設定
- `db`ディレクトリー内にある`.env`ファイルへ、下記を記載する

```conf/.env
MYSQL_DATABASE=web1_development
MYSQL_ROOT_PASSWORD=password
TZ=Asia/Tokyo
```

## phpMyAdmin
- `phpmyAdmin`ディレクトリー内にある`.env`ファイルへ、下記を記載する

```conf/.env
PMA_ARBITRARY=1
PMA_HOSTS=db
PMA_USER=root
PMA_PASSWORD=password
```


# 起動手順
## 新しい`Rails`アプリケーションを作成する
- 下記のコマンドを`compose.yml`ファイルの階層にてコマンドを実行する
- `web1`のディレクトリー内に、`Rails`を立ち上げる為に必要なファイルとディレクトリーが生成される
```sh
docker compose run web1 rails new . \
  --force \
  --no-deps \
  --database=mysql \
  --css=tailwind \
  --skip-action-mailbox \
  --skip-action-mailer \
  --skip-test
```
`Rails`を立ち上げる為のコマンドと説明

| コマンド | 説明 |
| :-: | - |
| docker compose run | `docker-compose.yml`の定義に基づいて、コンテナを作成・実行する |
| web1 | `docker-compose.yml`で定義された `web1`サービスのコンテナ内でコマンドを実行する |
| rails new . | カレントディレクトリに新しい`Rails`アプリを作成する |
| --force | 既存のファイルがあっても上書きする |
| --no-deps | 依存する他のコンテナ（例: `db` 等）を起動せずに `web1`のみを実行する |
| --database=mysql | データベースとして`MySQL`を使用するように`Rails`アプリを設定する |
| --css=tailwind | `CSS`フレームワークとして`Tailwind CSS`を使用する |
| --skip-action-mailbox | `Action Mailbox`（メール受信機能）のファイルを生成しない |
| --skip-action-mailer | `Action Mailer`（メール送信機能）のファイルを生成しない |
| --skip-test | デフォルトのテストファイルを生成しない |

- `Gemfile`に、環境変数を管理する為の`RubyGem`を記載する
- 詳細 : [[GitHub] bkeepers/dotenv](https://github.com/bkeepers/dotenv)
```sh
gem 'dotenv-rails'
```

- 下記のファイルに、以下の修正を行う
- `backend/web1/settings/.env`にある`.env`を読み込ませる設定を加える
```backend/web1/config/application.rb

# 省略

# Require the gems listed in Gemfile, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)

if %w[development test].include?(Rails.env)
  Dotenv.load(File.expand_path('../settings/.env', __dir__))
end

module Web1

# 省略

```

- `database.yml`を下記の様に修正

:::note warn
`database.yml`の注意点
- `development`には、直接データーベースの設定を書いても良い
- `credentials.yml.enc`をデーターベースの設定を書いて運用しても良い
:::

```backend/web1/config/database.yml
default: &default
  adapter: mysql2
  encoding: utf8mb4
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  host:     <%= ENV["HOST"] %>
  database: <%= ENV["DATABASE"] %>
  username: <%= ENV["USER_NAME"] %>
  password: <%= ENV["PASSWORD"] %>

test:
  <<: *default
  database: web1_test

production:
  primary: &primary_production
    <<: *default
    database: web1_production
    username: web1
    password: <%= ENV["WEB1_DATABASE_PASSWORD"] %>
  cache:
    <<: *primary_production
    database: web1_production_cache
    migrations_paths: db/cache_migrate
  queue:
    <<: *primary_production
    database: web1_production_queue
    migrations_paths: db/queue_migrate
  cable:
    <<: *primary_production
    database: web1_production_cable
    migrations_paths: db/cable_migrate
```

- `puma`の設定を修正
```backend/web1/config/puma.rb
threads_count = ENV.fetch("RAILS_MAX_THREADS", 3)
threads threads_count, threads_count
port ENV.fetch("PORT", 3000)
plugin :tmp_restart
plugin :solid_queue if ENV["SOLID_QUEUE_IN_PUMA"]
pidfile ENV["PIDFILE"] if ENV["PIDFILE"]

# 下記を追加する
app_root = File.expand_path("..", __dir__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"
```

- 下記の順番でコンテナーを立ち上げる
    1. 起動しているコンテナーを全て立ち下げる
    2. 再度、ビルドを行い、イメージを更新する
    3. コンテナを起動する

```sh
# コンテナーの立ち下げ
docker rm $(docker ps -aq)

# イメージをビルド
docker compose build --no-cache

# 不要なイメージを消去
docker images -f "dangling=true" -q | xargs docker rmi

# コンテナーの起動
docker compose up -d
```

- URLを叩くとRailsの初期画面が表示される
- http://localhost/

![スクリーンショット 2025-02-15 0.57.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/664659/edd55f4b-e728-4727-9464-0c4e4fd22c7e.png)


# GitHub
https://github.com/kengo-sk8/rails_nginx_app




# おまけ
- デバッグ機能を付与する為、web1の中に`.vscode`を作成した後、`launch.json`を作成する
- `launch.json`に下記の内容を追記する
- デバッグの設定の詳細は、[VSCode + Docker + Docker-compose +Railsのデバッグの設定方法](https://qiita.com/kengo-sk8/items/cae0d2063352f50e69fd) を見て下さい
```.vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "rdbg",
      "name": "Debug Ruby",
      "request": "attach",
      "debugPort": "localhost:1111",
      "localfsMap": "/web1:${workspaceFolder}",
    },
  ]
}
```

# 参考記事
- [VSCode + Docker + Docker-compose +Railsのデバッグの設定方法](https://qiita.com/kengo-sk8/items/cae0d2063352f50e69fd)
- [【初心者向け】Rails 7+MySQL 8.0の開発環境をDocker composeで作る方法](https://musclecoding.com/rails7-mysql8-docker/)
- [サクッとDocker+Rails7.13+Mysqlの環境構築](https://zenn.dev/kei1232/articles/0fac51829570c1)
- [DockerでNginx、Puma(Rails7.0)の環境構築](https://itoka.hatenadiary.com/entry/2022/03/04/004453)
- [【速攻】DockerでMySQLとphpMyAdminのコンテナ作成](https://zenn.dev/peishim/articles/f7a76ae6c253e4)
- [Ruby on RailsをNginxで起動](https://job-info.hateblo.jp/entry/2024/08/23/201711)
- [[Railsドキュメント] 新しいRailsのアプリケーションを作成](https://railsdoc.com/page/rails_new)
- [[Docker Hub] phpmyadmin](https://hub.docker.com/_/phpmyadmin)
- [【速攻】DockerでMySQLとphpMyAdminのコンテナ作成](https://zenn.dev/peishim/articles/f7a76ae6c253e4)


# 感想
今回、以前から挑戦したかった**Rails**と**Nginx**の**ソケット通信**をローカル環境で構築しました。特に重要だったのは、ボリュームを利用して**2**つのコンテナ(`Rails`と`Nginx`)を共有させるという構成です。当初はこの仕組みが理解できず苦戦しましたが、試行錯誤を重ねる中で徐々に構造を把握し、最終的に念願だった環境を構築する事ができました。

今回の構成の様に、アプリケーションサーバーの前段で処理を行う手法には、様々な選択肢があります。引き続き理解を深め、より最適な環境構築に取り組んでいきたいと思います。