はじめに
ポートフォリオサイトの開発環境を構築する際、複数のリポジトリを跨ぐマイクロサービス構成にしたいという要件がありました。具体的には、開発用の共通設定を管理する infrastructure リポジトリと、実際のアプリケーションコードを格納する portal リポジトリに分けて管理する形です。
この記事では、Docker Compose と VS Code の Dev Container 機能を組み合わせて、以下を実現する開発環境の構築方法を紹介します。
- Spring Boot バックエンド
- Vite + React フロントエンド
- PostgreSQL データベース
- Nginx リバースプロキシ
- VS Code での統合開発環境
全体構成
まず、全体のディレクトリ構成と Docker 環境の関係を図示します。
ディレクトリ構造は以下のようになっています:
portfolio/
├── infrastructure/ # インフラ設定リポジトリ
│ └── docker/
│ ├── docker-compose.all.yml
│ ├── rebuild-all.sh
│ └── nginx/
│ └── nginx.conf
│
└── portal/ # アプリケーションリポジトリ
├── backend/
│ ├── Dockerfile.dev
│ ├── .devcontainer/
│ │ └── devcontainer.json
│ └── src/...
│
└── frontend/
├── Dockerfile.dev
├── .devcontainer/
│ └── devcontainer.json
└── src/...
この構成にした理由は、Docker Compose ファイルや Nginx の設定など、複数のサービスで共通して使うインフラ設定を infrastructure リポジトリに集約し、各アプリケーションのコードは portal リポジトリで管理することで、責務を明確に分離するためです。
Docker Compose の構成
サービス構成図
Docker Compose で管理する 4 つのサービスの関係性は以下の通りです。
docker-compose.all.yml の設計
infrastructure/docker/docker-compose.all.yml は全サービスを統合管理します。
version: "3.8"
services:
# PostgreSQL
db:
image: postgres:16-alpine
container_name: portfolio-db
ports:
- "5432:5432"
environment:
POSTGRES_DB: portfolio
POSTGRES_USER: dev
POSTGRES_PASSWORD: devpass
volumes:
- pg-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dev"]
interval: 5s
timeout: 5s
retries: 5
networks:
- portfolio-net
# Portal Backend
portal-backend:
build:
context: ../../portal/backend
dockerfile: Dockerfile.dev
container_name: portal-backend
ports:
- "8080:8080"
volumes:
- ../../portal/backend:/app
- maven-cache:/root/.m2
environment:
- SPRING_PROFILES_ACTIVE=dev
- DB_HOST=db
- DB_PORT=5432
- DB_NAME=portfolio
- DB_USER=dev
- DB_PASSWORD=devpass
depends_on:
db:
condition: service_healthy
networks:
- portfolio-net
# Portal Frontend
portal-frontend:
build:
context: ../../portal/frontend
dockerfile: Dockerfile.dev
container_name: portal-frontend
ports:
- "5173:5173"
volumes:
- ../../portal/frontend:/app
- node-modules:/app/node_modules
environment:
- VITE_API_URL=http://localhost:8080/api
networks:
- portfolio-net
# Nginx
nginx:
image: nginx:alpine
container_name: portfolio-nginx
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- portal-backend
- portal-frontend
networks:
- portfolio-net
volumes:
pg-data:
maven-cache:
node-modules:
networks:
portfolio-net:
driver: bridge
ポイントは以下の通りです:
1. 相対パスでの参照
build:
context: ../../portal/backend
dockerfile: Dockerfile.dev
infrastructure リポジトリから portal リポジトリのディレクトリを相対パスで参照しています。これにより、リポジトリが分かれていても問題なくビルドできます。
2. ボリュームマウントでホットリロード
volumes:
- ../../portal/backend:/app
- maven-cache:/root/.m2
ホスト側のソースコードをコンテナにマウントすることで、ファイルを変更すると即座にコンテナ内に反映されます。また、Maven や npm のキャッシュは名前付きボリュームにすることで、ビルド時間を短縮しています。
3. ヘルスチェックと依存関係
depends_on:
db:
condition: service_healthy
データベースが完全に起動してから Backend を起動するように設定しています。これにより、接続エラーを防げます。
4. 開発用ポート公開
開発中は各サービスに直接アクセスできるよう、すべてのポートをホストに公開しています:
- Nginx:
80 - Backend:
8080 - Frontend:
5173 - Database:
5432
本番環境では Nginx の 80 ポートのみ公開すれば十分ですが、開発時はデバッグやテストのために各サービスへ直接アクセスできると便利です。
Dockerfile の設計
Backend (Spring Boot)
portal/backend/Dockerfile.dev は開発用に最適化しています。
FROM eclipse-temurin:21-jdk-alpine
# Dev Container用にbashとgitをインストール
RUN apk add --no-cache bash git
WORKDIR /app
# Mavenラッパーをコピー
COPY mvnw .
COPY .mvn .mvn
# 依存関係を先にダウンロード(キャッシュ効率化)
COPY pom.xml .
RUN ./mvnw dependency:go-offline
# ソースコードはボリュームマウントするのでCOPYしない
# Spring Boot DevToolsによる自動再起動を有効化
ENV SPRING_DEVTOOLS_RESTART_ENABLED=true
EXPOSE 8080
CMD ["./mvnw", "spring-boot:run"]
レイヤーキャッシュの活用
Dockerfile のレイヤーキャッシュを効率的に使うため、変更頻度が低い依存関係のダウンロードを先に実行しています。pom.xml が変わらない限り、この層はキャッシュされるため、再ビルドが高速になります。
Spring Boot DevTools
SPRING_DEVTOOLS_RESTART_ENABLED=true を設定することで、ファイル変更時の自動再起動が有効になります。実際に使ってみると、Java ファイルを保存してから数秒でアプリケーションが再起動するため、非常にスムーズです。
Frontend (Vite + React)
portal/frontend/Dockerfile.dev も同様に開発用に最適化しています。
FROM node:20-alpine
# Dev Container用にbashとgitをインストール
RUN apk add --no-cache bash git
WORKDIR /app
# package.jsonを先にコピー(キャッシュ効率化)
COPY package*.json ./
RUN npm install
# ソースコードはボリュームマウントするのでCOPYしない
EXPOSE 5173
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
Vite の HMR (Hot Module Replacement)
--host 0.0.0.0 を指定することで、Docker コンテナ内からでも HMR が正常に動作します。これがないと、ファイルを変更してもブラウザに反映されないという問題が発生します。
node_modules の扱い
volumes:
- ../../portal/frontend:/app
- node-modules:/app/node_modules
ホスト側のディレクトリをマウントしつつ、node_modules は名前付きボリュームで上書きしています。これにより、Windows/Mac とコンテナ (Linux) 間での node_modules の互換性問題を回避できます。
Nginx のリバースプロキシ設定
infrastructure/docker/nginx/nginx.conf で、フロントエンドとバックエンドを統合しています。
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
upstream frontend {
server portal-frontend:5173;
}
upstream backend {
server portal-backend:8080;
}
server {
listen 80;
server_name localhost;
# Frontend
location / {
proxy_pass http://frontend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
# Backend API
location /api/ {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
WebSocket サポート
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_cache_bypass $http_upgrade;
Vite の HMR は WebSocket を使うため、これらのヘッダー設定が必要です。最初これを設定せずに動かしたところ、HMR が動作せず、かなり悩みました。
パスベースのルーティング
-
/→ フロントエンド -
/api/→ バックエンド
このシンプルなルーティングにより、CORS の問題も発生しません。すべて同一オリジンからのリクエストとして扱われるためです。
Dev Container の統合
VS Code の Dev Container 機能を使うと、コンテナ内で直接コーディングできます。
Backend の devcontainer.json
portal/backend/.devcontainer/devcontainer.json:
{
"name": "Portal Backend",
"dockerComposeFile": "../../../infrastructure/docker/docker-compose.all.yml",
"service": "portal-backend",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"vscjava.vscode-java-pack",
"vmware.vscode-spring-boot",
"redhat.java",
"vscjava.vscode-maven"
],
"settings": {
"java.home": "/opt/java/openjdk",
"editor.formatOnSave": true,
"java.configuration.updateBuildConfiguration": "automatic"
}
}
},
"postCreateCommand": "./mvnw dependency:go-offline",
"forwardPorts": [8080],
"runServices": ["db", "portal-backend", "portal-frontend", "nginx"]
}
Frontend の devcontainer.json
portal/frontend/.devcontainer/devcontainer.json:
{
"name": "Portal Frontend",
"dockerComposeFile": "../../../infrastructure/docker/docker-compose.all.yml",
"service": "portal-frontend",
"workspaceFolder": "/app",
"customizations": {
"vscode": {
"extensions": [
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"bradlc.vscode-tailwindcss",
"dsznajder.es7-react-js-snippets"
],
"settings": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
}
},
"postCreateCommand": "npm install",
"forwardPorts": [5173],
"runServices": ["db", "portal-backend", "portal-frontend", "nginx"]
}
重要なポイント
1. dockerComposeFile の相対パス
"dockerComposeFile": "../../../infrastructure/docker/docker-compose.all.yml"
.devcontainer ディレクトリからの相対パスで、別リポジトリの Docker Compose ファイルを参照しています。
2. runServices の指定
"runServices": ["db", "portal-backend", "portal-frontend", "nginx"]
Dev Container を開いたときに起動するサービスを明示的に指定しています。これにより、Backend の開発中でもフロントエンドや Nginx が同時に起動し、統合された環境で開発できます。
3. 開発用拡張機能の自動インストール
コンテナを開くと、指定した VS Code 拡張機能が自動的にコンテナ内にインストールされます。チーム開発で全員が同じツールセットを使えるため、非常に便利です。
開発フロー
起動方法
全サービスを一括で起動するスクリプトを用意しています。
infrastructure/docker/rebuild-all.sh:
#!/bin/bash
echo "=========================================="
echo "Docker イメージ完全再ビルド"
echo "=========================================="
echo ""
cd "$(dirname "$0")"
echo "🛑 既存のコンテナとイメージを削除..."
docker-compose -f docker-compose.all.yml down -v
echo ""
echo "🗑️ 古いイメージを削除..."
docker rmi docker-portal-backend docker-portal-frontend 2>/dev/null || true
echo ""
echo "🔨 イメージを再ビルド(キャッシュなし)..."
docker-compose -f docker-compose.all.yml build --no-cache
echo ""
echo "🚀 サービスを起動..."
docker-compose -f docker-compose.all.yml up -d
echo ""
echo "⏳ コンテナ起動を待機中..."
sleep 5
echo ""
echo "✅ 起動状態確認:"
docker-compose -f docker-compose.all.yml ps
echo ""
echo "=========================================="
echo "完了!以下のURLにアクセスできます:"
echo " - Frontend: http://localhost/"
echo " - Backend: http://localhost/api/actuator/health"
echo " - Database: localhost:5432"
echo ""
echo "📊 ログ確認: docker-compose -f docker-compose.all.yml logs -f"
echo "🛑 停止: docker-compose -f docker-compose.all.yml down"
echo "=========================================="
実行方法:
cd infrastructure/docker
./rebuild-all.sh
開発時の流れ
実際の使用感としては:
- VS Code で
portal/backendまたはportal/frontendを Dev Container で開く - すべてのサービスが自動的に起動
- Java や TypeScript のファイルを編集
- 保存すると数秒でアプリケーションに反映
-
http://localhost/でブラウザ確認
この一連の流れがとてもスムーズで、従来のローカル環境に直接インストールする方法と比べて遜色ない開発体験が得られます。
トラブルシューティング
ポートが既に使用されている
Error: bind: address already in use
既存のプロセスを停止してから再起動します。
cd development/docker
docker-compose -f docker-compose.all.yml down
./rebuild-all.sh
HMR が動作しない
Vite の HMR が動作しない場合、以下を確認します:
-
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]が設定されているか - Nginx の WebSocket 設定が正しいか
- ファイアウォールがポート 5173 をブロックしていないか
Maven の依存関係エラー
docker-compose -f docker-compose.all.yml down -v
./rebuild-all.sh
-v オプションで名前付きボリュームも削除し、依存関係を完全にクリーンな状態から再取得します。
まとめ
この構成により、以下のメリットが得られました:
- 環境の一貫性: チーム全員が同じ環境で開発できる
- セットアップの簡単さ: Docker Desktop と VS Code さえあれば、数分で開発開始できる
- ホットリロード: ファイル変更が即座に反映される快適な開発体験
- 本番環境に近い: Nginx、データベース込みで動作確認できる
- リポジトリ分離: インフラ設定とアプリコードを明確に分離管理
当初はマルチリポジトリ構成で Docker Compose と Dev Container を連携させるのが難しそうに感じましたが、相対パスをうまく活用することで、意外とシンプルに実現できました。
これから同様の構成を考えている方の参考になれば幸いです。