Docker 中級 #2 ビルドキャッシュ — BuildKit とレイヤー順序の最適化
基礎 #6 で押さえた「依存 → コード」の順序がキャッシュの最初のボタンだとすれば、この記事はその上に BuildKit 時代のビルドキャッシュ道具 を本格的に重ねます。
Docker 中級 シリーズでこの記事の位置:
- #1 マルチステージビルドとイメージスリミング
- #2 ビルドキャッシュ — レイヤー順序の最適化 ← この記事
- #3 docker-compose 基礎 — web + db
- #4 compose 深掘り — depends_on, healthcheck, profiles
- #5 環境変数と secrets 管理
- #6 ロギングとデバッグ
BuildKit がデフォルト #
Docker のビルドエンジンが二世代に分かれているという事実から。旧ビルダー (legacy builder) と BuildKit。機能差が大きいです。
| Legacy | BuildKit | |
|---|---|---|
| 並列ビルド | しない | する |
--mount=type=cache | ✗ | ✓ |
--mount=type=secret | ✗ | ✓ |
COPY --link | ✗ | ✓ |
| マルチプラットフォーム | 難しい | 自然 |
| キャッシュインポート / エクスポート | ✗ | ✓ |
最近の Docker バージョン (20.10+) では BuildKit がデフォルト なので別途設定なしに上の機能を使えます。強制的に有効にしたいなら:
DOCKER_BUILDKIT=1 docker build -t myapp .または buildx コマンドで:
docker buildx build -t myapp .buildx は BuildKit の上で動く拡張 CLI です。単一ビルドは docker build と同じで、マルチプラットフォーム / キャッシュエクスポートのような高度な機能は buildx の方が自然です。
この記事の全ての例は BuildKit / buildx が有効な状態を仮定します。
レイヤーキャッシュ — もう一度深く #
基礎で依存コピーをコードコピーより上に置けというルールを見ました。これを一段解きほぐすと:
このレイヤーのキャッシュキー = 前のレイヤーダイジェスト
+ 命令自体 (変数含む)
+ (COPY/ADD の場合) コピーするファイル内容のハッシュ三つのうち どれか一つでも変わればキャッシュ無効化 です。これを頭に入れておくとキャッシュがなぜよく壊れるか追跡しやすくなります。
見落としやすいキャッシュ無効化の箇所 #
FROM python:3.14-slim
ARG GIT_SHA # この位置の ARG は以降の全レイヤーキャッシュキーに含まれる
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
ARG BUILD_TIME # この位置の ARG は下のレイヤーからだけ影響
LABEL build-time=$BUILD_TIMEARG は宣言された位置 次のレイヤーから キャッシュキーに入ります。毎ビルドで値が変わる BUILD_TIME のような引数を上に置くと、全レイヤーが毎回再ビルドされます。よく変わる ARG は 最後に 置いてください。
ENV APP_VERSION=1.2.3 # この位置以降の全レイヤーがこの値をキャッシュキーに含むENV も同じです。よく変わる環境変数は上に置かないでください。
--mount=type=cache — ビルド間でキャッシュ共有
#
ここからが BuildKit の最大の贈り物。レイヤーキャッシュが壊れたときも パッケージマネージャのダウンロードキャッシュは生かせます。
npm #
# syntax=docker/dockerfile:1.7
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .--mount=type=cache,target=/root/.npm の意味:
- この RUN コマンドが実行されている間だけ
/root/.npmの位置に ビルド間で永続的なキャッシュディレクトリ をマウント - 同じホストで次のビルドを回すとき同じキャッシュをまたマウント
- キャッシュ自体は イメージに刻まれない (イメージサイズが増えない)
package-lock.json だけが変われば RUN 自体は再実行されますが、npm が見るダウンロードキャッシュ (~/.npm/_cacache) が生きているのでネットワークを経由せず素早く終わります。
最初の行の
# syntax=docker/dockerfile:1.7は BuildKit にこの Dockerfile の文法バージョンを明示します。新機能 (特に--mount) を安全に使うための慣用一行です。
pip #
# syntax=docker/dockerfile:1.7
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .pip も同じパターン。ただし — 以前 --no-cache-dir を付けてキャッシュを作らないようにしていた慣用 とは反対の流れです。mount cache を使うなら --no-cache-dir は外します。
apt #
# syntax=docker/dockerfile:1.7
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
apt-get update && \
apt-get install -y --no-install-recommends curlapt の二つのキャッシュ位置を一緒にマウントします。sharing=locked は同時にビルドされる別ステージが同じキャッシュに触れないようロックをかけます。apt のように同時アクセスに弱い道具に必要です。
apt キャッシュを mount cache にすると、基礎で見た
rm -rf /var/lib/apt/lists/*がもう必要ありません。キャッシュがイメージの外にあるからです。
Go module キャッシュ #
# syntax=docker/dockerfile:1.7
FROM golang:1.23 AS builder
WORKDIR /build
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod \
go mod download
COPY . .
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 go build -o myappGo は二つのキャッシュ (/go/pkg/mod — モジュールキャッシュ、/root/.cache/go-build — コンパイルキャッシュ) を両方マウントすると再ビルドがとても速くなります。
COPY --link — ビルド並列化
#
デフォルト COPY は前のレイヤーの上に新しいファイルを乗せる方式なので、前のレイヤーが完成した後で 実行されます。--link を付けると親レイヤーから独立して作れるので BuildKit が並列化する余地ができます。
FROM node:20-slim AS deps
WORKDIR /app
COPY --link package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM node:20-slim
WORKDIR /app
COPY --link --from=deps /app/node_modules ./node_modules
COPY --link . .体感差が一番大きいのはマルチステージ + 大きい --from=other コピーです。使わない理由がほぼないので新しく書くなら --link をデフォルトにする方が良いです。
--mount=type=secret — 秘密をイメージに刻まない
#
ビルド時にだけ必要な秘密 (例: プライベートパッケージレジストリのトークン) をイメージに固めずに使う道具。
# syntax=docker/dockerfile:1.7
FROM python:3.14-slim
WORKDIR /app
RUN --mount=type=secret,id=pypi,target=/root/.pypirc \
pip install --extra-index-url https://... my-private-pkgビルドコマンドでシークレットファイルを渡す:
docker build --secret id=pypi,src=$HOME/.pypirc -t myapp ./root/.pypirc はこの RUN の間だけ存在し、イメージのどのレイヤーにも残りません。 ARG や ENV で秘密を渡す古いパターンは docker history で露出するので、秘密は常にこの方法で。
RUN --mount=type=ssh — プライベート Git リポジトリ
#
go get / pip install git+ssh://... のようにビルド中 SSH でプライベートリポジトリに届かなければならないとき。
# syntax=docker/dockerfile:1.7
FROM python:3.14
RUN --mount=type=ssh \
pip install git+ssh://git@github.com/myorg/private-repo.gitdocker build --ssh default -t myapp .イメージに SSH 鍵が刻まれず、ホストの ssh-agent をビルド時間だけ少し借りる流れです。
外部キャッシュ — --cache-from / --cache-to
#
ここまでのキャッシュは ビルドを回す同じホスト の上でだけ動作します。CI マシンは毎回新しく立ち上がったり、複数のランナーが分散しているので役に立ちません。外部キャッシュ がその穴を埋めます。
レジストリキャッシュ #
ビルドキャッシュ自体をイメージのようにレジストリにアップロードする方式。
docker buildx build \
--cache-to=type=registry,ref=ghcr.io/curtis/myapp:cache,mode=max \
--cache-from=type=registry,ref=ghcr.io/curtis/myapp:cache \
-t ghcr.io/curtis/myapp:1.0 \
--push .--cache-to— このビルドのキャッシュをどこに出すか--cache-from— このビルド開始前にどこからキャッシュを受け取るかmode=max— 全レイヤーキャッシュ (デフォルトminは結果レイヤーだけ)
CI が毎回新しいマシンで動いても、キャッシュがレジストリに生きているので速いビルドを維持します。
GitHub Actions キャッシュ #
GHA の上で動くビルドは GHA のキャッシュバックエンドを直接使えます。
- uses: docker/build-push-action@v5
with:
push: true
tags: ghcr.io/curtis/myapp:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=maxGHA キャッシュは GitHub が無料で提供し (リポジトリあたり 10GB ほど)、関連ワークフローの中だけで素早くアクセスされます。外部レジストリへの push なしにキャッシュ共有ができるので最も手軽なオプションです。
inline キャッシュ #
イメージ自体にキャッシュメタデータを埋め込む最も軽い方式。
docker buildx build \
--cache-to=type=inline \
--cache-from=ghcr.io/curtis/myapp:latest \
-t ghcr.io/curtis/myapp:latest \
--push .別途のキャッシュストレージが要りません。--cache-from で取ってくるイメージ自体がキャッシュ情報を持っています。ただし mode=min だけ可能なのでマルチステージの全中間レイヤーまではキャッシュできません。単純な単一ステージ / 小さなプロジェクトに適しています。
キャッシュ方針一表 #
| 状況 | 推奨キャッシュ |
|---|---|
| ローカル開発 | mount cache (type=cache) |
| 単一 CI マシン、頻繁ビルド | mount cache + レイヤーキャッシュ |
| 分散 CI (GitHub Actions) | type=gha |
| 分散 CI (セルフホスト) | type=registry |
| 単純プロジェクト、外部ストレージを増やしたくない | type=inline |
--no-cache とキャッシュ強制更新
#
キャッシュが古い結果を握っているとき:
docker build --no-cache -t myapp .
docker build --no-cache-filter=builder -t myapp . # 特定ステージだけ--no-cache-filter は BuildKit の機能で、どのステージのキャッシュだけ無視するか を選べます。依存はキャッシュを使いつつビルドは最初から回したいときに有効です。
キャッシュが効かない典型的な理由 #
syntax=行がない — 古い文法で解釈されて mount が無視されるARG/ENVが上の方にある — 毎ビルドキーが変わるCOPY . .が上の方にある — コードを一行変えるだけで全レイヤー無効化- CI マシンが毎回新しく立ち上がる — 外部キャッシュ (
type=gha、type=registry) なしには生かせない - BuildKit が有効になっていない — Docker Desktop はデフォルトだが古い CI 環境では明示が必要
--no-cacheがどこかに刻まれている — Makefile / CI スクリプトを確認
まとめ #
この記事で掴んだ絵:
- BuildKit がデフォルトビルダー。
# syntax=docker/dockerfile:1.7一行で新機能を有効化 - レイヤーキャッシュキー = 前のレイヤー + 命令 + コピーファイル内容。よく変わる
ARG/ENV/COPYは下に --mount=type=cacheで npm/pip/apt/Go キャッシュをビルド間共有 — レイヤーキャッシュが壊れてもパッケージダウンロードは速いCOPY --linkで BuildKit の並列化を活用 — 新しく書くならデフォルトに--mount=type=secret/sshで秘密と SSH 鍵をイメージに刻まずビルド- 分散 CI では 外部キャッシュ —
type=gha、type=registry、type=inlineの中から環境に合わせて選択
次の記事 (#3 docker-compose 基礎 — web + db) では一つのコンテナからもう一歩進んで、複数コンテナを一つのファイルで定義 する道具に入ります。今シリーズの半分は Compose が占めます。