「Dockerfileが怖い」を卒業する──Claude Code × Go/Python別マルチステージビルドとTrivyスキャンを自動化する

はじめに

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.envnode_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_healthy

    CLAUDE.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に組み込めば、ポリシーの継続的な維持が自動化される。

    コメント

    タイトルとURLをコピーしました