Claude CodeでghcrにDockerイメージを公開するワークフローを組む
Claude CodeにGitHub Actionsのワークフローを書かせ、Dockerイメージをghcrへpushするまでを、最小権限のGITHUB_TOKEN・sha/semverタグ・ghaキャッシュ込みで組みます。
ghcr(GitHub Container Registry)へのイメージ公開は、packages: writeを持つGITHUB_TOKENでログインし、docker/build-push-actionでpushするワークフローを1本置けば成立します。Claude Codeに任せられるのはこのYAMLの下書きです。ただし権限の範囲、タグの規則、キャッシュの置き場は、人が決めてから指示したほうが手戻りが減ります。
この記事は「ghcrへ公開するワークフロー」に絞り、権限・タグ付け・キャッシュの3点を決める順に進めます。Dockerfileそのものの軽量化はClaude CodeでDockerfileを最適化する手順の領分です。
ghcrへ公開するワークフローの完成形
ghcrはghcr.ioで提供されるコンテナレジストリです。Docker Image Manifest V2 Schema 2とOCIのイメージ形式に対応し、イメージを組織や個人のアカウントの下に置けます。リポジトリと関連づけて、その権限を引き継ぐこともできます。
先に完成形を置きます。PRではビルドだけ行い、mainへのpushとv*タグのpushでghcrへ公開する構成です。
name: publish-image
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v6
- uses: docker/setup-buildx-action@v4
- name: Log in to ghcr
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract tags and labels
id: meta
uses: docker/metadata-action@v6
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=maxアクションのバージョンは、各READMEの記載例(build-push-action@v7、metadata-action@v6、login-action@v4、setup-buildx-action@v4)に合わせています。サードパーティのアクションは、コミットSHAで固定する方法もあります。タグ参照だと、アクションの中身が予告なく変わることがあるためです。固定するかどうかは、組織の方針で決めます。
ワークフローが上から順にやること
- 1
checkoutとbuildxの準備
リポジトリを取得し、
docker-containerドライバーのbuilderを用意します。キャッシュの書き出しやマルチプラットフォームビルドに必要です。 - 2
ghcrへのログイン
GITHUB_TOKENで認証します。PRのときはpushしないので、ログインも省きます。 - 3
タグとラベルの生成
ブランチ名・タグ・コミットSHAから、イメージのタグとOCIラベルを組み立てます。
- 4
ビルドとpush
キャッシュを読み込んでビルドし、PR以外ならghcrへpushします。
packages: writeとGITHUB_TOKENで権限を最小にする
ghcrへの認証には、ワークフローの中ならGITHUB_TOKENを使います。同じワークフローのリポジトリに紐づくパッケージの公開にはGITHUB_TOKENを使います。個人アクセストークンを使っている場合は、GITHUB_TOKENへの置き換えが強く勧められています。
必要な権限は、jobs.<id>.permissionsに書きます。GitHubのレジストリ向けの案内では、contentsの読み取りとpackagesの書き込みです。上のYAMLのcontents: readとpackages: writeがそれにあたります。
GitHubの公開例には、これに加えてattestations: writeとid-token: writeが並びます。この2つは、イメージの来歴を証明するアーティファクト認証(attestation)の生成ステップ用です。認証を使わない構成なら不要で、足すかどうかは供給網の要件次第です。
| 権限 | 必要な場面 | 使わないとき |
|---|---|---|
contents: read | 必要な場面checkoutでソースを取得する | 使わないとき常に書いておく |
packages: write | 必要な場面ghcrへpushする | 使わないときPRのビルド検証だけなら不要 |
attestations: write / id-token: write | 必要な場面来歴の認証を生成する | 使わないとき認証を作らないなら不要 |
YAMLに最小の組を明示しておくと、Claude Codeが後から権限を足したときに差分で目につきます。
個人アクセストークン(classic)を使う場合の注意も、ドキュメントにあります。UIでwrite:packagesを選ぶとrepoスコープも一緒に選ばれ、必要以上に広い権限になります。GitHub Actionsのワークフローでは、特に避けるよう案内されています。
sha・semverのタグをmetadata-actionで決める
docker/metadata-actionは、GitのイベントからDockerのタグとラベルを生成します。タグ付けの規則をYAMLのtags:に書くので、Claude Codeに「タグ戦略を変えて」と頼むときの変更箇所が1か所に定まります。
上のワークフローが生成するタグは、次のとおりです。
| イベント | ref | 生成されるタグ |
|---|---|---|
pull_request | refrefs/pull/2/merge | 生成されるタグpr-2 |
push | refrefs/heads/main | 生成されるタグmain |
push tag | refrefs/tags/v1.2.3 | 生成されるタグ1.2.3、1.2、latest |
push tag | refrefs/tags/v2.0.8-beta.67 | 生成されるタグ2.0.8-beta.67 |
type=shaを足すと、sha-860c190のような短縮SHAのタグが付きます。短縮SHAの長さは既定で7文字です。大きなリポジトリで衝突が気になる場合は、環境変数DOCKER_METADATA_SHORT_SHA_LENGTHで長くできます。
latestはflavorの設定で扱われ、既定のautoではtype=ref,event=tagとtype=semverのときに付きます。つまりmainへのpushだけではlatestは付きません。「mainの最新をlatestにしたい」場合は、type=raw,value=latest,enable={{is_default_branch}}のような指定が必要です。この書き方はmetadata-actionのREADMEに載る形式です。
イメージ名は、metadata-actionが小文字に直します。github.repositoryに大文字が入っていても、imagesにそのまま渡せます。タグ名の不正な文字は-に置き換わります。
実運用では「何を不変の参照にするか」が分かれ目です。タグ1.2やmainは後から別のイメージを指します。デプロイ側で固定したいなら、タグではなくダイジェストを使います。ghcrからはダイジェスト指定でpullできます。
docker pull ghcr.io/OWNER/IMAGE@sha256:<digest>build-push-actionはdigestを出力に持つので、後続のステップでデプロイ先に渡せます。
buildxのキャッシュをtype=ghaで効かせる
GitHub Actionsのランナーは毎回まっさらな状態で始まります。レイヤーキャッシュを残さないと、依存のインストールが毎回走ります。buildxにはGitHub Actionsのキャッシュを使うghaバックエンドがあり、ワークフロー内で使う方式として推奨されています。
cache-from: type=gha
cache-to: type=gha,mode=maxbuild-push-actionを使えば、キャッシュのURLとトークンは自動で補われます。手動で書く必要はありません。ポイントは3つです。
- mode=max: 既定の
minは最終イメージのレイヤーだけを書き出します。maxはより多くのレイヤーを書き出します。マルチステージビルドで、ビルド用ステージの依存インストールまでキャッシュに残したいときに選びます - scope: 既定のscopeは
buildkitです。複数のイメージを同じリポジトリでビルドすると、後のビルドが前のキャッシュを上書きし、最後の1つしか残りません。イメージごとにscope=apiのような名前を付けて分けます - アクセス範囲: キャッシュを読めるのは、現在のブランチ・ベースブランチ・デフォルトブランチのものだけです。イベントによっては書き込みが読み取り専用になる場合もあります
scopeを分けた例です。
cache-from: type=gha,scope=api
cache-to: type=gha,mode=max,scope=apiもう1つ、キャッシュ書き出しの失敗に備えます。GitHub ActionsのキャッシュAPIには、短時間に多数のリクエストを送るとレート制限がかかる場合があります。その結果としてexporting to GitHub Actions Cacheがmaximum timeout reachedで止まる例があります。build-push-actionではgithub-tokenがsecrets.GITHUB_TOKENに自動で設定され、BuildKitは標準のGitHub APIでキャッシュキーを確認するため、通常は追加設定が要りません。
なお、ghaバックエンドはdockerドライバーのままだと、containerdイメージストアが有効な場合に限って動きます。この記事のワークフローはsetup-buildx-actionでdocker-containerドライバーを使うので、この制約には当たりません。ローカルでのdocker buildx buildに同じ指定を持ち込むと動かない場合があります。
Claude Codeにワークフローを書かせて確かめる
ここまでで決めたことを、Claude Codeへの指示とCLAUDE.mdに落とします。
CLAUDE.mdには、ワークフローを書き換えるたびに守らせたい制約を置きます。CLAUDE.mdに規約を書いておけば、ClaudeはPRの作成や依頼への対応でそれに従います。
## CIでのイメージ公開
- ghcrへのpushは`GITHUB_TOKEN`で行い、PATは使わない
- jobのpermissionsは`contents: read`と`packages: write`だけにする
- pull_requestイベントではpushしない(`push: false`相当にする)
- タグ規則は`docker/metadata-action`の`tags:`に集約し、build-push-actionの`tags`には手書きしない
- キャッシュは`type=gha,mode=max`。イメージが複数ならscopeを分けるそのうえで、Claude Codeに依頼します。
.github/workflows/publish-image.ymlを新規作成してください。CLAUDE.mdの「CIでのイメージ公開」に従い、mainへのpushとv*タグでghcr.io/${{ github.repository }}にpushします。PRではビルドのみ。タグはsha・semver・ブランチ名。出来たらpermissionsとpushの条件だけを箇条書きで説明してください。「permissionsとpushの条件だけを説明させる」のは、レビューの焦点をそこに固定するためです。YAMLの全行を読むより、権限の広がりとPRでのpush有無を先に見るほうが、事故につながる箇所を拾えます。
GitHub上でClaudeにワークフローを書かせたい場合は、@claudeのメンションで動くClaude Code GitHub Actionも使えます。その場合、ClaudeのコミットでCIが動かない点に注意してください。既定のGITHUB_TOKENで作ったコミットはワークフローを起動しません。claude-code-actionにgithub_token: ${{ secrets.GITHUB_TOKEN }}を渡していると、ClaudeのPRでビルド検証が走らなくなります。この引数を外してClaude GitHub Appとして認証するか、カスタムのAppトークンを渡します。導入の全体像はClaude CodeをGitHub Actionsに組み込むで扱っています。
作ったワークフローの確認は、次の順で進めます。
- PRを開き、
pull_requestでビルドだけが走ってpushされないことを確かめる mainにマージし、パッケージページにタグmainとsha-…が付くことを確かめるgit tag v0.1.0 && git push origin v0.1.0で、0.1.0・0.1・latestが付くことを確かめる- 同じコミットで再実行し、ビルドログのキャッシュ利用(
CACHED)を確かめる
初回公開で詰まりやすい点
- パッケージが非公開で作られる: 初めてpushしたパッケージの可視性は、既定でprivateです。公開したい場合は、パッケージの設定画面で可視性を変えます。リポジトリに紐づいたパッケージは、そのリポジトリのアクセス権限を引き継ぎます
- リポジトリと紐づかない: コマンドラインからpushしたイメージは、リポジトリ名と同じ名前空間でも、リポジトリには自動で紐づきません。ワークフローから
GITHUB_TOKENでpushすれば、そのリポジトリと自動で紐づきます。DockerfileにLABEL org.opencontainers.image.source=https://github.com/OWNER/REPOを入れておくと、コマンドラインからのpushでも紐づけられます - GITHUB_TOKENでpushできない: 同じ名前空間に、リポジトリへ紐づけないままpushしたパッケージが先にあると、
GITHUB_TOKENにpushの権限がありません。手元で試しにpushしたイメージと同名のパッケージがあれば、パッケージ設定でリポジトリを接続します - PRからのpush: forkからのPRでは、
GITHUB_TOKENは親リポジトリへの読み取り権限になります。push: falseの条件を外さないでください - レイヤーの大きさと時間: ghcrは1レイヤーあたり10GBの上限と、アップロード10分のタイムアウトを持ちます。巨大なモデルファイルをレイヤーに含める構成は、ここで止まります
まとめ
最小構成で足りるかは、公開範囲で分かれます。社内向けのイメージなら、完成形のYAMLにダイジェスト参照を添えるだけで運用できます。OSSとして外部に配るイメージは、アクションをコミットSHAで固定し、来歴のattestationを足す構成まで検討する価値があります。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。