【2026年版】Docker Compose本番運用ガイド:セキュリティ・監視・CI/CDの実践テクニック

Tech Trends AI
- 7 minutes read - 1315 wordsはじめに
Docker Composeは、複数のコンテナを定義・管理するためのツールとして広く利用されています。開発環境での利用が一般的ですが、適切な設計と運用を行えば中小規模の本番環境でも十分に活用できます。
2026年現在、Docker Compose V2は安定版となり、多くの機能強化が施されています。本記事では、Docker Composeを本番環境で安全に運用するために必要なセキュリティ対策、監視・ログ管理、CI/CDパイプラインの構築方法を実践的に解説します。
Docker Compose本番運用の基本設計
開発環境と本番環境の違い
Docker Composeを本番で使う場合、開発環境とは異なる設計が必要です。
| 項目 | 開発環境 | 本番環境 |
|---|---|---|
| イメージ | ローカルビルド | レジストリからプル(タグ固定) |
| ボリューム | バインドマウント | 名前付きボリューム |
| ポート公開 | 全ポート公開 | 必要最小限(リバースプロキシ経由) |
| 環境変数 | .envファイル | シークレット管理ツール |
| ログ | stdout | 集約ログシステム |
| リソース制限 | なし | CPU/メモリ制限必須 |
| 再起動ポリシー | なし | unless-stopped / always |
| ヘルスチェック | 任意 | 必須 |
本番用docker-compose.ymlの基本構成
# docker-compose.prod.yml
version: '3.8'
services:
# Webアプリケーション
app:
image: registry.example.com/myapp:${APP_VERSION:-latest}
restart: unless-stopped
deploy:
resources:
limits:
cpus: '2.0'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
secrets:
- db_password
- api_key
networks:
- frontend
- backend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
tag: "{{.Name}}/{{.ID}}"
read_only: true
tmpfs:
- /tmp
- /var/run
security_opt:
- no-new-privileges:true
# データベース
db:
image: postgres:16-alpine
restart: unless-stopped
deploy:
resources:
limits:
cpus: '4.0'
memory: 4G
reservations:
cpus: '1.0'
memory: 1G
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- db_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_DB: ${POSTGRES_DB}
secrets:
- db_password
networks:
- backend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Redis キャッシュ
redis:
image: redis:7-alpine
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1.0'
memory: 1G
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
command: >
redis-server
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--requirepass ${REDIS_PASSWORD}
--appendonly yes
volumes:
- redis_data:/data
networks:
- backend
# リバースプロキシ
nginx:
image: nginx:1.25-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certbot/conf:/etc/letsencrypt:ro
- ./certbot/www:/var/www/certbot:ro
depends_on:
app:
condition: service_healthy
networks:
- frontend
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "5"
volumes:
db_data:
driver: local
redis_data:
driver: local
networks:
frontend:
driver: bridge
backend:
driver: bridge
internal: true # 外部アクセス不可
secrets:
db_password:
file: ./secrets/db_password.txt
api_key:
file: ./secrets/api_key.txt
セキュリティ強化
コンテナセキュリティの原則
| 原則 | 説明 | 実装方法 |
|---|---|---|
| 最小権限 | 必要最小限の権限でコンテナを実行 | non-root user, read_only |
| イメージの信頼性 | 信頼できるベースイメージのみ使用 | 公式イメージ、署名検証 |
| ネットワーク分離 | コンテナ間のネットワークを最小限に | internal network |
| シークレット管理 | 機密情報を安全に管理 | Docker secrets, Vault |
| リソース制限 | リソース消費を制限 | deploy.resources |
| 脆弱性スキャン | 定期的な脆弱性チェック | Trivy, Snyk |
セキュアなDockerfileの構築
# マルチステージビルドによるセキュアなイメージ構築
# ステージ1: ビルド
FROM node:20-alpine AS builder
# セキュリティ更新の適用
RUN apk update && apk upgrade --no-cache
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production && npm cache clean --force
COPY . .
RUN npm run build
# ステージ2: 本番イメージ
FROM node:20-alpine AS production
# セキュリティ更新の適用
RUN apk update && apk upgrade --no-cache \
&& apk add --no-cache dumb-init curl \
&& rm -rf /var/cache/apk/*
# non-rootユーザーの作成
RUN addgroup -g 1001 -S appgroup \
&& adduser -S appuser -u 1001 -G appgroup
WORKDIR /app
# ビルド成果物のコピー
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=builder --chown=appuser:appgroup /app/package.json ./
# non-rootユーザーに切り替え
USER appuser
# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
EXPOSE 3000
# dumb-initでPID 1問題を解決
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/server.js"]
脆弱性スキャンの自動化
# .github/workflows/security-scan.yml
name: Container Security Scan
on:
push:
branches: [main]
schedule:
- cron: '0 2 * * 1' # 毎週月曜 AM2時
jobs:
trivy-scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t myapp:scan .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:scan'
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
exit-code: '1'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
docker-bench:
runs-on: ubuntu-latest
steps:
- name: Run Docker Bench Security
run: |
docker run --rm --net host --pid host \
--userns host --cap-add audit_control \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /etc:/etc:ro \
docker/docker-bench-security
監視・ログ管理
Prometheus + Grafana による監視スタック
# docker-compose.monitoring.yml
version: '3.8'
services:
prometheus:
image: prom/prometheus:v2.50.0
restart: unless-stopped
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- ./monitoring/alerts.yml:/etc/prometheus/alerts.yml:ro
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.retention.time=30d'
- '--web.enable-lifecycle'
ports:
- "9090:9090"
networks:
- monitoring
grafana:
image: grafana/grafana:10.3.0
restart: unless-stopped
environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_PASSWORD}
- GF_USERS_ALLOW_SIGN_UP=false
volumes:
- grafana_data:/var/lib/grafana
- ./monitoring/grafana/dashboards:/etc/grafana/provisioning/dashboards:ro
- ./monitoring/grafana/datasources:/etc/grafana/provisioning/datasources:ro
ports:
- "3001:3000"
networks:
- monitoring
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.0
restart: unless-stopped
privileged: true
volumes:
- /:/rootfs:ro
- /var/run:/var/run:ro
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
ports:
- "8080:8080"
networks:
- monitoring
node-exporter:
image: prom/node-exporter:v1.7.0
restart: unless-stopped
volumes:
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /:/rootfs:ro
command:
- '--path.procfs=/host/proc'
- '--path.sysfs=/host/sys'
- '--path.rootfs=/rootfs'
networks:
- monitoring
alertmanager:
image: prom/alertmanager:v0.27.0
restart: unless-stopped
volumes:
- ./monitoring/alertmanager.yml:/etc/alertmanager/alertmanager.yml:ro
ports:
- "9093:9093"
networks:
- monitoring
volumes:
prometheus_data:
grafana_data:
networks:
monitoring:
driver: bridge
Prometheus設定とアラートルール
# monitoring/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alerts.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
- job_name: 'app'
static_configs:
- targets: ['app:3000']
metrics_path: '/metrics'
# monitoring/alerts.yml
groups:
- name: container_alerts
rules:
- alert: ContainerHighCPU
expr: rate(container_cpu_usage_seconds_total{name!=""}[5m]) > 0.8
for: 5m
labels:
severity: warning
annotations:
summary: "コンテナCPU使用率が高い ({{ $labels.name }})"
description: "CPU使用率が80%を超えています: {{ $value | humanizePercentage }}"
- alert: ContainerHighMemory
expr: container_memory_usage_bytes{name!=""} / container_spec_memory_limit_bytes{name!=""} > 0.85
for: 5m
labels:
severity: warning
annotations:
summary: "コンテナメモリ使用率が高い ({{ $labels.name }})"
- alert: ContainerDown
expr: absent(container_last_seen{name=~".*app.*"})
for: 1m
labels:
severity: critical
annotations:
summary: "アプリケーションコンテナがダウンしています"
- alert: HighDiskUsage
expr: (node_filesystem_size_bytes - node_filesystem_free_bytes) / node_filesystem_size_bytes > 0.85
for: 10m
labels:
severity: warning
annotations:
summary: "ディスク使用率が85%を超えています"
ログ集約(Loki + Promtail)
# docker-compose.logging.yml
version: '3.8'
services:
loki:
image: grafana/loki:2.9.0
restart: unless-stopped
volumes:
- ./monitoring/loki-config.yml:/etc/loki/local-config.yaml:ro
- loki_data:/loki
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
networks:
- monitoring
promtail:
image: grafana/promtail:2.9.0
restart: unless-stopped
volumes:
- ./monitoring/promtail-config.yml:/etc/promtail/config.yml:ro
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
command: -config.file=/etc/promtail/config.yml
networks:
- monitoring
volumes:
loki_data:
CI/CDパイプライン
GitHub ActionsによるCI/CDの実装
# .github/workflows/deploy.yml
name: Build and Deploy
on:
push:
branches: [main]
tags: ['v*']
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: |
docker compose -f docker-compose.test.yml up --build --abort-on-container-exit
docker compose -f docker-compose.test.yml down -v
build:
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Login to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=semver,pattern={{version}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- uses: actions/checkout@v4
- name: Deploy to production
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /opt/myapp
export APP_VERSION=${{ github.sha }}
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d --remove-orphans
docker image prune -f
# ヘルスチェック
sleep 10
curl -f http://localhost:3000/health || exit 1
ゼロダウンタイムデプロイ
# docker-compose.prod.yml(ローリングアップデート対応)
services:
app:
image: registry.example.com/myapp:${APP_VERSION}
restart: unless-stopped
deploy:
replicas: 2
update_config:
parallelism: 1
delay: 30s
order: start-first
rollback_config:
parallelism: 1
delay: 10s
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 30s
#!/bin/bash
# deploy.sh - ゼロダウンタイムデプロイスクリプト
set -euo pipefail
APP_VERSION="${1:?バージョンを指定してください}"
COMPOSE_FILE="docker-compose.prod.yml"
echo "=== デプロイ開始: v${APP_VERSION} ==="
# 新しいイメージの取得
export APP_VERSION
docker compose -f "$COMPOSE_FILE" pull app
# ローリングアップデート
docker compose -f "$COMPOSE_FILE" up -d --no-deps --scale app=2 app
# 新コンテナのヘルスチェック待ち
echo "新コンテナのヘルスチェック待機中..."
sleep 30
# ヘルスチェック確認
if ! curl -sf http://localhost:3000/health > /dev/null; then
echo "ヘルスチェック失敗。ロールバックを実行します。"
docker compose -f "$COMPOSE_FILE" rollback app
exit 1
fi
echo "=== デプロイ完了: v${APP_VERSION} ==="
# 未使用イメージの削除
docker image prune -f
バックアップとリストア
データベースバックアップの自動化
# docker-compose.backup.yml
services:
db-backup:
image: postgres:16-alpine
environment:
PGHOST: db
PGUSER: ${POSTGRES_USER}
PGPASSWORD_FILE: /run/secrets/db_password
PGDATABASE: ${POSTGRES_DB}
secrets:
- db_password
volumes:
- ./backups:/backups
entrypoint: /bin/sh
command: >
-c 'pg_dump -Fc > /backups/db_$(date +%Y%m%d_%H%M%S).dump
&& find /backups -name "db_*.dump" -mtime +7 -delete
&& echo "バックアップ完了: $(date)"'
networks:
- backend
profiles:
- backup
#!/bin/bash
# backup.sh - 定期バックアップスクリプト
set -euo pipefail
BACKUP_DIR="/opt/myapp/backups"
RETENTION_DAYS=7
S3_BUCKET="s3://myapp-backups"
# データベースバックアップ
docker compose -f docker-compose.prod.yml \
--profile backup \
run --rm db-backup
# S3へのアップロード
LATEST_BACKUP=$(ls -t "${BACKUP_DIR}"/db_*.dump | head -1)
aws s3 cp "$LATEST_BACKUP" "$S3_BUCKET/$(basename "$LATEST_BACKUP")"
# 古いバックアップの削除
find "$BACKUP_DIR" -name "db_*.dump" -mtime +${RETENTION_DAYS} -delete
echo "バックアップ完了: $(basename "$LATEST_BACKUP")"
トラブルシューティング
よくある問題と対処法
| 問題 | 原因 | 対処法 |
|---|---|---|
| コンテナが再起動を繰り返す | OOMKill / ヘルスチェック失敗 | docker inspect でログを確認、リソース制限を緩和 |
| ネットワーク接続エラー | DNS解決失敗 / ネットワーク分離 | docker network inspect で確認 |
| ディスク容量不足 | ログ・イメージの蓄積 | docker system prune と定期クリーンアップ |
| ビルドが遅い | キャッシュ無効 / レイヤー最適化不足 | BuildKit有効化、マルチステージビルド |
| シークレット漏洩 | 環境変数にハードコード | Docker secrets / Vault使用 |
まとめ
Docker Composeを本番環境で運用する際のポイントをまとめます。
- セキュリティ: non-rootユーザー、read_onlyファイルシステム、ネットワーク分離、脆弱性スキャンの自動化
- 監視: Prometheus + Grafana + Loki によるメトリクス・ログの統合監視
- CI/CD: GitHub Actionsによるテスト・ビルド・デプロイの自動化とゼロダウンタイムデプロイ
- バックアップ: データベースの定期バックアップとリモートストレージへの保存
- リソース管理: CPU/メモリの制限設定、ヘルスチェック、再起動ポリシーの適切な設定
Kubernetesへの移行が必要になるまでは、適切に設計されたDocker Compose環境で十分に本番運用が可能です。本記事の設定をベースに、プロジェクトの要件に合わせてカスタマイズしてください。
関連記事
カテゴリー