目次

Docker ビルドキャッシュ入門:キャッシュの仕組みとイメージの軽量化

同じ Dockerfile でも、書き方によってビルド時間が 1 秒未満から数分に変わることがあり、イメージのサイズにも大きな差が出ます。この節では、実験を通じてキャッシュのルールとイメージを軽量化する方法を説明します。

キャッシュの仕組み

docker build は各コマンドを順番に処理します。各行について、Docker は次の点を確認します。このコマンドと、それより前のすべてのレイヤーは、前回のビルド時と同じですか?

  • 同じ場合:キャッシュをそのまま使い、出力に CACHED と表示されます。
  • 異なる場合:その行を再実行し、それ以降の行もすべてキャッシュが使われなくなります。
コマンド判定基準
RUNコマンド文字列が同じか(ネットワーク上のパッケージが更新されたかは確認しません)
COPY / ADDコピーするファイルの内容が同じか
その他コマンドと引数が同じか
コードを変更して再ビルド:パッケージを先にインストールする順序では npm ci がキャッシュされ、ソースコードを先にコピーする順序では npm ci が再実行される
レイヤーが変わると、その下のレイヤーはすべて再ビルドされます。変更頻度の低い手順を先に置きましょう

実験:手順の順序による影響

Express を使う小さなプロジェクト(package.json に express パッケージを記述)で、2 種類の Dockerfile を比較します。

悪い順序(Dockerfile.bad):

FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

正しい順序(Dockerfile.good):

FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]

悪い順序:コードを1行変更する

まず一度ビルドし、次に server.js の1文字だけを変更してから、もう一度ビルドします。

$ docker build -f Dockerfile.bad -t cache-bad .

[+] Building 6.9s (9/9) FINISHED                                                     docker:default
 => [internal] load build definition from Dockerfile.bad                                       0.0s
 => => transferring dockerfile: 125B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.2s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/4] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/4] WORKDIR /app                                                                  0.0s
 => [3/4] COPY . .                                                                             0.1s
 => [4/4] RUN npm install                                                                      5.4s
 => exporting to image                                                                         1.0s
 => => exporting layers                                                                        0.5s
 => => exporting manifest sha256:d27475f082a53018e303541bc1c0691ac25526e87761300e2aa8ec76d73e  0.0s
 => => exporting config sha256:19f2024df4329a5f2af23e50d8c18826f073d425e18133e9571c393999d913  0.0s
 => => exporting attestation manifest sha256:58b4730e9566f2ec7e8222cd41c14ea971d87272b6b0c190  0.0s
 => => exporting manifest list sha256:58b8fc79124f2fe21270801d5e40f09839c06ba5203304423125270  0.0s
 => => naming to docker.io/library/cache-bad:latest                                            0.0s
 => => unpacking to docker.io/library/cache-bad:latest                                         0.4s

[3/4] COPY . . は server.js の変更によって再実行され、その後の [4/4] RUN npm install も続けて再実行されました。すべてのパッケージを再ダウンロードしてインストールするのに、5.4 秒かかりました。

正しい順序:コードを1行変更する

初回のビルド(npm install の実行が必要です):

$ docker build -f Dockerfile.good -t cache-good .

[+] Building 5.7s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.1s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 95B                                                               0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => [3/5] COPY package.json ./                                                                 0.0s
 => [4/5] RUN npm install                                                                      4.4s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         0.9s
 => => exporting layers                                                                        0.5s
 => => exporting manifest sha256:4b36d92a774b02cc1a188193e178a445b90f330b19f2dcc741d631e246e8  0.0s
 => => exporting config sha256:4a597aa667d9ba52eab36c4483f7507bbcae158b1b22af5ca6e4ac5b7020fa  0.0s
 => => exporting attestation manifest sha256:efea2ef65e2672718391b6419b9f5f2be50396163032a29e  0.0s
 => => exporting manifest list sha256:236918100fd2092d0388da5c03b32dfa3abaa2e62304abb3e3dd335  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.4s

server.js を変更してから、もう一度ビルドします:

$ docker build -f Dockerfile.good -t cache-good .

[+] Building 0.4s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.1s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              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
 => [5/5] COPY . .                                                                             0.0s
 => exporting to image                                                                         0.1s
 => => exporting layers                                                                        0.0s
 => => exporting manifest sha256:6cd8c2671a295aab85724aefe72f8dc749f4d332224f5d534fff5e4e57fa  0.0s
 => => exporting config sha256:7df3d574ae91f98ea3634c4dcede63e64ce8018c902643c2db08c2b4dee4c3  0.0s
 => => exporting attestation manifest sha256:fc9ff7e57f4187c04b74446f5dd96e46ef9c7f0ea88372a6  0.0s
 => => exporting manifest list sha256:79e8a6d76ff2382f02bcadc183a44670959d1d0a30467a27b9d619e  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.0s

ビルド全体にかかった時間はわずか 0.4 秒です。COPY package.json と RUN npm install はどちらも CACHED で、最後の COPY . . だけが再実行されました。このプロジェクトのパッケージは1つだけですが、実際のプロジェクトでは数百のパッケージを扱うこともあり、npm install に数分かかる場合もあります。その場合、差はさらに大きくなります。

同じ考え方は、どの言語にも当てはまります:

言語先にコピー次にインストール
Node.jspackage.json、package-lock.jsonnpm ci
Pythonrequirements.txt または pyproject.tomlpip install
Gogo.mod、go.sumgo mod download
Java (Maven)pom.xmlmvn dependency:go-offline
PHPcomposer.json、composer.lockcomposer install

実験:同じレイヤー内で一時ファイルを削除する

各レイヤーは積み重なっています。後続の RUN でファイルを削除しても、前のレイヤーのサイズは減りません。curl をインストールする 2 通りの書き方を比べてみましょう。

Dockerfile.separate:

FROM debian:trixie-slim
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
RUN rm -rf /var/lib/apt/lists/*

Dockerfile.same:

FROM debian:trixie-slim
RUN apt-get update \
    && apt-get install -y --no-install-recommends curl \
    && rm -rf /var/lib/apt/lists/*

ビルド後にサイズを比較します。

$ docker images --filter reference=apt-separate --filter reference=apt-same --filter reference=debian:trixie-slim
IMAGE                 ID             DISK USAGE   CONTENT SIZE   EXTRA
apt-same:latest       5357e9775c1e        136MB         34.9MB
apt-separate:latest   ad6c0a37d9ea        174MB         51.7MB

内容は同じ(どちらも curl をインストールし、パッケージリストを削除しています)ですが、apt-separate のほうが 38 MB 大きくなっています。原因を docker history で確認しましょう。

$ docker history apt-separate
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
ad6c0a37d9ea   11 seconds ago   RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #…   20.5kB    buildkit.dockerfile.v0
<missing>      11 seconds ago   RUN /bin/sh -c apt-get install -y --no-insta…   13.4MB    buildkit.dockerfile.v0
<missing>      16 seconds ago   RUN /bin/sh -c apt-get update # buildkit        21.6MB    buildkit.dockerfile.v0
<missing>      8 days ago       # debian.sh --arch 'amd64' out/ 'trixie' '@1…   87.7MB    debuerreotype 0.17

$ docker history apt-same
IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
5357e9775c1e   1 second ago   RUN /bin/sh -c apt-get update     && apt-get…   13.4MB    buildkit.dockerfile.v0
<missing>      8 days ago     # debian.sh --arch 'amd64' out/ 'trixie' '@1…   87.7MB    debuerreotype 0.17

apt-separate では apt-get update のレイヤーが 21.6 MB あります(ダウンロードしたパッケージリストです)。後続の rm -rf は新しいレイヤーでそれを「削除済みとしてマーク」するだけなので、元の 21.6 MB はイメージに残ります。同じ RUN にまとめれば、このレイヤーが終わる前にパッケージリストが削除されるため、どのレイヤーにも保存されません。

一時ファイルを残さないためによく使われる書き方は次のとおりです。

RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci && npm cache clean --force
RUN apk add --no-cache curl

.dockerignore

ビルドの開始時に、Docker はビルドコンテキスト全体(通常は現在のフォルダー)をビルドエンジンに送ります。node_modules を除外しない場合、Dockerfile 入門 の例では 30 MB を送信しますが、除外すれば 159 B だけです。.dockerignore の例:

.git
node_modules
dist
*.log
.env
.env.*
Dockerfile*
.dockerignore

ビルドが速くなるだけでなく、これらのファイルの変更によって COPY . . のキャッシュが無効になるのを防ぎ、.env や秘密鍵がイメージに含まれるのも防げます。

小さなベースイメージを選ぶ

イメージ のセクションで実測した Python 3.14 イメージのサイズ:

ベースイメージディスク使用量
python:3.141.63 GB
python:3.14-slim180 MB
python:3.14-alpine82.6 MB

マルチステージビルド

コンパイラーや開発用パッケージが必要なのはビルド時だけです。最終イメージには含めないようにしましょう。マルチステージビルド は、サイズを縮小する最も効果的な方法です。

キャッシュマウント:パッケージのダウンロードを高速化する

BuildKit はキャッシュマウントをサポートしており、パッケージマネージャーのダウンロードキャッシュを複数回のビルドにわたって保持できます。RUN npm ci のレイヤーのキャッシュが無効になっても、すべてのパッケージをネットワークから再ダウンロードする必要はありません。

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

キャッシュマウントの内容はイメージに含まれないため、イメージサイズは増えません。

キャッシュを使わずにビルドする

パッケージの最新バージョンを取得したい場合や、キャッシュに問題があると思われる場合は、次のようにします。

$ docker build --no-cache -f Dockerfile.good -t cache-good .

[+] Building 6.6s (10/10) FINISHED                                                   docker:default
 => [internal] load build definition from Dockerfile.good                                      0.0s
 => => transferring dockerfile: 147B                                                           0.0s
 => [internal] load metadata for docker.io/library/node:24-alpine                              0.3s
 => [internal] load .dockerignore                                                              0.0s
 => => transferring context: 65B                                                               0.0s
 => [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401  0.0s
 => [internal] load build context                                                              0.0s
 => => transferring context: 280B                                                              0.0s
 => CACHED [2/5] WORKDIR /app                                                                  0.0s
 => [3/5] COPY package.json ./                                                                 0.0s
 => [4/5] RUN npm install                                                                      5.1s
 => [5/5] COPY . .                                                                             0.1s
 => exporting to image                                                                         1.0s
 => => exporting layers                                                                        0.4s
 => => exporting manifest sha256:30688d5c22069d97e9db0bd32d5939e8e99fda0c6a0c4af1f32fb5baacc9  0.0s
 => => exporting config sha256:e8e75108925e8f145e7367b8e36350a8175c3776154f560fb47a9fb287fe21  0.0s
 => => exporting attestation manifest sha256:adbbb2fc6683c9a35c67bb9237cf3afcfd24c39dd9aa4b55  0.0s
 => => exporting manifest list sha256:6b92b5704d4fa7a3f11cfda5ffde00e6516bdbf358e23dbece712a2  0.0s
 => => naming to docker.io/library/cache-good:latest                                           0.0s
 => => unpacking to docker.io/library/cache-good:latest                                        0.5s

--no-cache は各ステップを再実行します(CACHED は表示されません)。--pull も追加すると、ベースイメージに新しいバージョンがあるかどうかも再確認します。

ビルドキャッシュの確認と削除

ビルドキャッシュは次第に増えていきます。

$ docker builder du
ID                           RECLAIMABLE   SIZE       LAST ACCESSED
mr6xd3ylof28pr8obxiet3fo8    true          403B       3 minutes ago
l34l5occgcgh8bscivg2iusrn    true          626B       3 minutes ago
... (中略)
Shared:		488.2MB
Private:	118.5MB
Reclaimable:	606.7MB
Total:		606.7MB
$ docker builder prune -f
ID						RECLAIMABLE	SIZE		LAST ACCESSED
xqb9jtg0kmq0mdzb0vrmod68j               	true 		20.67kB   	3 minutes ago
u1pbndrekyh1q91qbb4n9e3fy*              	true 	8.192kB   	5 minutes ago
... (中略)
Total:	118.5MB

docker builder prune はビルドキャッシュだけを削除し、イメージやコンテナには影響しません。Docker のデータをすべて削除する方法は、ディスク容量のクリーンアップを参照してください。

まとめ

  • あるレイヤーが変更されると、それ以降のすべてのレイヤーを再ビルドする必要があります。
  • 変更頻度の低い手順(パッケージのインストール)を前に、変更頻度の高い手順(コードのコピー)を後に配置します。
  • 一つの RUN 内でダウンロード、インストール、一時ファイルの削除を行うと、一時ファイルがイメージ内に残りません。
  • RUN --mount=type=cache を使うと、パッケージのダウンロードデータを複数回のビルドで共有できます。

練習問題

練習 1:次の二つの Dockerfile の断片では、どちらのイメージが小さくなりますか? その理由も説明してください。

# A
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# B
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
解答

B のほうが小さくなります。A では rm が新しいレイヤーで実行されるため、パッケージリストは前のレイヤーに残ります。B では同じレイヤー内でダウンロードと削除を行うため、リストは保存されません。

練習 2:同じプロジェクトを docker build --no-cache とオプションなしでそれぞれ一度ビルドし、[+] Building に表示される合計時間を比較してください。

ヒント

オプションなしの場合、ほとんどの手順に CACHED と表示され、通常は 1 秒未満で完了します。--no-cache を指定すると、各手順が再実行されるため、所要時間はパッケージのインストール速度によって変わります。

よくある質問

RUN apt-get update がキャッシュされ、古いバージョンのパッケージがインストールされる場合はどうすればよいですか?

キャッシュはコマンド文字列だけを比較し、ネットワーク上のパッケージが更新されたかは確認しません。apt-get update と apt-get install を同じ RUN に記述し、必要に応じて docker build --no-cache で強制的に再ビルドします。

ビルドキャッシュがディスク容量を多く使っている場合は?

docker builder du で確認し、docker builder prune で削除します。実測については[ディスク容量を解放する](/ja/docker/cleanup/)を参照してください。

CI で毎回最初からビルドするのに時間がかかる場合は?

GitHub Actions の cache-from: type=gha などの外部キャッシュを使います。[GitHub Actions で自動ビルドする](/ja/docker/ci-cd/)を参照してください。

更新日 2026年10月14日