Dockerレイヤーキャッシュの仕組みとビルド最適化|ビルド時間を10倍速くする実践テクニック
「docker build が毎回5分もかかる…」「ソースを1行直しただけなのに npm install から全部やり直される」——Dockerfileを書き始めた人の9割がぶつかる壁が、ビルドの遅さです。
この問題の正体は レイヤーキャッシュの使い方。Dockerfileの書き順を少し変えるだけで、ビルド時間が 5分→30秒 になることも珍しくありません。この記事では、レイヤーキャッシュの内部メカニズムから、明日から使える最適化テクニックまで、図解つきで徹底解説します。
目次
- なぜビルド時間を気にするのか
- レイヤーとキャッシュの関係
- キャッシュヒット/ミスの判定ルール
- キャッシュが「壊れる」仕組みを図解
- ビルド最適化の5原則
- 実践① COPYの順序テクニック(npm/pipの最適化)
- 実践② RUNをまとめてレイヤーを減らす
- BuildKitのキャッシュ機能(Docker 23以降)
- キャッシュを意図的に制御する(–no-cache / ARG)
- よくある落とし穴
- まとめ
1. なぜビルド時間を気にするのか
ビルド時間の差は、単なる「待ち時間」ではなく、開発体験・CI/CDコスト・チーム生産性すべてに直結します。
| 影響領域 | 最適化前 | 最適化後 |
|---|---|---|
| ローカル開発サイクル | 1回のビルド 5分 → 集中が切れる | 差分ビルド 20秒 → 修正→確認が流れる |
| CI/CDパイプライン | PRごとに5分×コミット数 → 待機列 | 30秒程度 → マージが高速化 |
| GitHub Actions料金 | 無駄な分数課金が増える | 課金分数を1/10に削減も可能 |
後ほど示す例では、COPYの順番を2行入れ替えるだけで Node.js アプリのビルドが300秒→15秒になります。最適化の鍵は「レイヤーキャッシュ」を味方にすること。まずはその仕組みから理解しましょう。
2. レイヤーとキャッシュの関係
Dockerイメージは複数のレイヤーが積み重なった構造です。Dockerfileの各命令(FROM、RUN、COPY など)が、基本的に1レイヤーを生成します。
各レイヤーにはハッシュIDが振られており、Dockerはビルドのたびに「このレイヤー、前回と同じ?」を上から順にチェックします。同じであればキャッシュから再利用、違えばそのレイヤー以降を再ビルドする仕組みです。
レイヤーの内部実装は OverlayFS によるコピーオンライト。各レイヤーは読み取り専用で、差分だけが新しいレイヤーとして積まれます。この「読み取り専用+ハッシュID」の性質が、キャッシュを安全に再利用できる理由です(詳しくは 2-1「イメージとコンテナの関係」を参照)。
サンプル記事の docker_sim type="layers" で、レイヤーが共有される様子を復習できます。
3. キャッシュヒット/ミスの判定ルール
「どういう時にキャッシュが使われ、どういう時に壊れるのか」——この判定ルールは命令の種類によって違います。
| 命令 | キャッシュキーの計算方法 | 壊れる条件 |
|---|---|---|
FROM |
イメージ名+タグ+ダイジェスト | ベースイメージが更新されたとき(latest注意) |
RUN |
命令文字列の完全一致 | コマンドを1文字でも変えると壊れる |
COPY / ADD |
コピー対象ファイルのチェックサム | ファイル内容・権限・タイムスタンプが変わると壊れる |
ENV / ARG |
キー+値の文字列 | 値が変わると壊れる(以降全滅) |
WORKDIR / EXPOSE / CMD |
命令文字列 | 文字列変更で壊れる(ただし影響は小さい) |
キャッシュは 上から順に一致するかチェック され、一度壊れたら以降すべて再ビルドされます。つまり、変更頻度が高い命令を上に書くと、毎回全部を作り直すハメになります。
COPYのキャッシュは「ファイル内容」で決まる
COPYで注意すべきは、Dockerがコピー対象ファイルのチェックサムを比較する点です。同じファイルを同じ内容でCOPYすればキャッシュヒット、1バイトでも変わればミスになります。
# ソース変更なしで docker build
$ docker build -t myapp .
...
=> CACHED [1/5] FROM node:20 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package.json . 0.0s
=> CACHED [4/5] RUN npm install 0.0s
=> CACHED [5/5] COPY . . 0.0s
# 「CACHED」と表示されるのがキャッシュヒット
4. キャッシュが「壊れる」仕組みを図解
以下は、ソースコードを1行修正しただけで npm install が毎回走る悪い例です。
COPY . . が壊れると、その下の RUN npm install も巻き添えで再実行される原因は、COPY . .(ソース全体コピー)の直後に RUN npm install を置いたことです。app.js を修正すると COPY のキャッシュが壊れ、その後のすべてのレイヤーが再ビルドされます。
改善版:依存解決とソースコピーを分ける
npm install が壊れない限り 180秒がまるごとスキップされるポイントは、「変更頻度の低いもの(=依存関係)を先に、頻度の高いもの(=ソースコード)を後に」という順序です。
5. ビルド最適化の5原則
プロジェクトが変わっても使える、普遍的な原則を5つにまとめます。
| 原則 | 内容 | 効果 |
|---|---|---|
| ① 変更頻度順で並べる | ほぼ変わらないものを上、毎回変わるものを下へ | 最大の効果。迷ったらこれ |
| ② COPYを分割する | 依存定義(package.json等)を先に、ソースを後に | npm install / pip install のスキップ |
| ③ RUNをチェーンする | 関連コマンドは && で1行に |
レイヤー数削減+中間ファイル除去 |
| ④ .dockerignore を使う | node_modules・.gitなどを除外 | コンテキスト転送時間の削減(3-3で詳説) |
| ⑤ 意図的にキャッシュを壊す | --no-cache や ARG で制御 |
最新パッチを強制適用したいとき |
6. 実践① COPYの順序テクニック(npm/pipの最適化)
各言語のパッケージマネージャに応じた、キャッシュを活かすCOPY順序です。
Node.js(npm / yarn / pnpm)
FROM node:20-alpine
WORKDIR /app
# ① 依存定義だけ先にコピー
COPY package.json package-lock.json ./
# ② 依存解決(ここでキャッシュが効く)
RUN npm ci --omit=dev
# ③ ソースコードは最後にコピー
COPY . .
CMD ["node", "app.js"]
npm install ではなく npm ci を推奨。package-lock.json に忠実で、CIでの再現性が高く、高速です。
Python(pip)
FROM python:3.12-slim
WORKDIR /app
# ① requirements.txt だけ先にコピー
COPY requirements.txt .
# ② 依存インストール(キャッシュ対象)
RUN pip install --no-cache-dir -r requirements.txt
# ③ アプリコードは最後
COPY . .
CMD ["python", "app.py"]
pip install --no-cache-dir の意味これは「pipがダウンロードした.whlファイルをコンテナ内にキャッシュしない」という意味で、Dockerレイヤーキャッシュとは無関係です。最終イメージを軽くする目的で付けます。混同しないよう注意。
Go(go mod)
FROM golang:1.22-alpine
WORKDIR /app
# ① go.mod / go.sum だけ先に
COPY go.mod go.sum ./
# ② モジュールをダウンロード(キャッシュ対象)
RUN go mod download
# ③ ソースコピー&ビルド
COPY . .
RUN go build -o app
CMD ["./app"]
7. 実践② RUNをまとめてレイヤーを減らす
RUN命令を分けるとレイヤー数が増え、イメージサイズも膨らみます。関連コマンドは && で1つに繋ぐのが基本です。
apt-get の鉄則
# ❌ 悪い例:3レイヤー、中間キャッシュが残る
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# ✅ 良い例:1レイヤー、中間ファイルなし
RUN apt-get update && \
apt-get install -y --no-install-recommends curl && \
rm -rf /var/lib/apt/lists/*
別レイヤーにすると、
update の結果がキャッシュされ続け、古いパッケージ情報のまま install が実行されてしまう悪名高いバグの原因になります。必ず同じ RUN 内で実行しましょう。
RUNをまとめる利点と欠点
| 観点 | 分ける | まとめる |
|---|---|---|
| レイヤー数 | 多い | 少ない |
| イメージサイズ | 中間ファイルで膨張 | スリム |
| キャッシュの粒度 | 細かく制御可 | 変更時に全部やり直し |
| ベストプラクティス | デバッグ中のみ | 本番向け |
8. BuildKitのキャッシュ機能(Docker 23以降)
BuildKit は Docker の次世代ビルドエンジンで、Docker 23.0以降ではデフォルトで有効です。従来のキャッシュに加えて、強力な機能が使えます。
① 並列ビルド
依存関係のないステージを並列実行するため、マルチステージビルド(3-5で詳説)との相性が抜群です。
② RUN --mount=type=cache(永続キャッシュ)
通常のレイヤーキャッシュは「Dockerfileを変更しない限り有効」という性質ですが、BuildKitの --mount=type=cache はビルド間で永続化されるキャッシュディレクトリを用意できます。
# syntax=docker/dockerfile:1.4
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
# npm のキャッシュを /root/.npm に永続化
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
COPY . .
CMD ["node", "app.js"]
これで、package.json を変更して npm ci が走る時でも、ダウンロード済みパッケージは再利用されます。レイヤーキャッシュより更に速いビルドが実現できます。
BuildKit の高度なキャッシュ戦略(
--cache-from、--cache-to、レジストリキャッシュ等)は 第9章 9-2「イメージビルドの高速化」で詳しく扱います。
9. キャッシュを意図的に制御する(–no-cache / ARG)
キャッシュは便利ですが、意図的に壊したい場面もあります。
① docker build --no-cache:全部再実行
docker build --no-cache -t myapp .
セキュリティパッチを最新にしたい、ベースイメージの更新を取り込みたい時に使います。
② ARG を使った部分的リセット
FROM ubuntu:22.04
# ここより上はキャッシュされる
ARG CACHEBUST=1
# この行の値が変わると、以降のレイヤーは全て再ビルドされる
RUN apt-get update && apt-get install -y curl
COPY . .
# ビルド時に値を変えればキャッシュをリセットできる
docker build --build-arg CACHEBUST=$(date +%s) -t myapp .
③ --pull:ベースイメージだけ最新化
docker build --pull -t myapp .
FROM のイメージをレジストリから最新版で取り直します。latest タグを使っている場合に特に有効です。
10. よくある落とし穴
| 落とし穴 | 原因 | 対策 |
|---|---|---|
COPY . . を最初に置いてしまう |
全変更でキャッシュ崩壊 | 依存定義を先にCOPY、ソースは最後 |
apt-get update と install の分離 |
古いパッケージインデックスがキャッシュされる | 必ず同じRUNで && 連結 |
| タイムスタンプ入りのファイルをCOPY | 毎回チェックサムが変わる | 生成ファイルを .dockerignore で除外 |
FROM node:latest を使っている |
タグ更新で突然キャッシュが無効化 | バージョン固定(node:20.11-alpine 等) |
| 環境変数を毎回違う値で渡す | ARG や ENV で以降全滅 |
変更頻度の高いARGは下側に配置 |
| ビルドコンテキストが巨大 | 転送だけで数秒〜数十秒 | 3-3 .dockerignore で除外 |
ビルド出力に
CACHED と表示されていてもビルドが遅い場合、原因はビルドコンテキスト転送の可能性があります。docker build は最初にカレントディレクトリ全体を daemon に送信するため、node_modules や .git が含まれると数十秒かかることも。.dockerignore(3-3)で対処しましょう。
11. まとめ
レイヤーキャッシュの最適化は、Dockerfile の書き順をほんの少し工夫するだけで、ビルド時間を劇的に短縮できる最もコスパの高いテクニックです。
| テクニック | 何を最適化するか | 効果の大きさ |
|---|---|---|
| ① 変更頻度順にレイヤーを並べる | 再ビルド範囲 | ★★★(最大) |
| ② COPY分割(依存 → ソース) | npm/pip install のスキップ | ★★★ |
③ RUN を && でまとめる |
レイヤー数・イメージサイズ | ★★ |
| ④ .dockerignore で除外 | コンテキスト転送時間 | ★★ |
⑤ BuildKit --mount=type=cache |
ビルド間のパッケージ再利用 | ★★ |
⑥ --no-cache / ARG CACHEBUST |
意図的なキャッシュ無効化 | ★ |
3-5「マルチステージビルド」では、ビルド用と実行用のステージを分けて、本番イメージを数百MB→数十MBまで削減するテクニックを解説します。レイヤーキャッシュと組み合わせると、ビルド時間もイメージサイズも両立できる、プロ仕様のDockerfileが書けるようになります。
参考リンク
- Docker Build Cache(公式ドキュメント) — レイヤーキャッシュの仕組みとベストプラクティスの一次情報源。
- BuildKit 公式ドキュメント —
--mount=type=cacheなどBuildKit固有機能の詳細。 - Dockerfile best practices — 公式が推奨するDockerfileの書き方集。キャッシュ戦略も含む。



コメント