Dock Stay
ビルドキャッシュを再利用する - buildx cache の使い方・オプション・サンプル

ビルドキャッシュを再利用する - buildx cache

BuildKit のキャッシュを CI で再利用すると、ビルド時間を大幅に短縮できる。GitHub Actions の `gha` キャッシュやレジストリ型キャッシュを使い分ける。

概念図

buildx cache diagram

構文

bash
cache-from / cache-to: type=gha | type=registry | type=local | type=inline

実例

GitHub Actions 組み込みのキャッシュストレージを使う最も手軽な構成。

bash
- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ghcr.io/acme/app:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

レジストリ自体をキャッシュストアにする。複数 CI / ローカルからも共有可能。

bash
- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ghcr.io/acme/app:latest
    cache-from: type=registry,ref=ghcr.io/acme/app:buildcache
    cache-to: type=registry,ref=ghcr.io/acme/app:buildcache,mode=max

なぜキャッシュが要るのか

毎回クリーンな Runner 上で docker build すると、ベースイメージの pull、パッケージのインストール(npm ci / pip install / go mod download 等)、ビルド本体のすべてがやり直しになります。

実アプリでは 1 ビルド 5〜15 分が普通で、1 日に何十回と push される開発現場では支払いも待ち時間も馬鹿になりません。

BuildKit はレイヤー単位のキャッシュを持っており、「変わっていない部分」は再計算しません。

CI の Runner は毎回新品なので、そのキャッシュを外部ストレージ(GHA キャッシュ、レジストリ、S3 等)に保存し、次の run で pull して reuse する、という運用をします。

キャッシュバックエンドの種類

type 保存先 向く場面
gha GitHub Actions の組み込みキャッシュ GitHub で閉じるプロジェクト。導入が最も簡単
registry 任意の OCI レジストリ GHA 外にも共有したい/複数 CI で共有したい
s3 / gcs / azblob 各クラウドのオブジェクトストレージ 独自 Runner で細かく制御したい
local Runner のローカルディレクトリ self-hosted runner で永続ボリュームがある場合
inline イメージ自体のレイヤーに埋め込む 追加ストレージを用意できない/小規模なケース

mode=max を付けると中間レイヤーまで全部キャッシュし、mode=min(デフォルト)だと最終イメージに関わる分だけキャッシュします。

速度を取るなら max、容量を抑えるなら min です。

キャッシュが使われにくい Dockerfile の書き方

キャッシュはあくまで「命令と入力ファイルのハッシュ」で判定されます。

以下の 4 つは代表的なアンチパターンです。

  • COPY . . を依存解決の前に置く: ソース 1 文字の変更で npm install まで再実行される
  • RUN apt-get update && apt-get install ... を 2 命令に分ける: update だけ走り直して古いパッケージを掴む
  • --build-arg にタイムスタンプを毎回渡す: 以降の全レイヤーが無効になる
  • ADD <url> で動的なリソースを取りに行く: 外部が変わるたびに差分扱いになる

順番は「OS パッケージ → 依存ファイル (package.json 等) → 依存インストール → アプリソース」の 4 段階に整えると、ソース変更が多くても依存層まではキャッシュが残ります。

キャッシュサイズと有効期限

GHA キャッシュは 1 リポジトリあたり合計 10 GB の上限があり、古いものから自動的に evict されます。

巨大な node_modules や Python ホイールをそのまま入れると、すぐ枠が埋まります。

対処は 3 つ。

第一に .dockerignore で context を小さく保つ。

第二に、めったに変わらない重い依存は専用のベースイメージに切り出し、アプリ用キャッシュを軽く保つ。

第三に、registry 型キャッシュに移行して容量制約を回避する。

レジストリ型キャッシュは見かけ上レイヤーとしてプッシュされるため、古いタグは定期的に剪定(retention policy)しないと無限にストレージ代がかかります。

関連トピック