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からのみ
networks:
frontend-net:
backend-net:
必要なものだけを公開
services:
db:
ports:
- "127.0.0.1:5432:5432" # ホストからのみアクセス可能、ネットワークからは不可
# 本番ではポートを完全に省略 -- 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カーネルを共有するため、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 の実行可能キャッシュを、実行可能な
/tmpマウント上のNPM_CONFIG_CACHE=/tmp/npm-cacheに保持します。デフォルトサイズは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、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 にコピーします。実行中であることを確認し、ターミナルオープナーのDocker側v1データコントラクトを出力します:
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: 使用してはいけないパターン
# 1. :latest タグの使用
# 2. ルートユーザーでの実行
# 3. シークレットのイメージへのハードコード
# 4. バインドマウントの過剰な使用(本番)
# 5. ヘルスチェックの欠如
# 6. 依存関係の順序制御の欠如(depends_on: condition なし)
# 7. 巨大なイメージ(マルチステージビルドを使用しない)
# 8. ホストネットワークモードの使用
コンテナイメージのベストプラクティス
- 最小限のベースイメージを使用(alpine、distroless)
- マルチステージビルドを活用してイメージサイズを削減
- すべてのレイヤーをラベル付けし、イメージにラベルを追加
- 定期的にイメージをスキャンして脆弱性をチェック
- 不変のタグ(SHA256ダイジェスト)を使用して再現性を確保
- HEALTHCHECK命令を含める
- 機密情報がレイヤーに残らないように注意
リソース制限と監視
services:
app:
deploy:
resources:
limits:
cpus: "0.5"
memory: 256M
reservations:
cpus: "0.25"
memory: 128M
# docker compose 以外の場合:
# cpus: "0.5"
# mem_limit: 256m