Docker Patterns
コンテナ化開発のための Docker と Docker Compose のベストプラクティス。
ローカル開発向け Docker Compose
標準的な Web アプリケーションスタック
# docker-compose.yml
services:
app:
build:
context: .
target: dev # マルチステージ Dockerfile の dev ステージを使用
ports:
- "3000:3000"
volumes:
- .:/app # ホットリロード用のバインドマウント
- /app/node_modules # 匿名ボリューム -- コンテナの依存関係を保持
environment:
- DATABASE_URL=postgres://postgres:postgres@db:5432/app_dev
- REDIS_URL=redis://redis:6379/0
- NODE_ENV=development
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: npm run dev
db:
image: postgres:16-alpine
ports:
- "5432:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: app_dev
volumes:
- pgdata:/var/lib/postgresql/data
- ./scripts/init-db.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
mailpit: # ローカルメールテスト
image: axllent/mailpit
ports:
- "8025:8025" # Web UI
- "1025:1025" # SMTP
volumes:
pgdata:
redisdata:
開発用と本番用の Dockerfile
# Stage: dependencies
FROM node:22-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Stage: dev (ホットリロード、デバッグツール)
FROM node:22-alpine AS dev
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Stage: build
FROM node:22-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build && npm prune --production
# Stage: production (最小イメージ)
FROM node:22-alpine AS production
WORKDIR /app
RUN addgroup -g 1001 -S appgroup && adduser -S appuser -u 1001
USER appuser
COPY --from=build --chown=appuser:appgroup /app/dist ./dist
COPY --from=build --chown=appuser:appgroup /app/node_modules ./node_modules
COPY --from=build --chown=appuser:appgroup /app/package.json ./
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/server.js"]
オーバーライドファイル
# docker-compose.override.yml (自動読み込み、開発専用設定)
services:
app:
environment:
- DEBUG=app:*
- LOG_LEVEL=debug
ports:
- "9229:9229" # Node.js デバッガ
# docker-compose.prod.yml (本番用に明示的に指定)
services:
app:
build:
target: production
restart: always
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
# 開発 (オーバーライドを自動読み込み)
docker compose up
# 本番
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
ネットワーク
サービスディスカバリ
同じ Compose ネットワーク内のサービスは、サービス名で解決されます:
# "app" コンテナから:
postgres://postgres:postgres@db:5432/app_dev # "db" は db コンテナに解決
redis://redis:6379/0 # "redis" は redis コンテナに解決
カスタムネットワーク
services:
frontend:
networks:
- frontend-net
api:
networks:
- frontend-net
- backend-net
db:
networks:
- backend-net # api からのみ到達可能、frontend からは不可
networks:
frontend-net:
backend-net:
必要なものだけを公開
services:
db:
ports:
- "127.0.0.1:5432:5432" # ホストからのみアクセス可能、ネットワークからは不可
# 本番では ports を完全に省略 -- Docker ネットワーク内でのみアクセス可能
ボリューム戦略
volumes:
# 名前付きボリューム: コンテナ再起動後も永続化、Docker が管理
pgdata:
# バインドマウント: ホストディレクトリをコンテナにマッピング (開発用)
# - ./src:/app/src
# 匿名ボリューム: バインドマウントの上書きからコンテナ生成コンテンツを保護
# - /app/node_modules
一般的なパターン
services:
app:
volumes:
- .:/app # ソースコード (ホットリロード用バインドマウント)
- /app/node_modules # コンテナの node_modules をホストから保護
- /app/.next # ビルドキャッシュを保護
db:
volumes:
- pgdata:/var/lib/postgresql/data # 永続データ
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql # 初期化スクリプト
コンテナセキュリティ
Dockerfile のハードニング
# 1. 特定のタグを使用する (:latest は避ける)
FROM node:22.12-alpine3.20
# 2. 非 root で実行
RUN addgroup -g 1001 -S app && adduser -S app -u 1001
USER app
# 3. ケーパビリティを削除 (compose で)
# 4. 可能な限り読み取り専用ルートファイルシステム
# 5. イメージレイヤーにシークレットを含めない
Compose のセキュリティ
services:
app:
security_opt:
- no-new-privileges:true
read_only: true
tmpfs:
- /tmp
- /app/.cache
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE # ポート < 1024 にバインドする場合のみ必要
シークレット管理
# GOOD: 環境変数を使用 (実行時に注入)
services:
app:
env_file:
- .env # .env は git にコミットしない
environment:
- API_KEY # ホスト環境から継承
# GOOD: Docker シークレット (Swarm モード)
secrets:
db_password:
file: ./secrets/db_password.txt
services:
db:
secrets:
- db_password
# BAD: イメージにハードコード
# ENV API_KEY=sk-proj-xxxxx # 絶対にしないでください
ハードニングされた CLI インストーラーハーネス
コンテナを使用して、ソースチェックアウトを変更せずに、使い捨てのプロジェクトコピーに対してインストーラーの動作をテストします。
プラットフォームの境界を尊重する
- Debian や Ubuntu などの Linux ディストリビューションには実際のコンテナを実行します。
- macOS は Docker が Linux カーネルを共有するため、Docker コンテナとして実行できません。同じシェルフリーのテストエントリポイントを macOS 上でネイティブに実行します。
- Windows コンテナには Windows Docker エンジンが必要です。プラットフォームに依存しないロジックはネイティブの Windows CI ランナーで実行し、Windows コンテナは Windows ホスト用に予約します。
- ホスト固有のパス、コマンドラッパー、引用符、ファイルシステムの動作をテストするために、ネイティブの Ubuntu/macOS/Windows CI マトリックスを維持します。
Linux コンテナが macOS や Windows の動作を検証すると主張しないでください。
分離契約を強制する
- ベースイメージは不変のダイジェストで固定し、インストールする CLI バージョンも固定します。
- ディストリビューションごとにアカウント名が異なる場合に備え、非 root の数値 UID/GID で実行します。
- リポジトリとソースプロジェクトは読み取り専用でマウントします。
- 変更を加える前に、ソースプロジェクトを書き込み可能な
tmpfsワークスペースにコピーします。 /workspaceはnoexec、UID/GID 1000、mode=0700でマウントし、コンテナユーザーのみがプロジェクトデータを検査できるようにします。- npm と npx の実行可能キャッシュは
NPM_CONFIG_CACHE=/tmp/npm-cacheに、実行可能な/tmpマウント上に保持します。デフォルトサイズは 2 GiB で、ECC_TMPFS_SIZEで調整可能です。ECC_WORKSPACE_SIZEはプライベートワークスペースマウントを個別に制御します。 read_only: true、no-new-privileges:true、cap_drop: [ALL]、および有限のpids_limitを設定します。- デフォルトの実際の CLI サービスは
network_mode: noneに保ちます。ネットワークアクセスは、認証済みプロバイダーセッション用の名前付きオプトインサービスでのみ、明示的に追加します。偶発的な環境依存のデフォルトにしてはいけません。 - ツールが必要とする書き込み可能な一時パスのみを作成します。
- デフォルトでは、ホストの認証情報をコンテナに渡さないでください。
- デフォルトは dry-run とし、明示的な
dry-run、install、plugin、shellモードのみを許可します。 - クロスプラットフォームランナーには、引数配列または
spawnSync(..., { shell: false })を使用します。プロジェクトパスをシェルコマンドに展開しないでください。
ECC プラグインセットアップハーネスを実行する
docker/plugin-setup/compose.yaml をリファレンス実装として使用します。以下を提供します:
fixture-tests:フォーカスされたインストールマニフェスト、ターゲット、およびエグゼキュータスイート用。real-cli:固定された Debian ベースの汎用 Linux イメージ用。real-cli-ubuntu:固定された Ubuntu イメージ用。
ビルド前に Compose モデルを検証します:
docker compose -f docker/plugin-setup/compose.yaml config --quiet
両方の実際の Linux イメージをビルドします:
docker compose -f docker/plugin-setup/compose.yaml \
build real-cli real-cli-ubuntu
各イメージで安全なデフォルトフローを実行します:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli dry-run
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml \
run --rm -T real-cli-ubuntu dry-run
ドライランは現在のパブリックコマンド契約を実行します:
ecc install --profile core --target claude-project --dry-run --json
そのコマンドが実行される前に、コンテナは読み取り専用チェックアウトから npm pack --ignore-scripts でローカルにパッケージ化された npm アーティファクトを作成します。自己生成した tarball を /tmp の下に展開し、ecc-universal パッケージ名、必要なインストールマニフェスト、制限された package.json の bin.ecc マッピングを検証し、展開された ecc 実行可能ファイルを呼び出します。ランタイムは network_mode: none のままで、パッケージライフサイクルスクリプトを実行せず、ホストの node_modules に依存しません。その正確に固定された本番依存関係はすでにイメージに含まれています。
ハーネスは、空のプラン、claude-project 以外のターゲット、/workspace/project/.claude 外の操作、またはターゲットディレクトリを作成するドライランを拒否します。install は隔離された適用を2回実行し、管理されたインストール状態をチェックし、インストールされたターゲットを一覧表示し、doctor を実行します。
名前付きセッションの開始、開く、再接続、およびクリーンアップ
--rm なしでデタッチドコンテナを開始し、ターミナルを離れてもセッションが削除されないようにします:
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
run --detach --name ecc-plugin-shell real-cli shell
コンテナは読み取り専用フィクスチャを安定したプライベートディレクトリ /workspace/project にコピーします。実行中であることを確認し、ターミナルオープナー v1 データ契約の Docker 側を出力します:
docker inspect --format '{{.State.Running}}' ecc-plugin-shell
node docker/plugin-setup/interactive-plan.js \
--container ecc-plugin-shell \
--workdir /workspace/project \
--json \
-- bash
JSON 結果には、正確に executable と argv の境界(および contractVersion: 1)が含まれます:実行可能ファイルは docker で、argv は exec、-it、-w で始まります。そのデータを、ターミナルオープナースキルがインストールされている場合は、別のターミナルオープナースキルに渡します。この Docker ハーネスは、ターミナルアダプターのインポート、シェルコマンドの展開、またはホスト GUI プロセスの管理を意図的に行いません。それまでは、現在のホストターミナルで同じ PTY を直接開きます:
docker exec -it -w /workspace/project ecc-plugin-shell bash
デタッチドコンテナを停止せずにシェルを終了します。同じ docker exec -it コマンドで再接続します。完了したら、正確な名前付きコンテナとその Compose プロジェクトリソースを削除します:
docker rm --force ecc-plugin-shell
docker compose -p ecc-plugin-session \
-f docker/plugin-setup/compose.yaml \
down --remove-orphans
ホスト認証情報はデフォルトでは存在せず、認証情報ディレクトリはマウントされません。デフォルトのサービスはネットワークアクセスもありません。認証済みプロバイダーセッションが本当にネットワークを必要とする場合は、最初に real-cli をビルドし、docker compose --profile networked run real-cli-networked shell で明示的にオプトインします。その使い捨てセッション内で認証することを推奨します。CI 実行がホスト環境の認証情報を継承する必要がある場合は、呼び出し時に明示的な Compose --env NAME フラグでオプトインし、その値が検査可能でコンテナの存続期間中に流出する可能性があることを理解し、正確な名前付きコンテナをすぐに削除します。
同じフォーカスされたスイートをホスト上でネイティブに実行します:
npm run test:plugin-setup-platform
イメージを信頼する前に、生成された ID と環境を検査します:
docker image inspect ecc-plugin-setup:debian ecc-plugin-setup:ubuntu
関連のないボリュームやイメージを削除せずに、各名前付きテストプロジェクトをクリーンアップします:
docker compose -p ecc-plugin-debian-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
docker compose -p ecc-plugin-ubuntu-test \
-f docker/plugin-setup/compose.yaml down --remove-orphans
.dockerignore
node_modules
.git
.env
.env.*
dist
coverage
*.log
.next
.cache
docker-compose*.yml
Dockerfile*
README.md
tests/
デバッグ
一般的なコマンド
# ログを表示
docker compose logs -f app # app のログを追跡
docker compose logs --tail=50 db # db の最新50行
# 実行中のコンテナでコマンドを実行
docker compose exec app sh # app にシェルで入る
docker compose exec db psql -U postgres # postgres に接続
# 検査
docker compose ps # 実行中のサービス
docker compose top # 各コンテナのプロセス
docker stats # リソース使用量
# リビルド
docker compose up --build # イメージを再ビルド
docker compose build --no-cache app # 完全リビルドを強制
# クリーンアップ
docker compose down # コンテナを停止して削除
docker compose down -v # ボリュームも削除 (破壊的)
docker system prune # 未使用イメージ/コンテナを削除
ネットワーク問題のデバッグ
# コンテナ内の DNS 解決を確認
docker compose exec app nslookup db
# 接続性を確認
docker compose exec app wget -qO- http://api:3000/health
# ネットワークを検査
docker network ls
docker network inspect <project>_default
アンチパターン
# BAD: 最新のイメージタグを使用
FROM node:latest
# BAD: ルートで実行
USER root
# BAD: イメージにシークレットをハードコード
ENV API_KEY=sk-proj-xxxxx
# BAD: ビルドコンテキストに .env を含める
# (.dockerignore に追加すること)
# BAD: 依存関係の順序を正しく設定しない
# depends_on に condition: service_healthy を使用せず、サービスが準備できる前に接続しようとする
# BAD: 本番 Compose ファイルに開発ボリュームを残す
services:
app:
volumes:
- .:/app # 本番では絶対にしないでください