はじめに
Claude CodeにDockerfileを書かせると、シングルステージのまま返ってくることがある。ビルドツールや開発依存がそのまま本番イメージに入り、1GB超になる。CLAUDE.mdにDockerfileポリシーを定義すれば、マルチステージビルドと非rootユーザーを前提にしたコードが出てくる。よくある誤りパターンの修正から言語別Distrolessテンプレート、TrivyスキャンのCI統合まで一気通貫で扱う。
Claude CodeはどんなDockerfileを生成するか──よくあるパターンと修正方法
シングルステージで本番依存が肥大化する
CLAUDE.mdにルールがないと、FROM python:3.12から始まるシングルステージが返ってくる。pip・コンパイラ・テストツールが本番イメージに残りPythonでも1GB超になる。BuildとReleaseステージを分離すれば数分の1に圧縮できる。
rootユーザーで実行する
USER命令がないとコンテナはrootで動く。コンテナ侵害時にホストカーネルへの攻撃が容易になる。debian系ならRUN groupadd -r appuser && useradd -r -g appuser appuserでユーザーを作りUSER appuserで切り替える。distrolessはUSER nonroot:nonroot(UID 65532)が組み込まれているため1行で済む。
.dockerignoreを置かない
Claude Codeが生成するDockerfileに.dockerignoreが付いてこないケースがある。.git・.env・node_modules・__pycache__がビルドコンテキストに含まれると、ビルドが遅くなり.envがイメージレイヤーに混入して秘密情報が漏洩するリスクがある。
最低限の.dockerignoreは.git・.env・.env.*・node_modules・__pycache__・distだ。これをCLAUDE.mdに書いておく。COPYキャッシュ順序(依存定義ファイル→依存インストール→ソースコード)とlatestタグ禁止はテンプレート1行の注釈でカバーできる。
## Dockerfileポリシー
マルチステージビルド必須(BuildとReleaseステージを分離)
非rootユーザー必須(debian系: appuser作成 / distroless: USER nonroot:nonroot)
Dockerfileと同時に.dockerignoreを生成(.git/.env/.env.*を除外)
COPYは依存定義ファイル→依存インストール→ソースコードの順
latestタグ禁止(node:20-slim・python:3.12-slim-bookworm 等に固定) 言語別マルチステージビルド──GoとPythonのDistroless実践
Goは静的バイナリ × distroless/staticで2MB
CGO_ENABLED=0でGoを静的バイナリにコンパイルし、gcr.io/distroless/static-debian12:nonrootに入れる。distrolessはCA証明書・タイムゾーン・nonrootユーザーを内包しており、scratchと違いHTTPS通信がそのまま使える。イメージサイズは約2MBだ。
FROM golang:1.25-bookworm AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app/server /server
EXPOSE 8080
CMD ["/server"]Pythonはvirtualenv + distroless/python3
Pythonは静的バイナリ化できない。virtualenvを/opt/venvに作りdistrolessのPythonランタイムにCOPYする。distrolessはシェルがないためCMDにはフルパスを指定する。
FROM python:3.12-slim-bookworm AS builder
WORKDIR /app
RUN python -m venv /opt/venv
COPY requirements.txt .
RUN /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
FROM gcr.io/distroless/python3-debian12
WORKDIR /app
COPY --from=builder /opt/venv /opt/venv
COPY src/ ./src/
USER nonroot:nonroot
EXPOSE 8000
CMD ["/opt/venv/bin/python", "-m", "uvicorn", "src.main:app", "--host", "0.0.0.0", "--port", "8000"]docker-composeのhealthcheck × depends_on設計
アプリが起動してもDBが初期化中でエラーになる「起動順序の競合」は開発環境で頻発する。depends_on: condition: service_healthyで解決できる。
services:
db:
image: postgres:17-alpine
env_file: .env
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
app:
build: .
env_file: .env
ports: ["8080:8080"]
depends_on:
db:
condition: service_healthyCLAUDE.mdに「DBにhealthcheckを設定し、アプリはservice_healthyになってから起動」「環境変数は.envから読み込む」を書いておくと、依頼時にこのパターンが出てくる。
TrivyでセキュリティスキャンをCIに組み込む
CLAUDE.mdでDockerfileポリシーを定義しても、イメージに既知の脆弱性が入っていないかはスキャンしないと分からない。TrivyをGitHub Actionsに組み込む最小構成を示す。
# .github/workflows/docker-security.yml(主要ステップのみ)
run: docker build -t myapp:${{ github.sha }} .
uses: aquasecurity/trivy-action@0.30.0
with:
image-ref: myapp:${{ github.sha }}
format: sarif
output: trivy.sarif
severity: CRITICAL,HIGH
exit-code: 1
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy.sarif最初はexit-code: 0で検出だけにしておき、HIGH/CRITICALの件数を確認してからexit-code: 1でブロックを有効化する。ローカルならdocker scout cves でDocker CLIの即時スキャンができる。
まとめ
CLAUDE.mdに「マルチステージビルド必須・非rootユーザー必須・.dockerignore必須」の3行を追記してほしい。既存Dockerfileに「CLAUDE.mdのポリシーに合わせて改善して」と依頼すれば、言語別Distrolessパターンへのリファクタリングが行われる。TrivyをCIに組み込めば、ポリシーの継続的な維持が自動化される。

コメント