---
title: "GitHub ActionsでDockerイメージをビルドする｜push先とキャッシュ・マルチアーキの設計"
url: "https://www.issoh.co.jp/tech/details/16943/"
published: 2026-08-25
updated: 2026-09-27
categories: ["GitHub"]
publisher: "株式会社一創"
---

# GitHub ActionsでDockerイメージをビルドする｜push先とキャッシュ・マルチアーキの設計

GitHub Actions上のイメージビルドは、YAMLを写して`push: true`を付ければひとまず動きます。困るのはその先で、権限が足りずにpushが弾かれる、毎回フルビルドになって10分待たされる、`latest`が誰の手で動いたか追えない、といった詰まり方をします。この記事では`docker/build-push-action`を軸に、GHCRとECRで異なる権限の型、タグの生成規則、レイヤーキャッシュの置き場所、マルチアーキの作り分けまでを扱いました。ワークフローの基本構造は[GitHub Actionsとは？できること・使い方とCI/CD自動化の解説記事](https://www.issoh.co.jp/column/details/3005/)、ジョブとテストの組み方は[GitHub Actionsでビルド・自動テストを設定する方法](https://www.issoh.co.jp/tech/details/2497/)を先に押さえてください。

## まとめ：イメージビルドをCIへ載せる前に決める4点

第一に、push先を決めるとpermissionsの型が決まります。GHCR（ghcr.io）なら`GITHUB_TOKEN`に`packages: write`を与えるだけで済み、追加のシークレットは要りません。Amazon ECRなら`id-token: write`を付けてOIDCでIAMロールを引き受ける形になり、長期のアクセスキーをリポジトリへ置かずに済みます。どちらを選ぶかで、ワークフローの冒頭に書く`permissions`ブロックが変わります。

第二に、タグは手書きせず`docker/metadata-action`で生成してください。ブランチ名、プルリクエスト番号、セマンティックバージョン、コミットSHAのどれを出すかを宣言で書けます。運用で効くのは`flavor`の`latest`制御で、既定の`auto`はsemverやタグ参照のときに`latest`を付けます。デプロイ対象をSHAタグに固定しておくと、同じタグが別の中身を指す事故を避けられました。

第三に、レイヤーキャッシュは`type=gha`と`type=registry`のどちらに置くかを先に選びます。`type=gha`はGitHubのキャッシュサービスを使うため設定が短くて済む反面、2025年4月15日以降はAPI v2のみの対応となり、Docker Buildx v0.21.0以上・BuildKit v0.20.0以上が要件になりました。リポジトリを跨いで共有したい場合は`type=registry`が向きます。

第四に、マルチアーキは作り方によって所要時間に桁違いの差が生じる点に注意が必要です。QEMUで1ジョブに載せる形は設定が2行で済みますが、arm64側がエミュレーションになるため大きく遅くなります。arm64のランナーを別に立てて`digest`を結合する形なら実機ビルドのままで並列化できるので、ビルド時間が問題になった時点で切り替える価値があります。

## docker/build-push-actionでビルドしてpushする最小構成

まず土台となる4ステップの並びを固めます。ここが崩れていると、キャッシュもマルチアーキも後から載せられません。

### setup-buildx-actionとlogin-actionを並べる基本の順序

順序は`checkout`、`setup-buildx-action`、`login-action`、`build-push-action`の4つです。`setup-buildx-action`はBuildKitを使うビルダーを用意するステップで、これを省くとランナー既定のビルダーになり、`cache-to`や`platforms`の指定が効きません。ログインをビルドより前に置くのは、`push: true`のときにビルド完了と同時にレイヤーの送信が始まるためです。

```
name: build-image
on:
  push:
    branches: [main]
permissions:
  contents: read
  packages: write
jobs:
  build:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v7
      - uses: docker/setup-buildx-action@v4
      - uses: docker/login-action@v4
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - uses: docker/build-push-action@v7
        with:
          context: .
          file: ./Dockerfile
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
```

`context`を明示するのは、既定のGitコンテキストとローカルパスで挙動が変わるからです。`.`を渡すとチェックアウト済みのワークスペースがそのまま送られ、事前ステップで生成したファイルもビルドへ含められます。ビルド対象そのものの書き方は[Dockerfileとは｜書き方・主要命令・ベストプラクティス](https://www.issoh.co.jp/tech/details/3315/)で確認してください。なお、この記事のactionのメジャー版は2026年8月時点の公式ドキュメントで確認したもので、`build-push-action`はv7系、`login-action`と`setup-buildx-action`はv4系にあたります。

### GHCRへpushするときのpermissionsとGITHUB\_TOKENの範囲

GHCRへ送る場合、必要なのは`packages: write`の1行です。`permissions`をワークフローに1つも書かないと、リポジトリ設定側の既定値が使われます。既定が読み取りのみに絞られている組織では、ここを書かないとpushが403で落ちる形になりました。逆に`write-all`のような広い指定を残すと、外部actionが同じトークンでリポジトリを書き換えられる状態になるため、ジョブ単位で最小の組み合わせへ絞ってください。トークンの置き場所とOIDCへの寄せ方は[GitHub ActionsのSecrets管理｜置き場所の選定と漏えい経路](https://www.issoh.co.jp/tech/details/16939/)で整理しています。

もう1つ引っかかりやすいのが、パッケージとリポジトリの紐付けです。イメージ名を`ghcr.io/${{ github.repository }}`の形にしておくと所有者とリポジトリ名が一致し、パッケージが自動でそのリポジトリへ紐付きます。別名を付けると紐付けが外れ、`GITHUB_TOKEN`では書き込めない状態になります。

### ECRへpushするときのOIDC認証とamazon-ecr-loginの並び

ECRの場合はAWS側の認証を先に置く構成です。`aws-actions/configure-aws-credentials`でIAMロールを引き受け、`aws-actions/amazon-ecr-login`がdockerのログイン処理まで済ませたうえで、レジストリのホスト名を出力として返します。ジョブに`id-token: write`を付け忘れるとOIDCのトークンが発行されず、ロールの引き受けが失敗します。

```
permissions:
  contents: read
  id-token: write
steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/gha-ecr-push
      aws-region: ap-northeast-1
  - id: ecr
    uses: aws-actions/amazon-ecr-login@v2
  - uses: docker/build-push-action@v7
    with:
      context: .
      push: true
      tags: ${{ steps.ecr.outputs.registry }}/app:${{ github.sha }}
```

IAMロール側の信頼ポリシーでは、リポジトリとブランチを条件に絞ってください。`repo:org/name:*`のようにワイルドカードで開けると、そのリポジトリの任意のブランチやプルリクエストからpushできてしまいます。Google Cloud側へ送る場合の考え方も同じで、レジストリの構造と権限の粒度は[Artifact Registryとは？仕組み・料金とContainer Registry移行](https://www.issoh.co.jp/tech/details/15559/)に整理しました。ECRリポジトリやIAMの設計をまとめて引き取ってほしい場合は[インフラ構築（AWS・Google Cloud・Azure）](https://www.issoh.co.jp/service/system/aws/)でご相談ください。

## metadata-actionでタグを組み立ててpull側の指定を安定させる

タグを`latest`1本で回すと、どのコミットが本番にいるのか誰も答えられなくなります。生成規則を宣言で持つと、この曖昧さが消えます。

### type=refとtype=semverとtype=shaで生成されるタグの違い

`docker/metadata-action`のタグ指定は8種類あり、実務で使うのは主に4つです。`type=ref,event=branch`はブランチ名、`type=ref,event=pr`はプルリクエスト番号、`type=semver`はGitのバージョンタグ、`type=sha`はコミットSHAを出します。前3つは同じ文字列が別の中身を指し得る動くタグで、SHAだけが1つのビルドに固定されるタグです。

```
- id: meta
  uses: docker/metadata-action@v6
  with:
    images: ghcr.io/${{ github.repository }}
    tags: |
      type=ref,event=branch
      type=ref,event=pr
      type=semver,pattern={{version}}
      type=sha,format=long
    flavor: |
      latest=false
- uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    labels: ${{ steps.meta.outputs.labels }}
```

`labels`も一緒に渡しておくと、OCIの標準ラベルとしてソースリポジトリ、リビジョン、生成時刻がイメージへ埋め込まれます。障害時に「動いているイメージがどのコミットか」を`docker inspect`だけで辿れるので、書かない理由はほとんどありません。

### latestタグを誰がいつ動かすかをflavorで固定する書き方

`flavor`の`latest`は既定が`auto`で、semverやタグ参照が来たときに`latest`を付けます。この既定のままだと、リリースタグを打った瞬間に`latest`が動きます。段階的に配布したい場合は`latest=false`で自動付与を止め、必要なときだけ`type=raw,value=latest,enable={{is_default_branch}}`のようにデフォルトブランチ限定で付ける書き方に寄せてください。誰が動かすかを1箇所に閉じ込めるのが目的で、条件が複数のジョブへ散ると追跡できなくなります。

デプロイ側の指定は動くタグではなくSHAタグかダイジェスト参照にします。`latest`を参照したままだと、ロールバックのつもりで再デプロイしても同じ新しいイメージが降ってくる、という事故が起きます。

### pull側から見たパッケージの可視性と権限をどこで決めるかの整理

pushが通ってもpullで詰まる場合、原因はほぼパッケージの可視性です。GHCRのパッケージは既定で非公開になるため、別リポジトリのワークフローや本番のKubernetesノードから引くには、パッケージ設定でリポジトリへのアクセスを許可するか、公開へ切り替えるか、専用のトークンを配る必要があります。ここはワークフローのYAMLではなくパッケージ側の設定画面で決まる項目なので、CIのログをいくら読んでも答えが出ません。保存容量と課金の条件は[GitHub Container Registry（ghcr.io）の料金とは｜無料枠・課金条件](https://www.issoh.co.jp/tech/details/7315/)で確認できます。

## レイヤーキャッシュをtype=ghaとtype=registryのどちらで持つか

キャッシュが効いていないビルドは、依存の取得とコンパイルを毎回やり直します。どこに置くかで制約が変わるため、方式を選ぶところから始めます。

### type=ghaは2025年4月のAPI移行でbuildxの版が要件になった

GitHubのキャッシュサービスは2025年4月15日以降、v2のAPIのみを受け付ける形へ移りました。これに伴い`type=gha`を使うには、Docker Buildx v0.21.0以上とBuildKit v0.20.0以上が必要になっています。古いバージョンを固定している既存のワークフローでは、ここで静かにキャッシュが効かなくなる、あるいはエラーで止まるという症状が出ました。`setup-buildx-action`を最新のメジャーに寄せておけば、通常はこの要件を満たします。

```
- uses: docker/build-push-action@v7
  with:
    context: .
    push: true
    tags: ${{ steps.meta.outputs.tags }}
    cache-from: type=gha,scope=${{ github.ref_name }}
    cache-to: type=gha,mode=max,scope=${{ github.ref_name }}
```

GitHubのキャッシュ側にも制約があります。リポジトリあたりの容量は既定で10GB、7日間アクセスの無いエントリは削除され、復元できるのは現在のブランチとデフォルトブランチのキャッシュです。プルリクエストではベースブランチのものも参照できます。イメージのレイヤーは容易にGB級へ育つため、複数のサービスを1リポジトリで抱えていると10GBの枠を互いに押し出し合う状態になります。

### mode=maxとscopeの分け方でヒット率と保存量が変わる

`mode`の既定は`min`で、最終イメージに含まれるレイヤーだけを保存します。マルチステージビルドのビルド段を再利用したいなら`mode=max`にして中間レイヤーまで残す必要がありますが、その分だけ保存量が増えます。`scope`は既定が`buildkit`で、同じワークフロー内で複数のイメージを作るとキャッシュを取り合うため、イメージ名やブランチ名で分けてください。

### type=registryを選ぶ条件とレイアウトの指定で気をつける点

キャッシュをレジストリへ置く形なら、10GBの枠にも7日の期限にも縛られません。別リポジトリのワークフローやローカルの開発機からも同じキャッシュを引けるため、共通のベースイメージを何本ものサービスで共有している構成では`type=registry`のほうが素直です。代わりにレジストリの保存容量として課金され、キャッシュ用のタグが本体のタグ一覧に混ざるので、名前を`:buildcache`のように分けておきます。

```
    cache-from: type=registry,ref=ghcr.io/acme/app:buildcache
    cache-to: type=registry,ref=ghcr.io/acme/app:buildcache,mode=max
```

| 方式            | 保存先           | 向く場面          |
| ------------- | ------------- | ------------- |
| type=gha      | Actionsのキャッシュ | 1リポジトリで完結する時  |
| type=registry | イメージレジストリ     | 複数リポジトリで共有する時 |
| type=inline   | イメージ本体へ同梱     | mode=minで足りる時 |
| type=local    | ランナーのディスク     | 世代を自前で管理する時   |

`type=local`は古いエントリが自動で消えないため、キャッシュディレクトリが際限なく膨らみます。使うなら書き出し先を別ディレクトリにして入れ替える手順が要るので、選ぶ理由が特に無ければ避けてよい方式です。BuildKit側でレイヤーがどう識別され再利用されるかは[BuildKitとは｜新ビルドバックエンドの仕組みとキャッシュ/マルチアーキ](https://www.issoh.co.jp/tech/details/15701/)で解説しています。

### 依存キャッシュとレイヤーキャッシュを混ぜて考えないための線引き

`actions/cache`による依存キャッシュと、`cache-to`によるレイヤーキャッシュは別物です。前者はランナーのファイルシステム上のディレクトリを丸ごと保存する仕組みで、後者はBuildKitがレイヤー単位の一致で再利用する仕組みになります。コンテナビルドの中で走る`npm ci`や`go mod download`は、ランナー側の`actions/cache`では届きません。`Dockerfile`の中に`RUN --mount=type=cache`を書くか、レイヤーキャッシュを効かせるのが筋です。key設計と復元されない原因の切り分けは[GitHub Actionsのキャッシュ｜actions/cacheのkey設計と復元されない原因](https://www.issoh.co.jp/tech/details/16933/)にまとめました。

## マルチアーキイメージをQEMUとランナー分割のどちらで作るかの判断

Apple Silicon搭載機の開発端末とarm64インスタンスの普及で、amd64とarm64の2種を出す構成が定番になりました。作り方は大きく2通りです。

### QEMUで1つのジョブに載せる形と所要時間が伸びる度合いの目安

`setup-qemu-action`を入れて`platforms`に2つ並べるだけで、1ジョブから両アーキのイメージが出ます。設定が短く、既存のワークフローへ2行足すだけで移行できるのが利点です。

```
- uses: docker/setup-qemu-action@v4
- uses: docker/setup-buildx-action@v4
- uses: docker/build-push-action@v7
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ${{ steps.meta.outputs.tags }}
```

難点は速度です。amd64ランナー上でarm64を作る部分は命令のエミュレーションになるため、コンパイルを含む工程では数倍の時間がかかります。公式ドキュメントも、単一ランナーで複数プラットフォームを作るとビルド時間が大きく伸びる点を挙げています。インタプリタ言語で依存のインストールが主体なら実用範囲に収まりますが、ネイティブ拡張のビルドが走る構成では待ち時間が跳ね上がりました。

### ランナーを分けてdigestでマニフェストを結合する組み立て方

速度が問題になったら、アーキごとにランナーを分けます。それぞれのジョブは自分のアーキだけを実機でビルドし、タグを付けずにダイジェストだけをpushします。最後の結合ジョブが`docker buildx imagetools create`で1つのマニフェストリストへまとめる流れです。ランナーのラベル指定と`-latest`系の更新事情は[GitHub Actionsのruns-on｜ラベル指定の記法とarm64への備え](https://www.issoh.co.jp/tech/details/16929/)を参照してください。

```
jobs:
  build:
    strategy:
      matrix:
        include:
          - platform: linux/amd64
            runner: ubuntu-24.04
          - platform: linux/arm64
            runner: ubuntu-24.04-arm
    runs-on: ${{ matrix.runner }}
    steps:
      - uses: docker/build-push-action@v7
        id: build
        with:
          context: .
          platforms: ${{ matrix.platform }}
          outputs: type=image,push-by-digest=true,name-canonical=true,push=true
```

この形はジョブ数が増えるぶんYAMLが長くなり、ダイジェストを後続ジョブへ渡す受け渡しも要ります。その受け渡しに使うアーティファクトの命名と保持期間は、[GitHub Actionsのアーティファクト｜受け渡しの設計と同名不可・保持期間の決め方](https://www.issoh.co.jp/tech/details/16951/)にまとめてあります。組み合わせの展開規則そのものは[GitHub Actionsのマトリックスビルド｜組み合わせ展開とinclude・exclude](https://www.issoh.co.jp/tech/details/16941/)で扱いました。判断の目安として、QEMU構成のarm64ジョブが10分を超えたあたりから分割の手間が見合ってきます。

## CIでイメージを作る範囲をどこまで広げるかの採用条件と見送り場面

ここまでの部品を全部載せる必要はありません。載せる範囲を決める基準を言い切ります。

### CIでイメージをビルドしてよい条件は再現性と所要時間で決まる

採用してよいのは、次の3つが同時に成り立つときです。1つ目は、ビルドがネットワークとソースだけで完結すること。ライセンスドングルや社内ネットワーク限定のパッケージサーバーに依存していると、ホストランナーでは再現できません。2つ目は、キャッシュを効かせた状態で1回のビルドが10分以内に収まること。これを超えるとプルリクエストごとのビルドが開発の待ち時間として跳ね返ります。3つ目は、pushしたイメージの参照先がSHAタグかダイジェストで固定されていること。動くタグしか無い状態でCIから自動pushすると、本番の中身が誰の操作とも紐付かないまま入れ替わります。

この3条件が揃っているなら、イメージのビルドはCIへ寄せたほうが安全です。手元のマシンでビルドしてpushする運用は、担当者の環境差がそのままイメージへ入り込みます。パイプライン全体の組み立て方は[CI/CDとは？仕組み・パイプライン・導入すべき企業の判断基準](https://www.issoh.co.jp/column/details/13002/)で整理しました。

### CIでのビルドを見送って別の仕組みへ寄せたほうがよい3つの場面

1つ目は、ビルドに大容量のデータやモデルファイルを同梱する場合です。数GBのアーティファクトをジョブごとに転送すると、キャッシュ枠も課金分数も一気に消えます。実行時にオブジェクトストレージから取得する構成へ変えるほうが、イメージも軽くなります。

2つ目は、リリース頻度が月1回を下回り、ビルドが年に数回しか走らない場合です。CIの構成を保守する手間のほうが上回るため、リリース手順書と手動ビルドで足ります。ただしその場合でも、ビルド用の`Dockerfile`と版の記録だけはリポジトリへ残してください。

3つ目は、規制上ビルド環境をホストランナーへ出せない場合です。この場合はセルフホストランナーへ寄せる選択になりますが、ランナーの隔離とイメージの持ち出し経路を設計し直す前提の話になります。GitHubホストのままで押し切ろうとすると、シークレットの置き場所が破綻します。

## よくある質問

### push: true を付けずにビルドだけ確認したいときはどうしますか？

`push: false`のままでも構文エラーやビルド失敗は検出できます。プルリクエストではビルドのみ、デフォルトブランチへのマージでpushする、という分け方が一般的です。`load: true`を付ければランナー上のDockerへ読み込まれるので、続くステップでコンテナを起動したテストも回せます。テストが依存するDBやキャッシュを同じジョブへ並べたいときは、[GitHub Actionsのサービスコンテナ（services）](https://www.issoh.co.jp/tech/details/16961/)のほうが扱いやすくなります。

### キャッシュを設定したのに毎回フルビルドになるのはなぜですか？

まず`cache-to`が書かれているかを確認してください。`cache-from`だけでは保存が行われません。次にスコープです。ブランチ名でスコープを分けている場合、新規ブランチの初回は当然ヒットしません。それでも改善しないときは、`COPY . .`のような広い命令が`Dockerfile`の前段にあり、どんな変更でも以降のレイヤーが全て無効化されている可能性が高いです。

### GITHUB\_TOKENでDocker Hubへもpushできますか？

できません。`GITHUB_TOKEN`はGitHub内のリソース向けの資格情報で、Docker Hubへ送るならアクセストークンをシークレットとして登録し、`login-action`の`registry`を省略した形でログインします。レートリミットの条件もGHCRとは別なので、pull側の制限も併せて確認してください。

### build-push-actionのバージョンは固定すべきですか？

メジャーのタグ指定で運用し、供給網の要件が厳しい環境ではコミットSHAで固定します。メジャーを跨ぐ更新では実行環境のNode版が変わることがあり、セルフホストランナーの古いOSでは動かなくなる場合があるため、更新時はテスト用のブランチで通してから寄せてください。

### マルチアーキのイメージはどのタグで参照すればよいですか？

通常のタグをそのまま指定します。マニフェストリストが各アーキのイメージを束ねているため、pull側の環境に合ったものが自動で選ばれます。特定のアーキだけを引きたい場合は、`docker pull --platform`で明示するか、ダイジェストを直接指定してください。

## 関連記事

- [BuildKitとは｜Dockerの新ビルドバックエンドの仕組み・キャッシュ/secret/マルチアーキと採用判断](https://www.issoh.co.jp/tech/details/15701/)：レイヤーがどう識別され再利用されるかの内部側を確認できます。
- [GitHub Container Registry（ghcr.io）の料金とは｜無料枠・課金条件とDocker Hub/ECR比較](https://www.issoh.co.jp/tech/details/7315/)：push先を決めるときの保存容量と課金の条件です。
- [GitHub Actionsのキャッシュ｜actions/cacheのkey設計と復元されない原因の切り分け](https://www.issoh.co.jp/tech/details/16933/)：依存キャッシュ側の設計と切り分け手順です。
- [Artifact Registryとは？Google Cloudの成果物リポジトリの仕組み・料金とContainer Registry移行](https://www.issoh.co.jp/tech/details/15559/)：Google Cloudへpushする場合のレジストリ設計です。
- [GitHub Actionsでビルド・自動テストを設定する方法｜CI/CDワークフローの作り方](https://www.issoh.co.jp/tech/details/2497/)：イメージ化する前のジョブとテストの組み方です。

---

出典: [GitHub ActionsでDockerイメージをビルドする｜push先とキャッシュ・マルチアーキの設計](<https://www.issoh.co.jp/tech/details/16943/>)（株式会社一創）
