
Self-hosted Runner のカスタム Docker イメージを CI でビルドし、ghcr.io(GitHub Container Registry)に push する仕組みを作りました。GitHub-hosted Runner なら docker/login-action で一発ですが、macOS の Self-hosted Runner では動きませんでした。
Error: User interaction is not allowed. (-25308)
この記事では、osxkeychain 問題の原因と、3回の試行錯誤を経て辿り着いた回避策を紹介します。
問題:osxkeychain がブロックする
macOS の Docker Desktop は、認証情報を macOS Keychain に保存します。~/.docker/config.json には "credsStore": "desktop" が設定されており、docker login を実行すると Docker Desktop の credential helper が Keychain にアクセスします。
GitHub Actions のジョブは launchd 経由の非対話セッション で実行されます。GUI セッションがないため、Keychain へのアクセスが -25308(User interaction is not allowed)で拒否されます。
GitHub-hosted Runner ではこの問題が起きません。Linux VM 上で動作し、macOS Keychain に依存しないためです。
試行1:credsStore を空にする(失敗)
ジョブ専用の DOCKER_CONFIG ディレクトリを作り、credsStore を空にすれば credential helper を迂回できるはず——と思いました。
- name: Isolate DOCKER_CONFIG
run: |
docker_config_dir="${RUNNER_TEMP}/docker-config"
mkdir -p "$docker_config_dir"
echo '{"credsStore": ""}' > "$docker_config_dir/config.json"
echo "DOCKER_CONFIG=$docker_config_dir" >> "$GITHUB_ENV"
結果:失敗。 Docker CLI は credsStore が空でも、デフォルトの credential helper を探しに行きます。macOS ではそれが osxkeychain で、同じエラーが再発しました。
試行2:config.json に直接書き込む(成功)
docker login を一切使わず、認証情報を Base64 エンコードして config.json に直接書き込みます。
- name: Configure ghcr auth
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHCR_USER: ${{ github.actor }}
run: |
set -euo pipefail
docker_config_dir="${RUNNER_TEMP}/docker-config"
mkdir -p "$docker_config_dir"
auth=$(printf '%s:%s' "$GHCR_USER" "$GHCR_TOKEN" | base64)
cat > "$docker_config_dir/config.json" <<EOF
{"auths": {"ghcr.io": {"auth": "$auth"}}}
EOF
chmod 0600 "$docker_config_dir/config.json"
echo "DOCKER_CONFIG=$docker_config_dir" >> "$GITHUB_ENV"
ポイントは3つです。
docker loginを呼ばない:credential helper の解決プロセスを完全にスキップGITHUB_TOKENは短命:ジョブ実行中だけ有効なので、Base64 で平文保存しても許容範囲chmod 0600:Self-hosted Runner は$RUNNER_TEMPを自動クリーンアップしないため、ファイル権限を最小に
クリーンアップ
ジョブ終了時に認証情報を確実に消します。
- name: Cleanup docker config
if: always()
run: rm -rf "${RUNNER_TEMP}/docker-config" || true
if: always() はジョブがキャンセル・タイムアウトした場合でも実行されます。GitHub-hosted Runner と違い、Self-hosted Runner は次のジョブで同じファイルシステムを使うため、これが必須です。
試行3:step の実行順序の罠
試行2 で認証は通りましたが、今度は別のエラーが出ました。
Error: Docker buildx is required
docker/setup-buildx-action を使って buildx をセットアップしているのに、なぜ見つからないのか。
原因は DOCKER_CONFIG の設定タイミング でした。
# NG: setup-buildx の後に DOCKER_CONFIG を設定
- uses: docker/setup-buildx-action@v3 # ← $DOCKER_CONFIG/buildx/ にビルダーを作成
- name: Configure ghcr auth # ← DOCKER_CONFIG を別ディレクトリに変更
run: echo "DOCKER_CONFIG=..." >> "$GITHUB_ENV"
- uses: docker/build-push-action@v6 # ← 新しい DOCKER_CONFIG にビルダーがない!
setup-buildx-action は $DOCKER_CONFIG/buildx/ にビルダーの状態を保存します。後から DOCKER_CONFIG を変更すると、ビルダーが見つからなくなります。
# OK: DOCKER_CONFIG の設定を setup-buildx の前に移動
- name: Configure ghcr auth # ← 先に DOCKER_CONFIG を設定
- uses: docker/setup-buildx-action@v3 # ← 正しい DOCKER_CONFIG にビルダーを作成
- uses: docker/build-push-action@v6 # ← 同じ DOCKER_CONFIG からビルダーを参照
step の順序を入れ替えるだけで解決しました。
最終的なワークフロー構成
image-build-and-scan:
runs-on: [self-hosted, macOS, ARM64, native, biz-dev]
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
# 1. 認証設定(DOCKER_CONFIG を先に確定)
- name: Configure ghcr auth
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GHCR_USER: ${{ github.actor }}
run: |
docker_config_dir="${RUNNER_TEMP}/docker-config"
mkdir -p "$docker_config_dir"
auth=$(printf '%s:%s' "$GHCR_USER" "$GHCR_TOKEN" | base64)
cat > "$docker_config_dir/config.json" <<EOF
{"auths": {"ghcr.io": {"auth": "$auth"}}}
EOF
chmod 0600 "$docker_config_dir/config.json"
echo "DOCKER_CONFIG=$docker_config_dir" >> "$GITHUB_ENV"
# 2. Buildx セットアップ(正しい DOCKER_CONFIG 上で)
- uses: docker/setup-buildx-action@v3
# 3. ビルド → スキャン → Push
- uses: docker/build-push-action@v6
with:
push: ${{ github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/runner:buildcache
...
# 4. クリーンアップ(常に実行)
- name: Cleanup
if: always()
run: rm -rf "${RUNNER_TEMP}/docker-config" || true
GHCR の運用:retention 自動化
イメージを ghcr.io に push し続けると、古い untagged manifest が蓄積します。BuildKit の registry cache も同様です。
週次の retention ジョブで自動クリーンアップしています。
on:
schedule:
- cron: '0 18 * * 0' # 毎週日曜 18:00 UTC
workflow_dispatch:
inputs:
delete:
type: boolean
default: false
age-days:
type: string
default: '7'
max-delete:
type: string
default: '100'
スクリプトは GitHub API で untagged manifest を列挙し、指定日数より古いものを削除します。
# dry-run(デフォルト)
gh api "orgs/{org}/packages/container/{pkg}/versions" --paginate \
| jq '[.[] | select((.metadata.container.tags // []) | length == 0)]'
# 削除(--delete 指定時のみ)
gh api -X DELETE "orgs/{org}/packages/container/{pkg}/versions/{id}"
安全装置として max-delete で1回あたりの削除上限を設けています。schedule トリガーは常に dry-run で、実削除は workflow_dispatch で明示的に実行します。
まとめ
| 問題 | 原因 | 解決策 |
|---|---|---|
User interaction is not allowed |
macOS Keychain が非対話セッションをブロック | config.json に直接 Base64 認証を書き込む |
Docker buildx is required |
DOCKER_CONFIG 変更で buildx の状態が消失 | auth 設定を setup-buildx より前に移動 |
| untagged manifest の蓄積 | BuildKit cache + image push の残骸 | 週次 retention ジョブ |
GitHub-hosted Runner の「当たり前」が Self-hosted Runner では通用しないことがあります。docker/login-action が使えないのは macOS + launchd の組み合わせ固有の問題で、ドキュメントにも載っていません。同じ構成で困っている方の参考になれば幸いです。


