Claude Media
Claude CodeでDockerfileを最適化する手順とベストプラクティス

Claude CodeでDockerfileを最適化する手順とベストプラクティス

Claude CodeにDockerfileを読ませ、マルチステージビルド化とキャッシュ設計の見直しでイメージサイズとビルド時間を削減する手順を、権限設定まで含めて解説します。

Claude CodeでDockerfileを最適化するとは

Claude CodeでDockerfileを最適化するとは、既存のDockerfileをClaude Codeに読ませ、Docker公式のベストプラクティスに沿ってマルチステージビルド化・レイヤー順序の見直し・不要な依存の削除を指示する作業です。手動で1行ずつ見直すより、差分の洗い出しと書き換えを一度に進められます。

Dockerfileの最適化とは、ビルド時間・イメージサイズ・キャッシュの再利用率を改善するために、命令の並び順や使う命令そのものを見直す作業を指します。効果が大きいのはマルチステージビルド化とベースイメージの選び直しで、地味だが効くのがレイヤー順序と.dockerignoreです。

始める前に3つを用意します。Docker CLIとBuildxが有効なDockerホスト、対象リポジトリでのClaude Codeの起動、そして書き換え前のDockerfileです。言語やフレームワークは問いません。Node.js・Python・Goのいずれでも、ベストプラクティスの適用手順自体は共通です。

全般的なタスク分割やCLAUDE.mdの設計といったClaude Code運用の土台部分はClaude Codeベストプラクティスにまとめています。この記事はDockerfileという1つの対象に絞った実務手順です。

最適化の施策は何に効くのか

最適化には効き先の異なる施策があり、すべてを一度に変えるとレビューしづらい差分になります。何を縮めたいのかを先に決めてから、Claude Codeへの指示を組み立てます。

施策

施策と効き先

  • イメージサイズを縮める

    マルチステージビルド化で、最終イメージにはビルドツールを入れず実行に要るファイルだけを残します。小さなベースイメージに替える手もあり、DockerはAlpine(6MB未満)を勧めています。

  • ビルド時間を縮める

    .dockerignoreでコンテキストから不要なファイルを外し、変更の少ない命令を前に置いてキャッシュを再利用します。マルチステージなら、独立したステージを並列にビルドできます。

  • 再現性を保つ

    タグは発行元が別のイメージに付け替えられるため、ダイジェストでpin留めすると同じベースを使い続けられます。代わりにセキュリティ修正の自動取り込みは手放すことになります。ベースイメージを自動で更新したい場合は、package-ecosystem: "docker"を指定したGitHub Dependabotが、ベースイメージのタグとダイジェストを更新するプルリクエストを定期的に作ります。

イメージサイズを最優先するならマルチステージビルド化とベースイメージの見直しから、CIのビルド時間を優先するならレイヤー順序と.dockerignoreから着手します。

ステップ1: Claude CodeにDockerfileを診断させる

最初のステップは、書き換えを急がずClaude CodeにDockerfileと関連ファイルを読ませ、ベストプラクティスとの差分だけを洗い出させることです。診断結果を先に見ておくと、後の変更がどこから来たのか説明できます。

Dockerfileを読んで、Docker公式のベストプラクティス(マルチステージビルド・ベースイメージの選定・レイヤー順序・.dockerignoreの有無)に沿っていない点を診断してください。書き換えはまだ不要です。

例えば、次のような単一ステージのDockerfileがあるとします。

FROM node:22
 
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
 
CMD ["node", "dist/server.js"]

このDockerfileには複数の問題があります。COPY . .が依存関係のインストールより前にあるため、ソースを1行直しただけでnpm installの層までキャッシュが無効になります。ビルドツール一式が最終イメージに残り、.dockerignoreが無ければnode_modulesや.gitまで送信対象になります。Claude Codeにはこの診断結果を先に言語化させ、修正の根拠として残します。

ステップ2: マルチステージビルドへの書き換えを指示する

診断結果を確認したら、Claude Codeに具体的な書き換えを指示します。マルチステージビルドは、ビルド専用のステージと実行専用のステージを分け、最終イメージにはビルドツールを含めない構成です。ステージ間で共通処理があるなら、それを再利用可能な共通ステージにまとめるのが公式の推奨です。

診断結果を反映してマルチステージビルドに書き換えてください。ビルド用ステージと実行用ステージを分け、実行用ステージにはnpmやソースの中間ファイルを残さないようにしてください。実行用ステージのnode_modulesに開発用の依存が入らないようにもしてください。

書き換え後は次のような形になります。

# syntax=docker/dockerfile:1
FROM node:22 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
 
FROM node:22-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

依存関係のインストール(npm ci)をソース全体のコピーより前に置いているため、アプリケーションコードだけを変更した再ビルドでは依存関係のレイヤーがキャッシュから再利用されます。最終イメージのベースもnode:22-slimに切り替え、ビルド専用のツールチェーンを持ち込まない構成にしています。

この例には見落としやすい点が1つあります。buildステージのnpm ciは開発用の依存も入れるので、node_modulesをそのままコピーすると実行イメージにも開発用の依存が残ります。npmの--omit=devは、指定した種類の依存をディスクに入れないオプションです。本番用の依存だけを入れるステージを別に作り、実行ステージはそこからコピーします。

FROM node:22-slim AS prod-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
 
FROM node:22-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=prod-deps /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]

prod-depsはbuildと独立したステージなので、2つを並列にビルドできます。

ステップ3: レイヤー順序とキャッシュ設計を見直す

マルチステージ化のあとは、レイヤーの順序自体を見直します。Dockerは各命令を順番に実行し、変更が無い命令はキャッシュから再利用します。変更頻度が低いレイヤーを先に、頻繁に変わるレイヤー(アプリケーションコードのコピーなど)を後ろに置くことが、キャッシュの再利用率を左右します。

RUNで複数のパッケージをインストールする場合、ベストプラクティスは引数をアルファベット順に並べることを推奨しています。並び順が揃っていると、パッケージの重複追加を防げ、差分レビューも読みやすくなります。

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    curl \
    git \
    && rm -rf /var/lib/apt/lists/*

apt-get updateは必ず同じRUNの中でapt-get installと連結します。別々のRUNに分けると、installの行だけを書き換えたときにupdateの層がキャッシュから再利用され、古いパッケージ一覧のままインストールが失敗することがあります。パッケージのバージョンを指定すると、apt-get updateのキャッシュが破棄される点にも注意が要ります。末尾のrm -rf /var/lib/apt/lists/*は、パッケージ一覧のキャッシュをレイヤーに残さず、サイズを減らすための行です。公式のDebianとUbuntuのイメージはapt-get cleanを自動で実行するため、これを明示する必要はありません。

RUNで複数パッケージをインストールしている箇所を探し、引数をアルファベット順に並び替えてください。apt-get updateとinstallが別のRUNに分かれていれば連結してください。合わせて.dockerignoreを作成し、node_modules・.git・distを除外対象に追加してください。

.dockerignoreが無いと、ビルドコンテキスト全体(node_modulesや.gitを含む)がDockerデーモンに送信されます。除外パターンは.gitignoreに似た書式で書けるため、既存の.gitignoreをベースにClaude Codeへ変換を指示すると手早く済みます。

ステップ4: 変更をビルドして検証する

書き換えが終わったら、キャッシュに頼らない状態で一度ビルドし、意図した通りに動くかを確認します。Docker CLI 20.10.12のdocker build --helpは、関係するフラグを次のように説明します。

--no-cache         Do not use cache when building the image
--pull             Always attempt to pull a newer version of the image
--target string    Set the target build stage to build.

--pullは最新のベースイメージを取得し直すフラグ、--no-cacheはレイヤーキャッシュを無視して全命令を再実行するフラグです。目的が別なので、クリーンな検証では両方を組み合わせます。

docker build --pull --no-cache -t my-app:optimized .
docker run --rm my-app:optimized node -e "console.log('ok')"

--targetはステージ名を指定して、そのステージまでをビルドします。マルチステージ化した直後は、docker build --target build .でビルド用ステージだけを単独で通すと、失敗する場所を実行ステージと切り分けられます。

ベースイメージが最新かどうかはdocker scout recommendationsでも確認できます。Docker Scoutの既定ポリシー(No outdated base images)は、ベースイメージが最新版か、pin留めしたダイジェストが正しいバージョンに対応しているかをチェックします。

docker scout recommendations my-app:optimized

docker scoutに対応していないDocker CLIでは、docker: 'scout' is not a docker command.と表示されます。Docker CLI 20.10.12がこの例です。その場合はベースイメージのタグを自分で見比べます。

イメージサイズの変化を数値で見たい場合はdocker imagesでBefore/Afterのサイズを比較します。Claude Codeに書き換え前後のDockerfileを両方渡し、差分の理由を説明させておくと、レビュー時に「なぜこの命令の順序を変えたか」を追跡しやすくなります。

ステップ5: 実行ユーザーとCMDの形式を見直す

マルチステージ化とキャッシュ設計が終わったら、残っている命令の書き方もベストプラクティスに合わせます。対象は主に3つで、実行ユーザー・CMDの記法・WORKDIRです。

特権が要らないサービスは、USERで非rootユーザーに切り替えて動かします。ユーザーとグループはDockerfileの中で作り、UIDとGIDが重要なら明示的に割り当てます。何も指定しないと、再ビルドしても決まった値にならないためです。sudoは、TTYやシグナル転送の挙動が予測しにくく問題を起こすので入れません。rootで起動して非rootで動かしたい場合の代替として、gosuが挙げられています。

CMDはCMD ["executable", "param1"]のexec形式で書くのが基本で、ほぼ常にこの形を使うよう説明されています。WORKDIRはRUN cd ... && do-somethingのような相対的なディレクトリ移動の代わりに使い、常に絶対パスを指定します。

Dockerfileの実行ユーザーとCMDの書き方を確認してください。特権が不要なのにrootのまま実行していれば、明示的なUID/GIDで非rootユーザーを作成してください。CMDがシェル形式ならexec形式に直し、WORKDIRが相対パスやRUN cdになっていれば絶対パス指定に直してください。

Claude Codeにdocker buildの実行を許可するには

allowルールに一致しないBashコマンドは、実行前に確認を求められます。docker buildやdocker scoutを確認なしで実行させたい場合、settings.jsonのpermissions.allowにルールを追加します。ルールは、最初の*の手前までを書いた通りに照合します。Bash(docker build *)はdocker build系のコマンドだけを許可し、docker rmiやdocker system pruneのような別のサブコマンドは許可しません。

{
  "permissions": {
    "allow": [
      "Bash(docker build *)",
      "Bash(docker scout *)"
    ],
    "deny": [
      "Bash(docker rmi *)",
      "Bash(docker system prune *)"
    ]
  }
}

設定ファイルを触らず、そのセッションだけ許可したいときは起動フラグを使います。v2.1.287のclaude --helpは--allowedToolsを「ツール名のカンマまたは空白区切りのリスト」と説明し、例として"Bash(git *) Edit"を挙げています。

claude --allowedTools "Bash(docker build *)" "Bash(docker scout *)"

許可するのは、サブコマンドを個別に列挙した形が安全です。広い書き方との違いは次のとおりです。

くらべる

dockerのルールの効き方

Bash(docker build *)

絞った許可

docker buildと、末尾に引数を足した形に一致します。docker build . && docker push .のような連結はサブコマンドごとに別々に照合され、docker pushの側は許可ルールが無ければ確認に回ります。

Bash(docker *)

広すぎる許可

docker配下のすべてのサブコマンドに一致し、イメージの削除やビルドキャッシュの掃除も確認なしで通ります。docker execはラッパーとして外されないコマンドなので、Bash(docker exec *)はコンテナ内で実行する任意のコマンドを通してしまいます。

denyルールを置くときに押さえる点

  • 判定の順序はdeny、ask、allowで、最初に一致したものが結果になる。Bash(docker *)をallowに置いても、Bash(docker rmi *)のdenyが先に効く
  • 広いdenyの中から、狭いallowで例外を作ることはできない
  • Bashのdenyは文字列の照合で、公式は「セキュリティ境界ではない」と説明している。例の表ではcurlのdenyに対し/usr/bin/curlやsh -c '...'の形が一致しないとされており、dockerでも絶対パスやsh -c経由はdenyで止められない形が出る
  • -Hや--contextで別のデーモンを指すdockerは、読み取り専用の形でも確認が出る

サンドボックスの内側ではdockerコマンドが動きません。dockerはサンドボックスと互換性がないと明記されており、必要なコマンドだけをexcludedCommandsで外に出す方法を案内されています。設定はsandbox.excludedCommandsでdockerなど非対応コマンドを対象外にするで扱っています。外に出したコマンドはフルアクセスで動くので、docker *のような広いパターンは避け、docker build *のように絞るほうが安全です。

Claude Code自体をコンテナの中で動かしたい場合は、この記事のDockerfile最適化とは別の観点が必要です。認証の永続化やヘッドレス実行の設定はClaude Code Docker実行ガイドにまとめています。

よくあるつまずき

  • Dockerデーモンが起動していない: Docker CLI 20.10.12で、デーモンが止まった状態のdocker imagesはCannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?と返します。Claude Codeにdocker buildを任せる前に、デーモンが動いていることを自分で確かめます
  • 最終ステージにビルドツールが残る: 最終ステージでCOPY --from=build /appのように広く取ると、ビルド専用の中間ファイルが実行イメージに入ります。最終ステージの内容をClaude Codeに一覧させて確認します
  • .dockerignoreを作らずコンテキストだけ肥大化する: マルチステージ化してもビルドコンテキストの送信自体は減りません。.dockerignoreは別途必要です
  • ADDとCOPYを混同する: ADDはリモートのURLやGitリポジトリの取得と、tarの自動展開に対応します。公式はステージ間のコピーなど通常の用途にCOPYを勧め、ADDはリモートの成果物をダウンロードするときに向くと説明しています。ビルドコンテキストのファイルをRUNの間だけ使うなら、RUN --mount=type=bindでコピーせずに済み、最終イメージにも残りません
  • ENVで一時変数を使ったつもりで値が残る: ENVで設定した値は、後続のRUN unsetでは消えず、そのレイヤーに残って取り出せます。一時的な値はRUN内でexportして、同じレイヤー内でunsetします
  • --no-cacheと--pullを混同する: --no-cacheはレイヤーキャッシュを無視するだけで、ベースイメージ自体は再取得しません。ベースイメージも最新にしたい場合は--pullを併用します

まとめ

Dockerfileの最適化は、縮めたいものを決め、診断で差分を出し、--pull --no-cacheのビルドで確かめる流れで進めます。docker buildをClaude Codeに任せるときは、許可をdocker build *のようにサブコマンド単位で絞り、docker execのような何でも通せるコマンドを広く許可しないことが分かれ目です。

コンテナ化したツールをClaude Codeから直接操作したい場合は、Docker MCP ToolkitでClaude Codeからコンテナを操作する設定も合わせて検討してください。

この記事を共有:XはてブLinkedIn