はじめに

Docker Composeの基本的な書き方を習得したら、次のステップは実際の開発プロジェクトで使える構成を組み立てることです。モダンなWebアプリケーションでは、フロントエンド、バックエンドAPI、データベース、キャッシュサーバーなど複数のサービスが連携して動作します。

本記事では、React/Node.js + PostgreSQL + Redisという実践的な3層構成をDocker Composeで構築する方法を解説します。サービス間の依存関係をdepends_onhealthcheckで適切に制御し、開発時のホットリロード設定で効率的な開発環境を実現する手法を紹介します。

この記事を読み終えると、以下のことができるようになります。

  • Web+DB+キャッシュ構成のcompose.yamlを設計・実装できる
  • depends_onhealthcheckを組み合わせてサービスの起動順序を制御できる
  • 開発時のホットリロード設定でコード変更を即座に反映できる
  • 実際のプロジェクトで使えるcompose.yamlのテンプレートを持てる

前提として、Docker Composeの基本操作を理解していることを想定しています。compose.yamlの基本構文に不安がある場合は、Docker Compose入門を参照してください。

Web+DB+キャッシュ構成の全体像

構成するサービス

今回構築する構成は、以下の4つのサービスで構成されます。

サービス 役割 使用イメージ
frontend Reactフロントエンド(開発サーバー) node:22-alpine
api Node.js/Express APIサーバー node:22-alpine
db PostgreSQLデータベース postgres:18
cache Redisキャッシュサーバー redis:8-alpine

アーキテクチャ図

各サービスの関係性を図で示します。

flowchart TB
    subgraph DockerCompose["Docker Compose 環境"]
        frontend["frontend\nReact:5173"]
        api["api\nExpress:3000"]
        db["db\nPostgres:5432"]
        cache["cache\nRedis:6379"]
        dbData[("db-data\n(Volume)")]
        cacheData[("cache-data\n(Volume)")]
        
        frontend -->|"API呼出"| api
        api --> db
        api --> cache
        db --> dbData
        cache --> cacheData
    end
    
    browser["ホスト:5173\n(ブラウザ)"] --> frontend

通信フロー

  1. ブラウザからfrontend(React開発サーバー)にアクセス
  2. frontendからapiサーバーにHTTPリクエストを送信
  3. apiはキャッシュ(Redis)を確認し、キャッシュヒットならそのまま返却
  4. キャッシュミスの場合、db(PostgreSQL)からデータを取得
  5. 取得したデータをキャッシュに保存し、クライアントに返却

サービス定義例

各サービスの設定を順番に解説します。

PostgreSQLサービス(db)

データベースサービスの定義から始めます。PostgreSQLは他のサービスの基盤となるため、最も重要な設定です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
services:
  db:
    image: postgres:18
    container_name: myapp-db
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: apppassword
      POSTGRES_DB: myapp_development
      TZ: Asia/Tokyo
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./docker/db/init:/docker-entrypoint-initdb.d:ro
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped

ポイントとなる設定を説明します。

環境変数

POSTGRES_USERPOSTGRES_PASSWORDPOSTGRES_DBでデータベースの初期設定を行います。TZでタイムゾーンを日本時間に設定しています。

ボリューム設定

db-dataという名前付きボリュームでデータを永続化します。./docker/db/initディレクトリに配置したSQLファイルは、コンテナ初回起動時に自動実行されます。:ro(read-only)を付けることで、コンテナからの書き込みを防止しています。

healthcheck設定

pg_isreadyコマンドでPostgreSQLが接続を受け付けられる状態かを確認します。$${POSTGRES_USER}のようにドル記号を2つ重ねることで、Composeの変数展開を避けてコンテナ内で環境変数を参照できます。

Redisサービス(cache)

セッション管理やAPIレスポンスのキャッシュに使用するRedisの設定です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
services:
  cache:
    image: redis:8-alpine
    container_name: myapp-cache
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - cache-data:/data
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped

commandによる設定カスタマイズ

redis-serverのオプションでRedisの動作をカスタマイズしています。

オプション 説明
--appendonly yes AOF(Append Only File)永続化を有効化
--maxmemory 256mb 最大メモリ使用量を256MBに制限
--maxmemory-policy allkeys-lru メモリ上限到達時、LRU方式で古いキーを削除

healthcheck設定

redis-cli pingコマンドでRedisサーバーの応答を確認します。正常な場合はPONGが返却されます。

Node.js APIサービス(api)

Express.jsで構築されたAPIサーバーの設定です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: myapp-api
    environment:
      NODE_ENV: development
      PORT: 3000
      DATABASE_URL: postgresql://appuser:apppassword@db:5432/myapp_development
      REDIS_URL: redis://cache:6379
    volumes:
      - ./api:/app
      - /app/node_modules
    ports:
      - "3000:3000"
    depends_on:
      db:
        condition: service_healthy
      cache:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped

DATABASE_URLとREDIS_URL

Docker Composeでは、サービス名がそのままホスト名として解決されます。db:5432でPostgreSQLに、cache:6379でRedisに接続できます。

ボリュームマウントの工夫

1
2
3
volumes:
  - ./api:/app           # ソースコードをマウント
  - /app/node_modules    # node_modulesはコンテナ内のものを使用

/app/node_modulesを匿名ボリュームとしてマウントすることで、ホストのnode_modulesがコンテナ内を上書きすることを防ぎます。これにより、ホストとコンテナで異なるOS(例:WindowsホストとLinuxコンテナ)でも正しく動作します。

depends_onとcondition

condition: service_healthyを指定することで、依存サービスのhealthcheckが成功するまでAPIサーバーの起動を待機します。

Reactフロントエンドサービス(frontend)

Viteを使用したReactアプリケーションの開発サーバーです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
services:
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: myapp-frontend
    environment:
      NODE_ENV: development
      VITE_API_URL: http://localhost:3000
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "5173:5173"
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped

VITE_API_URL

ViteではVITE_プレフィックスを付けた環境変数がクライアントサイドで利用可能になります。ブラウザからAPIにアクセスするため、localhost:3000を指定しています。

depends_onとhealthcheckの活用

サービスの起動順序と依存関係を適切に制御することは、安定した開発環境の構築に不可欠です。

depends_onの基本

depends_onには2つの記法があります。

短縮形式(Short Syntax)

1
2
3
4
5
services:
  api:
    depends_on:
      - db
      - cache

この形式では、依存サービスが「起動した」ことのみを保証します。サービスが実際にリクエストを受け付けられる状態かどうかは確認しません。

長形式(Long Syntax)

1
2
3
4
5
6
7
8
services:
  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      cache:
        condition: service_started

長形式では追加のオプションを指定できます。

オプション 説明
condition 依存サービスの状態条件
restart 依存サービス再起動時に自動再起動するか
required 依存サービスが必須かどうか(デフォルト: true)

conditionの種類

説明
service_started サービスが起動したら(デフォルト)
service_healthy healthcheckが成功したら
service_completed_successfully サービスが正常終了したら

healthcheckの設定方法

healthcheckは、コンテナが「正常に動作している」かを定期的に確認する仕組みです。

1
2
3
4
5
6
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

各パラメータの意味

パラメータ 説明 デフォルト
test ヘルスチェックコマンド なし
interval チェック間隔 30s
timeout タイムアウト時間 30s
retries 失敗許容回数 3
start_period 起動猶予期間 0s

start_periodは、コンテナ起動直後の初期化期間を指定します。この期間中のヘルスチェック失敗はカウントされません。

testコマンドの形式

1
2
3
4
5
6
7
8
# exec形式(推奨)
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]

# shell形式
test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]

# 文字列形式(CMD-SHELLと同等)
test: curl -f http://localhost:3000/health || exit 1

各サービスのhealthcheck例

PostgreSQL

1
2
3
4
5
6
healthcheck:
  test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 30s

Redis

1
2
3
4
5
6
healthcheck:
  test: ["CMD", "redis-cli", "ping"]
  interval: 10s
  timeout: 5s
  retries: 5
  start_period: 10s

Node.js API

1
2
3
4
5
6
healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s

依存関係のチェーン

複数サービスの依存関係を正しく定義することで、起動順序が自動的に決定されます。

起動順序:
  db, cache (並列) → api → frontend

停止順序:
  frontend → api → db, cache (並列)

Composeは依存関係を解析し、依存されていないサービスから順番に起動します。停止時は逆順になります。

開発時のホットリロード設定

開発効率を高めるために、コード変更を即座にコンテナに反映させるホットリロード設定を行います。

バインドマウントによるソース同期

ホストのソースコードをコンテナ内にマウントすることで、ファイル変更がリアルタイムで反映されます。

1
2
3
4
5
services:
  api:
    volumes:
      - ./api:/app
      - /app/node_modules

Node.jsのホットリロード設定

APIサーバーでnodemonを使用したホットリロードを設定します。

Dockerfile(api/Dockerfile)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
FROM node:22-alpine

WORKDIR /app

# 依存関係のインストール
COPY package*.json ./
RUN npm ci

# nodemonをグローバルインストール
RUN npm install -g nodemon

# ソースコードのコピー(開発時はマウントで上書き)
COPY . .

EXPOSE 3000

# 開発時はnodemonで起動
CMD ["nodemon", "--watch", "src", "-e", "js,ts,json", "src/index.js"]

compose.yamlでの設定

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    volumes:
      - ./api:/app
      - /app/node_modules
    environment:
      NODE_ENV: development

Vite(React)のホットリロード設定

Viteは標準でHMR(Hot Module Replacement)をサポートしていますが、Docker環境では追加設定が必要です。

vite.config.js

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  server: {
    host: '0.0.0.0',  // コンテナ外からのアクセスを許可
    port: 5173,
    watch: {
      usePolling: true,  // Dockerボリュームマウント時に必要
    },
    hmr: {
      port: 5173,
    },
  },
})

Dockerfile(frontend/Dockerfile)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
FROM node:22-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .

EXPOSE 5173

CMD ["npm", "run", "dev", "--", "--host"]

Docker Compose Watchの活用

Docker Compose 2.22.0以降では、developセクションでwatch機能を利用できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
services:
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    develop:
      watch:
        - action: sync
          path: ./api/src
          target: /app/src
        - action: rebuild
          path: ./api/package.json

actionの種類

action 説明
sync ファイル変更をコンテナに同期
rebuild ファイル変更時にイメージを再ビルド
sync+restart 同期後にコンテナを再起動

watchモードは以下のコマンドで起動します。

1
docker compose watch

実践compose.yamlサンプル

これまでの設定をまとめた完全なcompose.yamlを示します。

ディレクトリ構成

my-project/
├── compose.yaml
├── .env
├── api/
│   ├── Dockerfile
│   ├── package.json
│   └── src/
│       └── index.js
├── frontend/
│   ├── Dockerfile
│   ├── package.json
│   ├── vite.config.js
│   └── src/
│       └── App.jsx
└── docker/
    └── db/
        └── init/
            └── 01_schema.sql

完全なcompose.yaml

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
name: myapp

services:
  # PostgreSQLデータベース
  db:
    image: postgres:18
    container_name: myapp-db
    environment:
      POSTGRES_USER: ${DB_USER:-appuser}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-apppassword}
      POSTGRES_DB: ${DB_NAME:-myapp_development}
      TZ: Asia/Tokyo
    volumes:
      - db-data:/var/lib/postgresql/data
      - ./docker/db/init:/docker-entrypoint-initdb.d:ro
    ports:
      - "${DB_PORT:-5432}:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 30s
    restart: unless-stopped
    networks:
      - backend

  # Redisキャッシュ
  cache:
    image: redis:8-alpine
    container_name: myapp-cache
    command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru
    volumes:
      - cache-data:/data
    ports:
      - "${REDIS_PORT:-6379}:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
      start_period: 10s
    restart: unless-stopped
    networks:
      - backend

  # Node.js APIサーバー
  api:
    build:
      context: ./api
      dockerfile: Dockerfile
    container_name: myapp-api
    environment:
      NODE_ENV: development
      PORT: 3000
      DATABASE_URL: postgresql://${DB_USER:-appuser}:${DB_PASSWORD:-apppassword}@db:5432/${DB_NAME:-myapp_development}
      REDIS_URL: redis://cache:6379
    volumes:
      - ./api:/app
      - /app/node_modules
    ports:
      - "${API_PORT:-3000}:3000"
    depends_on:
      db:
        condition: service_healthy
        restart: true
      cache:
        condition: service_healthy
        restart: true
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s
    restart: unless-stopped
    networks:
      - backend
      - frontend

  # Reactフロントエンド
  frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile
    container_name: myapp-frontend
    environment:
      NODE_ENV: development
      VITE_API_URL: http://localhost:${API_PORT:-3000}
    volumes:
      - ./frontend:/app
      - /app/node_modules
    ports:
      - "${FRONTEND_PORT:-5173}:5173"
    depends_on:
      api:
        condition: service_healthy
    restart: unless-stopped
    networks:
      - frontend

networks:
  backend:
    driver: bridge
  frontend:
    driver: bridge

volumes:
  db-data:
  cache-data:

環境変数ファイル(.env)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# Database
DB_USER=appuser
DB_PASSWORD=apppassword
DB_NAME=myapp_development
DB_PORT=5432

# Redis
REDIS_PORT=6379

# API
API_PORT=3000

# Frontend
FRONTEND_PORT=5173

初期化SQL(docker/db/init/01_schema.sql)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- テーブル作成例
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS posts (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    title VARCHAR(255) NOT NULL,
    content TEXT,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- インデックス作成
CREATE INDEX IF NOT EXISTS idx_posts_user_id ON posts(user_id);

起動と動作確認

全サービスの起動

1
2
3
4
5
6
7
8
# バックグラウンドで起動
docker compose up -d

# ログを確認しながら起動
docker compose up

# 特定サービスのみ起動
docker compose up -d db cache

サービス状態の確認

1
2
3
4
5
# 全サービスの状態確認
docker compose ps

# ヘルスチェック状態を含めて確認
docker compose ps --format "table {{.Name}}\t{{.Status}}\t{{.Ports}}"

ログの確認

1
2
3
4
5
6
7
8
# 全サービスのログ
docker compose logs

# 特定サービスのログをフォロー
docker compose logs -f api

# 最新100行のみ表示
docker compose logs --tail 100 api

停止と削除

1
2
3
4
5
6
7
8
# サービス停止(コンテナは残る)
docker compose stop

# サービス停止とコンテナ削除
docker compose down

# ボリュームも含めて削除(データ初期化)
docker compose down -v

まとめ

本記事では、Docker Composeを使用してReact/Node.js + PostgreSQL + Redisの実践的な開発環境を構築する方法を解説しました。

学んだポイント

  • Web+DB+キャッシュの3層構成におけるサービス設計
  • depends_onの長形式とconditionによる起動順序の制御
  • healthcheckによるサービス正常性の確認
  • バインドマウントとDocker Compose Watchによるホットリロード設定
  • 環境変数とネットワーク分離を活用した保守性の高いcompose.yaml

この構成をベースに、ログ収集(Fluentd)、メトリクス監視(Prometheus)、リバースプロキシ(Nginx)などを追加していくことで、より本格的な開発・ステージング環境を構築できます。

次のステップとして、複数のCompose設定ファイルを使い分ける手法や、本番環境向けの設定について学ぶと、さらに実践的なDocker活用ができるようになります。

参考リンク