記事概要
本記事では、Docker および Docker Compose を用いて、単一サーバー上において Nginx をリバースプロキシとして利用し、複数の Ruby on Rails アプリケーションを同時に動作させる開発環境の構築手法を示す。
本構成により、以下の事項について実践的な理解を深めることを目的とする。
- 単一マシン上で複数の Rails アプリケーションを安全に公開する構成手法
- リバースプロキシの動作原理およびリクエストルーティングの仕組み
- Docker 環境におけるポートフォワーディングおよび Host ヘッダーの挙動
リバースプロシキの概要
利用者(ブラウザ)と Web アプリケーションの間に入り、通信を中継するサーバーの仕組み。
通常は、ブラウザから直接 Ruby on Rails などのアプリケーションサーバーにアクセスする。
しかし、リバースプロキシを利用することで、ブラウザは Nginx にアクセスし、Nginx が裏側の Rails サーバーへ リクエストを転送する構成となる。
この仕組みにより、Nginx が通信の窓口となり、適切な Rails アプリケーションへリクエストを振り分ける役割を担う。
リバースプロキシのメリット
- 複数の Web アプリを 1 つのサーバーで公開できる
- SSL(HTTPS)設定を Nginx に集約できる
- 負荷分散やキャッシュなどの高度な機能を利用できる
- アプリケーションサーバーを直接インターネットに公開しないで使用できる
プロキシとの違い
リバースプロキシは、プロキシとは通信方向が逆であるため「リバース」と呼ばれる。
- プロキシはクライアント側の代理サーバー
- リバースプロキシはサーバー側の代理サーバー
構築時の前提条件
- Dockerが正常に動作する
- 2台以上のRailsサーバーを用意する
構築手順
構築完了後のディレクトリー構成は以下の構成とする。
ディレクトリー構成
.
├── backend/
│ ├── nginx/ # リバースプロキシ
│ │ ├── conf/web.conf # Nginx 設定
│ │ ├── error_page/ # カスタムエラーページ
│ │ └── Dockerfile
│ │
│ ├── web1/ # Rails アプリ1
│ ├── web2/ # Rails アプリ2
│ └── web3/ # Rails アプリ3
│
├── db/ # PostgreSQL 永続化領域
│ ├── conf/.env # DB 環境変数
│ └── postgresql_volume/ # コンテナー立ち上げ時に自動生成
│ └── 18/docker/
│
├── pgadmin/ # 任意で導入
│ ├── conf/.env
│ └── pgadmin_volume/ # コンテナー立ち上げ時に自動生成
│
├── compose.yml
├── .gitignore
└── README.md
Dockerfileの設定
以下のディレクトリに、それぞれ同一内容の Dockerfile を作成する。ただし、APP_ROOT の値のみ各ディレクトリ名に対応した値へ変更する。それ以外の設定はすべて共通とする。
backend/web1/Dockerfilebackend/web2/Dockerfilebackend/web3/Dockerfile
注意
本記事では構成の簡略化のため同一内容の Dockerfile を使用している。
実運用や検証用途に応じて、各アプリケーションごとに Dockerfile の内容を変更しても問題ない。
ARG RUBY_VERSION=4.0.1
FROM ruby:${RUBY_VERSION}-slim-trixie
ENV LANG=C.UTF-8 \
TZ=Asia/Tokyo \
RUBY_YJIT_ENABLE=1 \
RUBY_ENV=development \
APP_ROOT=/web # ← 変更部分
RUN apt-get update -qq && apt-get install -y \
build-essential \
libpq-dev \
libyaml-dev \
pkg-config \
git \
nodejs \
curl \
npm \
vim \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR ${APP_ROOT}
COPY Gemfile Gemfile.lock ./
RUN gem install bundler && \
bundle install --jobs 4 --retry 3
COPY . .
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]
Docker-composeの設定
ルートディレクトリーに配置し、以下の設定を入れる。
注意
複数コンテナから同時にマイグレーションを実行すると、ActiveRecord の排他制御(同時実行を防ぐためのロック機構)により ConcurrentMigrationError が発生する。そのため、
services:
db:
container_name: postgresql
image: postgres:latest
env_file: ./db/conf/.env
volumes:
- ./db/postgresql_volume:/var/lib/postgresql
ports:
- "5432:5432"
restart: always
pgadmin:
container_name: pgadmin
image: dpage/pgadmin4
restart: always
volumes:
- ./pgadmin/pgadmin_volume:/var/lib/pgadmin
env_file:
- ./pgadmin/conf/.env
ports:
- 8081:80
depends_on:
- db
nginx:
container_name: nginx
build: ./backend/nginx
image: nginx:latest
pull_policy: never
ports:
- 81:81
- 82:82
- 83:83
volumes:
- web1_socket_volume:/web1/tmp
- web2_socket_volume:/web2/tmp
- web3_socket_volume:/web3/tmp
depends_on:
- web1
- web2
- web3
web1:
container_name: web1
image: web1:latest
pull_policy: never
build:
context: ./backend/web1
dockerfile: Dockerfile
ports:
- 1111:1111
volumes:
- ./backend/web1:/web1
- web1_socket_volume:/web1/tmp
environment:
APP_NAME: web1
depends_on:
- db
tty: true
stdin_open: true
command: >
bash -c "
rm -rf /web1/tmp/{pids,sockets} &&
mkdir -p /web1/tmp/{pids,sockets} &&
rails db:migrate &&
bundle exec pumactl start
"
web2:
container_name: web2
image: web2:latest
build:
context: ./backend/web2
dockerfile: Dockerfile
pull_policy: never
ports:
- 2222:2222
volumes:
- ./backend/web2:/web2
- web2_socket_volume:/web2/tmp
environment:
APP_NAME: web2
depends_on:
- db
tty: true
stdin_open: true
command: >
bash -c "
rm -rf /web2/tmp/{pids,sockets} &&
mkdir -p /web2/tmp/{pids,sockets} &&
bundle exec pumactl start
"
web3:
container_name: web3
image: web3:latest
build:
context: ./backend/web3
dockerfile: Dockerfile
pull_policy: never
ports:
- 3333:3333
volumes:
- ./backend/web3:/web3
- web3_socket_volume:/web3/tmp
environment:
APP_NAME: web3
depends_on:
- db
tty: true
stdin_open: true
command: >
bash -c "
rm -rf /web3/tmp/{pids,sockets} &&
mkdir -p /web3/tmp/{pids,sockets} &&
bundle exec pumactl start
"
x-tmpfs-volume: &tmpfs-volume
driver: local
driver_opts:
type: tmpfs
device: tmpfs
volumes:
web1_socket_volume:
name: web1_puma_socket
<<: *tmpfs-volume
web2_socket_volume:
name: web2_puma_socket
<<: *tmpfs-volume
web3_socket_volume:
name: web3_puma_socket
<<: *tmpfs-volume
Top-level volumes sectionの説明
| メソッド | メソッドの説明 |
|---|---|
x-tmpfs-volume: |
Compose Extension Field(拡張フィールド)x- で始まるキーは Docker Compose に無視される特別な設定。実際の設定をまとめておく「テンプレートの定義(共通設定のひな形)」として利用できる |
&tmpfs-volume |
YAML Anchor 設定ブロックに名前を付ける仕組みで、この設定セットに tmpfs-volume という名前を付ける |
*tmpfs-volume |
YAML Alias Anchor で定義した設定を参照する仕組み。 tmpfs-volume で設定した値をここで使用する |
<<: *tmpfs-volume |
YAML Merge Key Alias で参照した設定内容を現在の設定に展開し、設定を合成する機能。 |
volumes: |
Top-level Volumes Docker Compose で使用する Named Volume(名前付きボリューム)を定義するセクション。複数コンテナ間で共有できるストレージを作成する。 |
name: |
Volume Name Docker 上に作成される実際のボリューム名を固定し、Compose による自動命名(プロジェクト名付き)を防ぐための設定。 |
driver: local |
Volume Driver Docker 標準の local ドライバ(ホスト上のストレージを利用するデフォルト実装)を使用する |
driver_opts: |
Driver Options ボリュームドライバに追加パラメータを渡すための設定項目。 |
driver_opts.type=tmpfsdevice=tmpfs
|
tmpfs ボリューム ディスクではなく RAM(メモリ)上に作成される一時ストレージであり、コンテナ停止やボリューム削除時にデータが消失する。 |
Nginxの設定
本設定により、Nginx と複数の Rails アプリケーション間の通信を構成する。
-
Nginx から
web1・web2・web3の Puma に Unix ソケット経由で接続する - アクセスされたポート番号(
81,82,83)に応じて転送先 Rails アプリを切り替える - ブラウザからの HTTP リクエストを Nginx が受け取り、Rails に中継するリバースプロキシ構成を実現する
Browser
↓ :81 / :82 / :83
Nginx (map で振り分け)
↓ Unix Socket
Puma (web1 / web2 / web3)
backend/nginx/confの配下に、web.confを作成し、以下の内容を追加する。
# web1のコンテナを Puma と Unix ソケットで接続する設定
upstream web1 {
server unix:///web1/tmp/sockets/puma.sock;
}
# web2のコンテナを Puma と Unix ソケットで接続する設定
upstream web2 {
server unix:///web2/tmp/sockets/puma.sock;
}
# web3のコンテナを Puma と Unix ソケットで接続する設定
upstream web3 {
server unix:///web3/tmp/sockets/puma.sock;
}
# ポート番号ごとに転送先の Rails アプリを切り替える設定
map $server_port $upstream {
81 web1;
82 web2;
83 web3;
default web1; # 想定外のポートは web1 にフォールバック
}
server {
# Nginx が受信する TCPポートを定義し、ポート番号に基づくリクエストの分岐を可能にする
listen 81;
listen 82;
listen 83;
server_name localhost; # サーバー名(ローカル開発用)
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # クライアント IP を Rails 側に渡す
proxy_set_header Host $http_host; # Host ヘッダー(ホスト名とポート)を Rails に渡す
proxy_redirect off; # リダイレクトヘッダーを書き換えない
proxy_pass http://$upstream; # ポート番号に応じた upstream(Rails)へ転送
proxy_intercept_errors on; # Rails 側のエラーを Nginx で処理できるようにする
}
# エラーページの設定
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; # HTML ファイルの配置場所
internal; # 外部から直接アクセス不可(内部専用)
}
}
Railsの設定
pumaの設定
以下に設置されているpuma.rbに下記の設定を追加する。この設定により、Rails の **Web サーバー(Puma)**はポートではなく puma.sock というファイルを通じて Nginx からの通信を受け取る。
backend/web1/config/puma.rbbackend/web2/config/puma.rbbackend/web3/config/puma.rb
app_root = File.expand_path("..", __dir__)
bind "unix://#{app_root}/tmp/sockets/puma.sock"
追加した設定のコード説明
| メソッド(コード) | 説明 |
|---|---|
File.expand_path("..", __dir__) |
現在のファイルが置かれているディレクトリの一つ上の階層を取得し、アプリケーションのルートディレクトリの絶対パスを求める。 |
__dir__ |
この Ruby ファイルが保存されているフォルダの場所(パス)を取得するための特別な変数。 |
bind "unix://#{app_root}/tmp/sockets/puma.sock" |
Puma が通信を待ち受ける場所を Unix ソケットファイルに設定する。ポート番号ではなくファイルで通信を受け付ける設定である。 |
Unix ソケットファイルの概要
- 同じサーバー内で動作するプログラム同士が通信するために使用される ファイル形式の通信口
- 通常の Web サーバーはポート番号(例:
3000)を用いて通信を行うが、Unix ソケットファイルはファイルパス(例:/tmp/puma.sock)を通信先として利用する点が特徴
この仕組みは プロセス間通信(IPC: Inter-Process Communication) の一種であり、同一ホスト内で高速かつ安全にデータをやり取りするために利用される。
特に、Nginx と Puma のような Web サーバー構成 では、TCP ポート通信の代わりに Unix ソケットファイルを用いることで、オーバーヘッド(通信のための余計な処理)を削減し、セキュリティを向上させることができる。
database.ymlの設定
以下に配置されている database.yml に、下記の設定を追加する。
backend/web1/config/database.ymlbackend/web2/config/database.ymlbackend/web3/config/database.yml
注意
本記事では構成の簡略化のため、すべての Rails アプリケーションで同一の database.yml を使用している。
実運用や検証用途に応じて、データベース名やユーザー、接続先ホストなどをアプリケーションごとに分離しても問題ない。
default: &default
adapter: postgresql
encoding: unicode
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
port: 5432
development:
<<: *default
host: db
database: web_development
username: test_user1
password: test1234
production:
primary: &primary_production
<<: *default
database: web_production
username: web
password: <%= ENV["WEB1_DATABASE_PASSWORD"] %>
cache:
<<: *primary_production
database: web_production_cache
migrations_paths: db/cache_migrate
queue:
<<: *primary_production
database: web_production_queue
migrations_paths: db/queue_migrate
cable:
<<: *primary_production
database: web_production_cable
migrations_paths: db/cable_migrate
route.rbとviewファイルの設定
任意処理
本手順は、複数の Rails アプリが正しく振り分けられていることを確認するために「表示確認用ページ」として追加した。
そのため、デモや動作確認を目的とする場合にのみ使用する。
既にアプリ側のルーティングや画面が存在する場合、または実運用の構成に組み込む場合は、本手順は不要。
各 Rails アプリのコンテナ内で、以下のコマンドを実行する。
bin/rails g controller Home index
それぞれのコンテナーのconfig/routes.rbを編集する。
Rails.application.routes.draw do
root "home#index"
end
以下に設置されているindex.html.erbに下記の設定を追加する。
backend/web1/app/views/home/index.html.erbbackend/web2/app/views/home/index.html.erbbackend/web3/app/views/home/index.html.erb
<style>
.page-bg {
position: fixed;
inset: 0;
z-index: 9998;
background: radial-gradient(circle at top, #111827, #020617);
}
.card {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999;
background: rgba(17, 24, 39, 0.88);
backdrop-filter: blur(10px);
border-radius: 16px;
padding: 20px 24px;
width: min(520px, calc(100vw - 32px));
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.6);
color: #e5e7eb;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
h1 {
font-size: 30px;
font-weight: 700;
margin: 0 0 34px 0;
color: #facc15;
letter-spacing: -0.02em;
text-align: center;
}
.row {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 40px;
padding: 20px 0;
border-top: 1px solid rgba(255, 255, 255, 0.06);
}
.row:first-of-type {
border-top: none;
}
.label {
font-size: 20px;
font-weight: 650;
color: rgba(229, 231, 235, 0.55);
letter-spacing: 0.01em;
}
.value {
font-size: 20px;
font-weight: 650;
color: #f9fafb;
text-align: right;
}
.mono {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
letter-spacing: 0.01em;
}
.pill {
display: inline-block;
margin-left: 18px;
padding: 10px 20px;
border-radius: 999px;
background: #dc2626;
color: white;
font-size: 18px;
font-weight: 800;
vertical-align: middle;
}
</style>
<div class="page-bg"></div>
<div class="card">
<h1>Rails Application Information</h1>
<div class="row">
<div class="label">App</div>
<div class="value">
<span class="pill">Rails</span>
</div>
</div>
<div class="row">
<div class="label">Container</div>
<div class="value"><%= ENV.fetch("APP_NAME", "web1") %></div>
</div>
<div class="row">
<div class="label">Host</div>
<div class="value"><%= request.host_with_port %></div>
</div>
<div class="row">
<div class="label">Path</div>
<div class="value"><%= request.fullpath %></div>
</div>
</div>
疎通確認
以下の URL にアクセスし、それぞれ対応する Rails アプリケーションの画面が表示されることを確認する。ポート番号ごとに異なる Rails アプリケーションが表示されれば、Nginx のリバースプロキシ設定およびポートベースのルーティングが正しく機能している。
http://localhost:81/
http://localhost:82/
http://localhost:83/
GitHub
参考資料
公式
非公式
- 【Docker × PostgreSQL】18系にアップデートしたらDBコンテナが突然起動しなくなった
- PostgreSQL18コンテナのマウント先の仕様変更
- Unixソケットって何?
- UNIX domain sockets
感想
今回、Nginx 1 台で複数の Rails サーバーにアクセスする構成を構築しました。以前から試してみたかった構成だったため、実際に動かせたことに加えて、記事としてまとめられた点も含めてとても満足しています。
今後は、より実運用に近い構成や、Host ヘッダーやパスベースルーティングなど別のルーティング方式にも挑戦してみたいと思います。


