Docker Composeは開発環境だけでなく、小〜中規模の本番環境でも十分に活用できます。本記事では、開発環境との違いを明確にしながら、セキュリティ、パフォーマンス、運用性を考慮したプロダクションレディな設定を構築します。
この記事で学ぶこと
- 開発環境と本番環境の設定分離
- マルチステージビルドによる最適化
- シークレット管理とセキュリティ
- ヘルスチェックと自動復旧
- ログ管理とモニタリング
- バックアップと復旧戦略
プロジェクト構成
project/
├── docker/
│ ├── app/
│ │ └── Dockerfile
│ ├── nginx/
│ │ ├── Dockerfile
│ │ └── nginx.conf
│ └── postgres/
│ └── init.sql
├── docker-compose.yml # 基本設定
├── docker-compose.override.yml # 開発環境(自動読み込み)
├── docker-compose.prod.yml # 本番環境
├── docker-compose.staging.yml # ステージング環境
├── .env.example # 環境変数テンプレート
└── src/
└── ...
開発環境と本番環境の分離
基本設定(docker-compose.yml)
# docker-compose.yml - 共通設定
services:
app:
build:
context: .
dockerfile: docker/app/Dockerfile
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
networks:
- backend
db:
image: postgres:16-alpine
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- backend
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
networks:
- backend
networks:
backend:
driver: bridge
volumes:
postgres_data:
開発環境設定(docker-compose.override.yml)
# docker-compose.override.yml - 開発環境
# docker compose up で自動的に読み込まれる
services:
app:
build:
target: development
volumes:
- .:/app
- /app/node_modules
environment:
- NODE_ENV=development
- DEBUG=app:*
ports:
- "3000:3000"
- "9229:9229" # デバッガ用
command: npm run dev
db:
environment:
- POSTGRES_USER=dev
- POSTGRES_PASSWORD=dev
- POSTGRES_DB=app_dev
ports:
- "5432:5432"
redis:
ports:
- "6379:6379"
本番環境設定(docker-compose.prod.yml)
# docker-compose.prod.yml - 本番環境
services:
app:
build:
target: production
environment:
- NODE_ENV=production
deploy:
resources:
limits:
cpus: '2'
memory: 2G
reservations:
cpus: '1'
memory: 1G
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "5"
secrets:
- db_password
- app_secret
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./docker/nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./docker/nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- backend
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
db:
environment:
- POSTGRES_USER_FILE=/run/secrets/db_user
- POSTGRES_PASSWORD_FILE=/run/secrets/db_password
- POSTGRES_DB=app_production
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
deploy:
resources:
limits:
cpus: '2'
memory: 4G
secrets:
- db_user
- db_password
redis:
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
deploy:
resources:
limits:
cpus: '0.5'
memory: 512M
secrets:
db_user:
file: ./secrets/db_user.txt
db_password:
file: ./secrets/db_password.txt
app_secret:
file: ./secrets/app_secret.txt
volumes:
postgres_data:
redis_data:
マルチステージビルド
Node.js アプリケーション
# docker/app/Dockerfile
# ========== Base Stage ==========
FROM node:20-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# ========== Dependencies Stage ==========
FROM base AS deps
COPY package.json package-lock.json ./
RUN npm ci --only=production && \
cp -R node_modules /tmp/prod_modules && \
npm ci
# ========== Development Stage ==========
FROM base AS development
COPY /app/node_modules ./node_modules
COPY . .
ENV NODE_ENV=development
EXPOSE 3000 9229
CMD ["npm", "run", "dev"]
# ========== Builder Stage ==========
FROM base AS builder
COPY /app/node_modules ./node_modules
COPY . .
RUN npm run build
# ========== Production Stage ==========
FROM base AS production
# 非rootユーザーを作成
RUN addgroup -g 1001 -S nodejs && \
adduser -S nextjs -u 1001
# 本番用の依存関係のみ
COPY /tmp/prod_modules ./node_modules
COPY /app/dist ./dist
COPY /app/package.json ./
# 所有権を変更
RUN chown -R nextjs:nodejs /app
USER nextjs
ENV NODE_ENV=production
ENV PORT=3000
EXPOSE 3000
# ヘルスチェック
HEALTHCHECK \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
イメージサイズの比較
マルチステージビルドの効果
| ステージ | サイズ | 削減率 |
|---|---|---|
| 開発用(全依存関係) | 1.2 GB | - |
| ビルドステージ | 1.5 GB | - |
| 本番用(最終) | 180 MB | 85% |
シークレット管理
Docker Secrets(推奨)
# シークレットファイルの作成
mkdir -p secrets
echo "app_user" > secrets/db_user.txt
echo "$(openssl rand -base64 32)" > secrets/db_password.txt
echo "$(openssl rand -base64 64)" > secrets/app_secret.txt
# ファイル権限の設定
chmod 600 secrets/*
# docker-compose.prod.yml
services:
app:
secrets:
- db_password
- app_secret
environment:
# シークレットはファイルから読み込み
- DATABASE_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
file: ./secrets/db_password.txt
app_secret:
file: ./secrets/app_secret.txt
// アプリケーション側でのシークレット読み込み
import { readFileSync } from 'fs';
function getSecret(name: string): string {
const filePath = `/run/secrets/${name}`;
try {
return readFileSync(filePath, 'utf8').trim();
} catch {
// 開発環境では環境変数から取得
return process.env[name.toUpperCase()] || '';
}
}
const dbPassword = getSecret('db_password');
環境変数の安全な管理
# .env.example(テンプレート)
NODE_ENV=production
DB_HOST=db
DB_PORT=5432
DB_NAME=app_production
# シークレットはファイルから読み込むため記載しない
# .env.prod(本番用 - .gitignoreに追加)
NODE_ENV=production
DB_HOST=db
DB_PORT=5432
DB_NAME=app_production
# docker-compose.prod.yml
services:
app:
env_file:
- .env.prod
セキュリティ設定
Nginx リバースプロキシ
# docker/nginx/nginx.conf
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# ログフォーマット
log_format main_json escape=json '{'
'"time": "$time_iso8601",'
'"remote_addr": "$remote_addr",'
'"method": "$request_method",'
'"uri": "$request_uri",'
'"status": $status,'
'"body_bytes_sent": $body_bytes_sent,'
'"request_time": $request_time,'
'"upstream_response_time": "$upstream_response_time",'
'"user_agent": "$http_user_agent"'
'}';
access_log /var/log/nginx/access.log main_json;
# パフォーマンス設定
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;
# セキュリティヘッダー
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 Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;
# サーバー情報を隠す
server_tokens off;
# Gzip圧縮
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript application/xml;
# レート制限
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=login:10m rate=1r/s;
upstream app {
server app:3000;
keepalive 32;
}
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://app;
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 /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app;
# ... 他の設定
}
location /api/auth/login {
limit_req zone=login burst=5 nodelay;
proxy_pass http://app;
}
# 静的ファイル
location /static/ {
alias /app/static/;
expires 30d;
add_header Cache-Control "public, immutable";
}
# ヘルスチェック
location /health {
access_log off;
proxy_pass http://app;
}
}
}
コンテナのセキュリティ強化
# docker-compose.prod.yml
services:
app:
# 読み取り専用ファイルシステム
read_only: true
tmpfs:
- /tmp
- /app/tmp
# 権限の制限
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # 必要な権限のみ追加
# セキュリティオプション
security_opt:
- no-new-privileges:true
# 非rootユーザー(Dockerfileで設定済み)
user: "1001:1001"
ヘルスチェックと自動復旧
ヘルスチェックの設定
# docker-compose.prod.yml
services:
app:
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d app_production"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
redis:
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 3
アプリケーション側のヘルスチェックエンドポイント
// src/health.ts
import { Router } from 'express';
import { Pool } from 'pg';
import Redis from 'ioredis';
const router = Router();
const pool = new Pool();
const redis = new Redis();
interface HealthStatus {
status: 'healthy' | 'unhealthy';
timestamp: string;
checks: {
database: { status: string; latency?: number };
redis: { status: string; latency?: number };
memory: { used: number; total: number };
};
}
router.get('/health', async (req, res) => {
const health: HealthStatus = {
status: 'healthy',
timestamp: new Date().toISOString(),
checks: {
database: { status: 'unknown' },
redis: { status: 'unknown' },
memory: {
used: process.memoryUsage().heapUsed,
total: process.memoryUsage().heapTotal,
},
},
};
try {
// データベースチェック
const dbStart = Date.now();
await pool.query('SELECT 1');
health.checks.database = {
status: 'healthy',
latency: Date.now() - dbStart,
};
} catch {
health.checks.database = { status: 'unhealthy' };
health.status = 'unhealthy';
}
try {
// Redisチェック
const redisStart = Date.now();
await redis.ping();
health.checks.redis = {
status: 'healthy',
latency: Date.now() - redisStart,
};
} catch {
health.checks.redis = { status: 'unhealthy' };
health.status = 'unhealthy';
}
const statusCode = health.status === 'healthy' ? 200 : 503;
res.status(statusCode).json(health);
});
// Liveness probe(生存確認)
router.get('/health/live', (req, res) => {
res.status(200).json({ status: 'alive' });
});
// Readiness probe(準備完了確認)
router.get('/health/ready', async (req, res) => {
try {
await pool.query('SELECT 1');
await redis.ping();
res.status(200).json({ status: 'ready' });
} catch {
res.status(503).json({ status: 'not ready' });
}
});
export default router;
ログ管理
構造化ログ
// src/logger.ts
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL || 'info',
formatters: {
level: (label) => ({ level: label }),
},
timestamp: () => `,"timestamp":"${new Date().toISOString()}"`,
base: {
service: 'app',
version: process.env.APP_VERSION || '1.0.0',
},
});
export default logger;
Fluentd/Loki との統合
# docker-compose.prod.yml
services:
app:
logging:
driver: "fluentd"
options:
fluentd-address: "localhost:24224"
tag: "app.{{.Name}}"
fluentd-async-connect: "true"
fluentd:
image: fluent/fluentd:v1.16-debian
volumes:
- ./docker/fluentd/fluent.conf:/fluentd/etc/fluent.conf:ro
- fluentd_logs:/fluentd/log
ports:
- "24224:24224"
networks:
- backend
volumes:
fluentd_logs:
# docker/fluentd/fluent.conf
<source>
@type forward
port 24224
bind 0.0.0.0
</source>
<filter app.**>
@type parser
key_name log
reserve_data true
<parse>
@type json
</parse>
</filter>
<match app.**>
@type elasticsearch
host elasticsearch
port 9200
logstash_format true
logstash_prefix app
<buffer>
@type file
path /fluentd/log/buffer
flush_interval 5s
</buffer>
</match>
バックアップと復旧
データベースバックアップスクリプト
#!/bin/bash
# scripts/backup-db.sh
set -euo pipefail
BACKUP_DIR="/backups/postgres"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="${BACKUP_DIR}/backup_${DATE}.sql.gz"
RETENTION_DAYS=7
# バックアップディレクトリ作成
mkdir -p "${BACKUP_DIR}"
# バックアップ実行
docker compose -f docker-compose.prod.yml exec -T db \
pg_dump -U postgres -d app_production | gzip > "${BACKUP_FILE}"
# 古いバックアップの削除
find "${BACKUP_DIR}" -name "backup_*.sql.gz" -mtime +${RETENTION_DAYS} -delete
# バックアップの検証
if gzip -t "${BACKUP_FILE}"; then
echo "Backup successful: ${BACKUP_FILE}"
echo "Size: $(du -h ${BACKUP_FILE} | cut -f1)"
else
echo "Backup verification failed!"
exit 1
fi
復元スクリプト
#!/bin/bash
# scripts/restore-db.sh
set -euo pipefail
BACKUP_FILE=$1
if [ -z "${BACKUP_FILE}" ]; then
echo "Usage: $0 <backup_file>"
exit 1
fi
echo "Restoring from: ${BACKUP_FILE}"
echo "WARNING: This will overwrite the current database!"
read -p "Continue? (y/N): " confirm
if [ "${confirm}" != "y" ]; then
echo "Aborted."
exit 0
fi
# 復元実行
gunzip -c "${BACKUP_FILE}" | docker compose -f docker-compose.prod.yml exec -T db \
psql -U postgres -d app_production
echo "Restore completed."
デプロイコマンド
デプロイスクリプト
#!/bin/bash
# scripts/deploy.sh
set -euo pipefail
echo "=== Starting deployment ==="
# 設定ファイルの検証
docker compose -f docker-compose.yml -f docker-compose.prod.yml config --quiet
# 最新イメージのビルド
echo "Building images..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml build --pull
# データベースバックアップ
echo "Creating database backup..."
./scripts/backup-db.sh
# ローリングアップデート
echo "Starting rolling update..."
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps app
# ヘルスチェック待機
echo "Waiting for health check..."
sleep 10
# ヘルスチェック確認
if curl -sf http://localhost/health > /dev/null; then
echo "=== Deployment successful ==="
else
echo "=== Health check failed! Rolling back... ==="
docker compose -f docker-compose.yml -f docker-compose.prod.yml rollback
exit 1
fi
# 未使用イメージの削除
docker image prune -f
echo "=== Deployment completed ==="
Makefileによる操作の標準化
# Makefile
.PHONY: dev prod build deploy logs backup restore clean
# 開発環境
dev:
docker compose up -d
dev-logs:
docker compose logs -f
# 本番環境
prod:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
prod-logs:
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f
# ビルド
build:
docker compose -f docker-compose.yml -f docker-compose.prod.yml build --no-cache
# デプロイ
deploy:
./scripts/deploy.sh
# バックアップ
backup:
./scripts/backup-db.sh
restore:
./scripts/restore-db.sh $(FILE)
# クリーンアップ
clean:
docker compose down -v --remove-orphans
docker image prune -af
docker volume prune -f
運用コマンド早見表
# 起動
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
# 停止
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
# ログ確認
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f app
# スケールアウト
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --scale app=3
# コンテナに入る
docker compose -f docker-compose.yml -f docker-compose.prod.yml exec app sh
# リソース使用状況
docker stats
# 設定の検証
docker compose -f docker-compose.yml -f docker-compose.prod.yml config
まとめ
Docker Composeを本番環境で使用する際のポイントをまとめます。
セキュリティ
- シークレット管理: Docker Secretsを使用
- 非rootユーザー: コンテナ内で特権を持たない
- 最小権限: 必要な権限のみ付与
- イメージ最適化: マルチステージビルドで攻撃面を削減
信頼性
- ヘルスチェック: 全サービスに設定
- 再起動ポリシー: 障害時の自動復旧
- リソース制限: OOMキラー対策
- バックアップ: 定期的な自動バックアップ
運用性
- ログ管理: 構造化ログと集約
- モニタリング: メトリクス収集
- デプロイ自動化: スクリプト化
- 設定分離: 環境ごとの設定ファイル
Docker Composeは、適切に設定すれば本番環境でも十分に活用できます。