はじめに
Rails APIモードとNext.jsでMVPまでローカル開発した後に、途中からDocker環境へ移行することに挑戦しました。
本来であれば、最初からDockerの使用を決めて開発をスタートするのがベストプラクティスかもしれません。
しかし、技術のキャッチアップ状況や、実際に開発を進めてみて初めて見えてくる景色もあります。
備忘録も兼ねて、
「これから個人開発を始めようとしている方」
「ローカル開発からDockerへの移行・環境構築に悩んでいる方」
「途中からDockerを導入するとどう開発が進むのか知りたい方」
そんな方々の参考になれば幸いです。
前提条件
開発環境・使用技術
- マシン: MacBook M5 (Apple Silicon)
- バックエンド: Ruby 4.0.1 / Rails 8.1.3 (APIモード, PostgreSQL指定)
- フロントエンド: Next.js 16.2.3 (TypeScript / React)
- データベース: PostgreSQL 18
- その他ツール: RuboCop, RSpec, Biome 導入済み
- 認証方式: JWT認証
ポート番号の設計
- フロントエンド (Next.js): 3000
- バックエンド (Rails): 3001
※ Dockerコンテナ内での、バックエンドポート番号は、3000です。
💡 移行前の状態
ローカル環境にて、バックエンドはrails new back --api -T -d postgresql、フロントエンドはnpx create-next-app@latestでプロジェクトを作成し、すでに主要機能の検証(MVP)を完了している状態からスタートしました。
最終的なフォルダ構成
ルートディレクトリに compose.yml を配置し、フロントとバックをそれぞれ内包するマルチコンテナ構成を目指します。
.
├── compose.yml
├── back/
│ └── Dockerfile.dev
└── front/
└── Dockerfile
実行手順
各種設定ファイルの作成
バックエンド(back)の設定
Rails 8で自動生成される本番用の Dockerfile はそのまま残し、開発用として back/Dockerfile.dev を新規作成します。
FROM ruby:4.0.1
ENV LANG=C.UTF-8
ENV TZ=Asia/Tokyo
RUN apt-get update -qq && \
apt-get install -y \
build-essential \
libpq-dev \
postgresql-client
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN gem install bundler
RUN bundle install
COPY . .
EXPOSE 3000
CMD ["rails", "server", "-b", "0.0.0.0"]
フロントエンド(front)の設定
フロントエンド直下に front/Dockerfile を作成します。
FROM node:26
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
ルートディレクトリ(compose.yml)の設定
プロジェクトのルート直下にマルチコンテナを管理する compose.yml を作成します。
services:
db:
image: postgres:18
environment:
POSTGRES_DB: app_development
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql
back:
build:
context: ./back
dockerfile: Dockerfile.dev
volumes:
- ./back:/app
ports:
- "3001:3000"
depends_on:
- db
command: bundle exec rails server -b '0.0.0.0'
tty: true
stdin_open: true
environment:
- RAILS_ENV=development
front:
build:
context: ./front
dockerfile: Dockerfile
volumes:
- ./front:/app
- /app/node_modules
ports:
- "3000:3000"
env_file:
- front/.env
depends_on:
- back
command: sh -c "npm install && npm run dev"
volumes:
db_data:
Railsのデータベース接続設定
back/config/database.yml を修正し、接続先ホストをDockerコンテナのサービス名(db)に変更します。
default: &default
adapter: postgresql
encoding: unicode
max_connections: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
host: db
username: postgres
password: password
development:
<<: *default
database: app_development
test:
<<: *default
database: app_test
production:
primary: &primary_production
<<: *default
database: back_production
username: back
password: <%= ENV["BACK_DATABASE_PASSWORD"] %>
cache:
<<: *primary_production
database: back_production_cache
migrations_paths: db/cache_migrate
queue:
<<: *primary_production
database: back_production_queue
migrations_paths: db/queue_migrate
cable:
<<: *primary_production
database: back_production_cable
migrations_paths: db/cable_migrate
コマンドの実行とDBセットアップ
設定が完了したら、ルートディレクトリでコンテナをビルド・起動します。
# コンテナのビルド
docker compose build
# コンテナの起動(バックグラウンドで起動する場合は -d を付与)
docker compose up
コンテナ内へのアクセス方法
デバッグやコマンド実行の際、必要に応じて各コンテナに入ることができます。
# バックエンドコンテナに入る
docker compose exec back bash
# フロントエンドコンテナに入る
docker compose exec front bash
# データベースコンテナに入る
docker compose exec db bash
# コンテナから抜けるとき
exit
データベースの初期化
コンテナを立ち上げただけではデータベースが存在しないためエラーになります。
初回のみ以下のコマンドでDBの作成とマイグレーションを実行します。
# backコンテナに入って実行する場合
docker compose exec back bash
bin/rails db:create
bin/rails db:migrate
bin/rails db:seed # 必要に応じて
# または、コンテナに入らず直接1行で実行することも可能
docker compose exec back bin/rails db:create db:migrate
遭遇したエラーと対処法
ここからは、ローカルからDocker環境へ移行するにあたって、実際に私が直面したエラーと解決するまでの泥臭いプロセスをまとめています。
PostgreSQL 18系のマウントパス仕様変更エラー
docker compose up を実行した際、db コンテナだけが以下のログを吐いて落ちてしまいました。
db-1 | Error: in 18+, these Docker images are configured to store database data in a
db-1 | format which is compatible with "pg_ctlcluster" (specifically, using
db-1 | major-version-specific directory names). This better reflects how
db-1 | PostgreSQL itself works, and how upgrades are to be performed.
db-1 |
db-1 | See also https://github.com/docker-library/postgres/pull/1259 db-1 |
db-1 | Counter to that, there appears to be PostgreSQL data in:
db-1 | /var/lib/postgresql/data (unused mount/volume)
db-1 |
db-1 | This is usually the result of upgrading the Docker image without
db-1 | upgrading the underlying database using "pg_upgrade" (which requires both db-1 | versions).
db-1 |
db-1 | The suggested container configuration for 18+ is to place a single mount db-1 | at /var/lib/postgresql which will then place PostgreSQL data in a
db-1 | subdirectory, allowing usage of "pg_upgrade --link" without mount point db-1 | boundary issues.
db-1 |
db-1 | See https://github.com/docker-library/postgres/issues/37 for a (long)
db-1 | discussion around this process, and suggestions for how to do so.
db-1 exited with code 1
原因と対策
PostgreSQL 18公式Dockerイメージではデータディレクトリの運用方針が変更され、従来の /var/lib/postgresql/data ではなく、 /var/lib/postgresql をマウントすることが推奨されるようになったようです。
compose.yml を以下のように修正することで無事に起動しました。
# 修正前
volumes:
- db_data:/var/lib/postgresql/data
# 修正後
volumes:
- db_data:/var/lib/postgresql
Dockerの容量不足によるinitdbエラー
容量不足が原因でエラーとなりました。
db-1 | The files belonging to this database system will be owned by user "postgres".
db-1 | This user must also own the server process.
db-1 |
db-1 | The database cluster will be initialized with locale "en_US.utf8".
db-1 | The default database encoding has accordingly been set to "UTF8".
db-1 | The default text search configuration will be set to "english".
db-1 |
db-1 | Data page checksums are enabled.
db-1 | initdb: error: could not create directory "/var/lib/postgresql/18/docker/pg_wal": No space left on device
db-1 | initdb: removing contents of data directory "/var/lib/postgresql/18/docker"
db-1 |
db-1 | fixing permissions on existing directory /var/lib/postgresql/18/docker ... ok
db-1 exited with code 1
No space left on deviceよりDockerの容量が不足しているのがわかります。
対策
docker system df コマンドでストレージの使用状況を確認したところ、過去のビルドキャッシュや未使用のボリューム(Local Volumes:95%)がディスクを圧迫していました。
TYPE TOTAL ACTIVE SIZE RECLAIMABLE
Images 41 5 16.57GB 10.65GB (64%)
Containers 5 2 540B 6B (1%)
Local Volumes 298 3 30.98GB 29.62GB (95%)
Build Cache 138 0 12.75GB 3.04GB
以下のクリーンアップコマンドを実行し、不要な領域を解放することで解消しました。
# 未使用のコンテナ、イメージ、ネットワークの削除
docker system prune -a
# 未使用のボリュームの削除(古いデータが消えるので注意)
docker volume prune
Next.jsの型定義・モジュール未認識エラー
Docker環境作成後、フロントエンドで以下のような node_modules 起因のエラーが多発しました。
- 名前 'process' が見つかりません。ノードの型定義をインストールする必要がありますか? npm i --save-dev @types/node を試してから、tsconfig の型フィールドに 'node' を追加してみてください。ts(2591)
- モジュール 'react' またはそれに対応する型宣言が見つかりません。ts(2307)
- この JSX タグにはモジュール パス 'react/jsx-runtime' が存在する必要がありますが、見つかりませんでした。適切なパッケージの種類がインストールされていることを確認してください。ts(2875)
原因と対策
ローカル環境の node_modules と、Dockerコンテナ内の環境で依存関係が正しく同期、またはインストールされていなかったことが原因でした。
compose.yml の front サービスの起動コマンドに npm install を明示的に挟むことで、コンテナ起動時に確実に最新のパッケージがインストールされるように修正しました。
# 修正前
command: npm run dev
# 修正後
command: sh -c "npm install && npm run dev"
サーバー間通信(fetch)時の ECONNREFUSED & HostAuthorization エラー
今回の移行で一番の難所だったのが、Next.jsからRails APIへの通信部分です。
発生した現象
Next.jsのServer Actions(またはSSR)内からの通信で connect ECONNREFUSED 127.0.0.1:3001 が発生。さらにそれを修正しようとすると、Rails側で Blocked hosts: back:3000(HostAuthorizationエラー)が発生しました。
connect ECONNREFUSED 127.0.0.1:3001
front-1 | ⨯ [TypeError: fetch failed] {
front-1 | [cause]: Error: connect ECONNREFUSED 127.0.0.1:3001
front-1 | at <unknown> (Error: connect ECONNREFUSED 127.0.0.1:3001) {
front-1 | errno: -111,
front-1 | code: 'ECONNREFUSED',
front-1 | syscall: 'connect',
front-1 | address: '127.0.0.1',
front-1 | port: 3001
front-1 | }
front-1 | }
Blocked hosts: back:3000(HostAuthorizationエラー)
[ActionDispatch::HostAuthorization::DefaultResponseApp] Blocked hosts: back:3000
問題が発生していたコード
"use server";
import { getBackendJwt } from "./getBackendJwt";
export default async function fetchCategories() {
const jwt = await getBackendJwt();
// ローカル開発時の感覚で「localhost:3001」を指定していた
const res = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/categories`, {
headers: {
Authorization: `Bearer ${jwt}`,
},
method: "GET",
cache: "no-store",
});
const categories = await res.json();
return categories;
}
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api/v1
解決方法
Dockerコンテナ間における localhost は「自分自身のコンテナ」を指してしまいます。Next.jsコンテナから見た通信相手(Rails)は localhost ではなく、Dockerネットワーク内のサービス名である back になります。
また、今回は "use server"(サーバーサイド処理)からのフェッチだったため、宛先ポートはホスト側(3001)ではなくコンテナ内部のポート(3000)を指定する必要がありました。
フロントエンドの環境変数を修正
# 修正前
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001/api/v1
# 修正後(Next.jsサーバーからRailsコンテナへ直接通信させる)
NEXT_PUBLIC_API_BASE_URL=http://back:3000/api/v1
Rails側の許可ホストを設定
Dockerネットワーク経由のホスト名(back)からのリクエストを許可するため、開発環境の設定にホストを追加します。
config.hosts << "back:3000"
設定後にDocker再起動を行います。
おわりに
「Docker環境の構築は一番最初に行うもの」という固定観念がありましたが、MVPまで開発した後からでも十分に整備・移行が可能であると自力で証明できたことは、大きな自信になりました。
また、サーバー間通信のエラーで5日間くらい溶かして、一時は投げ出してやる気がなくなりました。
それでもコンテナのネットワーク構造を一つずつ紐解いたり、別のことを進めたり、Dockerを再起動させたら、思ってない形でエラー解消に辿り着きました。
今回、後付けでDocker環境を整備してみて考えたことですが、「途中からDocker環境整備するのは全然アリ」だと思います。
もちろん、最初から組んでおいたほうが依存関係のズレや余計なネットワークエラーにハマるリスクを減らせるので理想的です。
しかし、「Dockerの勉強に時間を取られて開発が全然進まない……」となってしまうくらいなら、まずはローカルで勢いよくMVPを作り上げ、後からこの記事を参考にDocker化するアプローチも、個人開発の戦略として非常に現実的だと感じました。
この記事が、同じように環境構築で悩む誰かの助けになれば幸いです。もし記述に誤りなどがあれば、コメント等でご教示いただけますと幸いです。