Docker Compose本番運用ガイド — マルチサービス構成のベストプラクティス


Docker Composeは、開発環境だけでなく、本番環境でのマルチサービス構成にも活用できます。この記事では、Docker Composeを使った本番環境の構築から、セキュリティ、パフォーマンス最適化、監視、CI/CD統合まで、実践的なベストプラクティスを徹底的に解説します。

Docker Compose本番運用の概要

Docker Composeは、複数のコンテナを定義・管理するためのツールです。本番環境での使用には以下の利点があります。

  • インフラのコード化 - YAML形式で環境を定義
  • 再現性 - 環境の一貫性を保証
  • スケーラビリティ - サービスの簡単なスケール
  • 依存関係管理 - サービス間の起動順序を制御
  • ネットワーク分離 - セキュアなサービス間通信

基本的な構成例

フルスタックアプリケーション

# docker-compose.yml
version: '3.8'

services:
  # Next.jsアプリケーション
  web:
    build:
      context: ./web
      dockerfile: Dockerfile.prod
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://user:password@db:5432/myapp
      - REDIS_URL=redis://cache:6379
    depends_on:
      - db
      - cache
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  # PostgreSQLデータベース
  db:
    image: postgres:16-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=myapp
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U user"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Redisキャッシュ
  cache:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    command: redis-server --appendonly yes
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  # Nginx リバースプロキシ
  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
      - ./nginx/logs:/var/log/nginx
    depends_on:
      - web
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

本番用Dockerfile

マルチステージビルド

# web/Dockerfile.prod
# Stage 1: 依存関係のインストール
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: ビルド
FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# Stage 3: 本番実行
FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

USER nextjs

EXPOSE 3000

ENV PORT 3000

CMD ["node", "server.js"]

APIサーバー(Express)

# api/Dockerfile.prod
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app

ENV NODE_ENV production

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 apiuser

COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./

USER apiuser

EXPOSE 4000

CMD ["node", "dist/index.js"]

環境変数管理

.envファイルの使用

# .env.production
NODE_ENV=production

# データベース
POSTGRES_USER=myapp_user
POSTGRES_PASSWORD=super_secret_password
POSTGRES_DB=myapp_prod

# Redis
REDIS_PASSWORD=redis_secret

# アプリケーション
DATABASE_URL=postgresql://myapp_user:super_secret_password@db:5432/myapp_prod
REDIS_URL=redis://:redis_secret@cache:6379

# セキュリティ
JWT_SECRET=your_jwt_secret_here
ENCRYPTION_KEY=your_encryption_key

# 外部API
STRIPE_SECRET_KEY=sk_live_...
SENDGRID_API_KEY=SG...

docker-compose.prod.yml

version: '3.8'

services:
  web:
    env_file:
      - .env.production
    environment:
      - NODE_ENV=production
    # その他の設定...

  db:
    env_file:
      - .env.production
    # その他の設定...

シークレット管理(Docker Swarm)

version: '3.8'

services:
  web:
    secrets:
      - db_password
      - jwt_secret
    environment:
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - JWT_SECRET_FILE=/run/secrets/jwt_secret

secrets:
  db_password:
    external: true
  jwt_secret:
    external: true

Nginxリバースプロキシ設定

基本設定

# nginx/nginx.conf
events {
    worker_connections 1024;
}

http {
    upstream web {
        server web:3000;
    }

    # レート制限
    limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
    limit_conn_zone $binary_remote_addr zone=addr:10m;

    # ログ形式
    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
                    '$status $body_bytes_sent "$http_referer" '
                    '"$http_user_agent" "$http_x_forwarded_for"';

    access_log /var/log/nginx/access.log main;
    error_log /var/log/nginx/error.log warn;

    # gzip圧縮
    gzip on;
    gzip_vary on;
    gzip_min_length 1000;
    gzip_types text/plain text/css text/xml text/javascript 
               application/x-javascript application/xml+rss 
               application/json application/javascript;

    server {
        listen 80;
        server_name example.com www.example.com;

        # HTTPSへリダイレクト
        return 301 https://$server_name$request_uri;
    }

    server {
        listen 443 ssl http2;
        server_name example.com www.example.com;

        # SSL証明書
        ssl_certificate /etc/nginx/ssl/cert.pem;
        ssl_certificate_key /etc/nginx/ssl/key.pem;

        # SSL設定
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        ssl_prefer_server_ciphers on;

        # セキュリティヘッダー
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

        # 静的ファイルのキャッシュ
        location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
            proxy_pass http://web;
            proxy_cache_valid 200 30d;
            add_header Cache-Control "public, immutable";
        }

        # APIエンドポイント
        location /api/ {
            limit_req zone=api_limit burst=20 nodelay;
            limit_conn addr 10;

            proxy_pass http://web;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
        }

        # その他のリクエスト
        location / {
            proxy_pass http://web;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
            proxy_cache_bypass $http_upgrade;
        }
    }
}

データ永続化とバックアップ

ボリューム戦略

version: '3.8'

services:
  db:
    image: postgres:16-alpine
    volumes:
      # 名前付きボリューム(推奨)
      - postgres_data:/var/lib/postgresql/data
      # バックアップ用バインドマウント
      - ./backups:/backups
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=password

volumes:
  postgres_data:
    driver: local
    driver_opts:
      type: none
      device: /data/postgres
      o: bind

バックアップスクリプト

#!/bin/bash
# backup.sh

DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="/backups"

# PostgreSQLバックアップ
docker compose exec -T db pg_dump -U user myapp > "$BACKUP_DIR/db_$DATE.sql"

# 圧縮
gzip "$BACKUP_DIR/db_$DATE.sql"

# 古いバックアップを削除(7日以上前)
find "$BACKUP_DIR" -name "db_*.sql.gz" -mtime +7 -delete

echo "Backup completed: db_$DATE.sql.gz"

自動バックアップ(cron)

# docker-compose.yml
services:
  backup:
    image: alpine:latest
    volumes:
      - ./backup.sh:/backup.sh
      - ./backups:/backups
      - /var/run/docker.sock:/var/run/docker.sock
    command: sh -c "apk add --no-cache docker-cli && crond -f"
    restart: unless-stopped
# crontab
0 2 * * * /backup.sh

監視とログ管理

Prometheusとグラフana

# docker-compose.monitoring.yml
version: '3.8'

services:
  prometheus:
    image: prom/prometheus:latest
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
      - prometheus_data:/prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    ports:
      - "9090:9090"
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    volumes:
      - grafana_data:/var/lib/grafana
      - ./grafana/dashboards:/etc/grafana/provisioning/dashboards
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_USERS_ALLOW_SIGN_UP=false
    ports:
      - "3001:3000"
    depends_on:
      - prometheus
    restart: unless-stopped

  node-exporter:
    image: prom/node-exporter:latest
    command:
      - '--path.rootfs=/host'
    volumes:
      - '/:/host:ro,rslave'
    restart: unless-stopped

volumes:
  prometheus_data:
  grafana_data:

Prometheus設定

# prometheus/prometheus.yml
global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: 'prometheus'
    static_configs:
      - targets: ['localhost:9090']

  - job_name: 'node'
    static_configs:
      - targets: ['node-exporter:9100']

  - job_name: 'web'
    static_configs:
      - targets: ['web:3000']
    metrics_path: '/api/metrics'

ログ集約(Loki + Promtail)

# docker-compose.logging.yml
version: '3.8'

services:
  loki:
    image: grafana/loki:latest
    ports:
      - "3100:3100"
    volumes:
      - ./loki/loki-config.yml:/etc/loki/local-config.yaml
      - loki_data:/loki
    command: -config.file=/etc/loki/local-config.yaml
    restart: unless-stopped

  promtail:
    image: grafana/promtail:latest
    volumes:
      - ./promtail/promtail-config.yml:/etc/promtail/config.yml
      - /var/log:/var/log:ro
      - /var/lib/docker/containers:/var/lib/docker/containers:ro
    command: -config.file=/etc/promtail/config.yml
    restart: unless-stopped

volumes:
  loki_data:

スケーリングとロードバランシング

水平スケーリング

version: '3.8'

services:
  web:
    build: ./web
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
      restart_policy:
        condition: on-failure
    environment:
      - NODE_ENV=production

  nginx:
    image: nginx:alpine
    volumes:
      - ./nginx/nginx-lb.conf:/etc/nginx/nginx.conf:ro
    ports:
      - "80:80"
    depends_on:
      - web

Nginxロードバランサー設定

# nginx/nginx-lb.conf
http {
    upstream web_cluster {
        least_conn;
        server web_1:3000 weight=3;
        server web_2:3000 weight=3;
        server web_3:3000 weight=3;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://web_cluster;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
        }
    }
}

セキュリティベストプラクティス

ネットワーク分離

version: '3.8'

services:
  web:
    networks:
      - frontend
      - backend

  db:
    networks:
      - backend

  cache:
    networks:
      - backend

  nginx:
    networks:
      - frontend
    ports:
      - "80:80"
      - "443:443"

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true  # 外部アクセス不可

リソース制限

version: '3.8'

services:
  web:
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

  db:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 1G

セキュリティスキャン

# Trivyでイメージスキャン
docker run --rm \
  -v /var/run/docker.sock:/var/run/docker.sock \
  aquasec/trivy image myapp:latest

CI/CD統合

GitHub Actions

# .github/workflows/deploy.yml
name: Deploy to Production

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: ./web
          file: ./web/Dockerfile.prod
          push: true
          tags: myapp/web:latest
          cache-from: type=gha
          cache-to: type=gha,mode=max

      - name: Deploy to server
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /app
            docker compose pull
            docker compose up -d
            docker system prune -f

GitLab CI

# .gitlab-ci.yml
stages:
  - build
  - deploy

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA ./web
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA

deploy:
  stage: deploy
  image: alpine:latest
  before_script:
    - apk add --no-cache openssh-client
    - eval $(ssh-agent -s)
    - echo "$SSH_PRIVATE_KEY" | ssh-add -
  script:
    - ssh $SERVER_USER@$SERVER_HOST "
        cd /app &&
        docker compose pull &&
        docker compose up -d &&
        docker system prune -f"
  only:
    - main

ゼロダウンタイムデプロイ

ローリングアップデート

version: '3.8'

services:
  web:
    image: myapp/web:latest
    deploy:
      replicas: 3
      update_config:
        parallelism: 1
        delay: 10s
        order: start-first
      rollback_config:
        parallelism: 1
        delay: 5s

ヘルスチェック

services:
  web:
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

トラブルシューティング

ログ確認

# 全サービスのログ
docker compose logs -f

# 特定サービスのログ
docker compose logs -f web

# 最新100行のみ
docker compose logs --tail=100 web

コンテナの再起動

# 特定サービスの再起動
docker compose restart web

# 全サービスの再起動
docker compose restart

リソース使用状況

# コンテナのリソース使用状況
docker stats

# ディスク使用量
docker system df

まとめ

Docker Composeを使った本番環境の運用には、適切な設定とベストプラクティスの実践が重要です。

重要なポイント:

  • マルチステージビルドで最適化
  • 環境変数とシークレットの適切な管理
  • Nginxでのリバースプロキシとロードバランシング
  • データの永続化とバックアップ
  • 監視とログ管理の実装
  • セキュリティとネットワーク分離
  • CI/CDとの統合

これらのベストプラクティスを実践することで、スケーラブルで安全な本番環境を構築できます。