GitHub

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

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

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

まとめ:イメージビルドを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とは|書き方・主要命令・ベストプラクティスで確認してください。なお、この記事の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管理|置き場所の選定と漏えい経路で整理しています。

もう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移行に整理しました。ECRリポジトリやIAMの設計をまとめて引き取ってほしい場合はインフラ構築(AWS・Google Cloud・Azure)でご相談ください。

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

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

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)の料金とは|無料枠・課金条件で確認できます。

レイヤーキャッシュを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とは|新ビルドバックエンドの仕組みとキャッシュ/マルチアーキで解説しています。

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

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

マルチアーキイメージを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への備えを参照してください。

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のアーティファクト|受け渡しの設計と同名不可・保持期間の決め方にまとめてあります。組み合わせの展開規則そのものはGitHub Actionsのマトリックスビルド|組み合わせ展開とinclude・excludeで扱いました。判断の目安として、QEMU構成のarm64ジョブが10分を超えたあたりから分割の手間が見合ってきます。

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

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

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

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

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

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

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

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

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

よくある質問

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

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

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

まず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で明示するか、ダイジェストを直接指定してください。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.09 テックブログ IDCFクラウド(IDCフロンティア)不正アクセス・ランサムウェア:影響先・復旧・データは戻るか
  2. 2026.10.08 テックブログ 大阪公立大学のランサムウェア被害と仮想化基盤の停止|全授業休講に至った経緯とバックアップを守る設定
  3. 2024.11.08 テックブログ OpenAPI GeneratorでJavaコードを自動生成する方法|CLI導入からSpring・ライブラリ選択まで
  4. 2026.10.09 テックブログ ニッスイのサイバー攻撃で日水物流の入出荷停止|委託先クラウド障害に荷主が備える手順
  5. 2026.10.09 テックブログ 京王電鉄のランサムウェア被害とグループ共通基盤:決済・ポイント・予約が止まった範囲と遮断の初動

RELATED POSTS 関連記事

目次