🐳

「GitHub Actions の Docker ビルドキャッシュが効かない」を解決した話

に公開
2026/02/18

はじめに

弊社のCI/CD環境はGitHub Actionsで構築しています。その中で8つのworkflowでdocker/build-push-actionでimage buildを実施しています。

(アーキテクチャ全体像は↓のブログから参照できます)
https://zenn.dev/my_vision/articles/my-vision-system

image buildの時間短縮のためキャッシュを保存する仕組みにしていましたが、不定期にキャッシュミスが発生し、本来約34秒で終わるbuildが約2分37秒かかる事態が高頻度で発生していました。

調査してみると、原因はbuild-push-actionの見落としやすい仕様によるものでした。別の方が同じ現象で悩まぬように原因と対策を共有します。

前提知識:build-push-action の type=gha キャッシュとは

docker/build-push-action は、GitHub Actions上で Docker imageをbuild・pushするための公式アクションです。type=gha を指定すると、BuildKit の GHA キャッシュバックエンドが有効になり、buildレイヤーをGitHub Actionsのキャッシュストレージに保存・復元し大きな時間短縮が期待できます。

基本的な設定は以下のとおりです。

- uses: docker/build-push-action@v6
  with:
    context: .
    file: ./Dockerfile
    push: true
    tags: my-image:latest
    cache-from: type=gha   # キャッシュからレイヤーを復元
    cache-to: type=gha,mode=max  # 全レイヤーをキャッシュに保存
  • cache-from: type=gha — build前に GitHub Actions Cache からレイヤーキャッシュを取得
  • cache-to: type=gha,mode=max — build後に全レイヤーをキャッシュに保存(mode=max で中間レイヤーも含む)

問題1:feature ブランチの初回 CI でキャッシュが効かない

あるとき、Pull Request作成直後の1回目のCIでbuildキャッシュが使用されていないことに気づきました。

同じPRに変更を加えた後に走る2回目のCIでは、キャッシュがヒットしbuild時間が短縮されていました。

タイミング build-push-action の所要時間 キャッシュ
PR 作成直後(初回) 約2分37秒 ミス
同じ PR への追加 push(2回目以降) 約34秒 ヒット

原因:GitHub Actions キャッシュのブランチスコープ制約

docker/build-push-action の type=gha キャッシュは、内部的に GitHub Actions のキャッシュ機構(actions/cache と同じ基盤)を使っています。このキャッシュには、ブランチスコープという制約があります。

ワークフロー実行では、子ブランチまたは兄弟ブランチ用に作成されたキャッシュを復元することはできません。

(出典:GitHub Docs - キャッシュへのアクセスに関する制限)

具体的には、あるワークフローがキャッシュを復元できるのは以下の 2 つのスコープに限られます。

  1. 自分自身のブランチで過去に保存されたキャッシュ
  2. ベースブランチ(= PR のマージ先)で保存されたキャッシュ
develop        ← ベースブランチ
  ├── feature/A ← このブランチのキャッシュは feature/B からは見えない
  └── feature/B ← develop のキャッシュは見える

ここで問題になるのは 「ベースブランチにキャッシュが存在するか?」 です。

対象のリポジトリではCIがpull_request イベントでのみ起動する設定でしたが、この設定ではベースブランチを起点にCIが実行されないためfeatureブランチで参照可能なキャッシュが作られません。

解決策:ベースブランチへの push トリガーを追加する

対策はシンプルです。ベースブランチへの push(= PR マージ)時にも CI を実行し様々なブランチから参照できるキャッシュを作成するようにしました。

# 変更後
on:
  pull_request:
    paths:
      - "{監視対象のパス}"
  push:
    branches:
      - develop  # ← 追加: ベースブランチへの push 時にも CI を実行
    paths:
      - "{監視対象のパス}"

キャッシュのライフサイクルは以下のように変わります。

1. feature/A の PR を develop にマージ
2. develop への push イベントで CI が起動
3. develop ブランチのキャッシュが作成・更新される
4. 次に feature/B の PR を作成
5. feature/B の初回 CI が develop のキャッシュを復元できる ← ここが改善

この修正により新規に作成されたPRのでも初回からキャッシュが参照できるようになりました。

問題2:異なるワークフロー間でキャッシュが上書きされる

問題1により解決したかと思いきや、依然としてキャッシュが定期的に参照できなくなる現象が発生していました。新規PRでimageのキャッシュが利用できる場合もあればそうではない場合もあるようなflakyな状態でした。

原因:scope 未指定によるキャッシュキーの衝突

build-push-action の type=gha キャッシュは、BuildKit の GHA キャッシュバックエンドを使用しています。このバックエンドは内部的に インデックスキー でキャッシュを管理しており、そのキーは以下の構造で生成されます。

index-{scope}-{version}-{hash(branch)}

scope はキャッシュの名前空間、version は内部バージョン、hash(branch)はブランチ名(refs/heads/develop など)の SHA256 ハッシュです。

補足: buildkitの実装でインデックスキーが宣言されている箇所

https://github.com/moby/buildkit/blob/59e64f1fc22101a01fb8d770a1963ad1b9024c86/cache/remotecache/gha/gha.go#L202
config.Scopeとscopeという似た変数名で別の内容を表しているのが少し紛らわしい

ここで問題になるのが scope パラメータです。scope を指定しない場合、デフォルト値は固定文字列 buildkit が使われます。

Scope is a key used to identify the cache object. By default, it is set to buildkit. If you build multiple images, each build will overwrite the cache of the previous, leaving only the final cache.

(出典:Docker Docs - GitHub Actions cache)

対象のリポジトリでは、複数のワークフローでDocker imageのbuildが実施されていましたが、すべて同じindex-buildkit-1-fd3dfe2c というキーを共有しており、異なるワークフロー同士でキャッシュを上書きしあう状況が起きていました。

解決策:ワークフローごとに scope を明示的に分離する

cache-from と cache-to に scope パラメータを追加し、build構成ごとにキャッシュを分離しました。

# 変更前
- uses: docker/build-push-action@v6
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

# 変更後
- uses: docker/build-push-action@v6
  with:
    cache-from: type=gha,scope=ci-rails
    cache-to: type=gha,mode=max,scope=ci-rails

GitHub CLI を使用して修正前後でキャッシュキーがどう変わったか見てみます。

gh cache list --json key,ref,createdAt --jq '.[] | select(.key | startswith("index-"))'

Before(scope 未指定)

キャッシュキー ブランチ 作成日時
index-buildkit-1-fd3dfe2c refs/heads/develop 2026-02-12 09:30
index-buildkit-1-fd3dfe2c refs/heads/develop 2026-02-12 09:32
index-buildkit-1-fd3dfe2c refs/heads/develop 2026-02-12 09:35

同じキーが短時間に何度も上書きされていることがわかります。

After(scope 指定)

キャッシュキー ブランチ 作成日時
index-ci-rails-1-fd3dfe2c refs/heads/develop 2026-02-13 17:55
index-deploy-rails-1-fd3dfe2c refs/heads/develop 2026-02-13 17:56

ci-rails と deploy-rails でキャッシュが独立し、上書きされないようになりました。

まとめ

docker/build-push-action の type=gha キャッシュを正しく運用するには、以下の 2 点を忘れないようにしてください。

# チェックポイント 原因 対策
1 ベースブランチでキャッシュが生成されているか? pull_request トリガーだけではベースブランチ自体のキャッシュが作られない ベースブランチへの push トリガーを追加する
2 複数のワークフローが同じ scope を共有していないか? scope 未指定だとデフォルト buildkit に集約され、異なる Dockerfile 間でキャッシュが上書きされる cache-from / cache-to に明示的な scope を指定する

これらを押さえることで正しくキャッシュが機能するようになり、build時間を安定して短縮できるようになります。

MyVision技術ブログ

Discussion