![プログラマーがデジタルなワークフロー上で credsStore を操作し、osxkeychain エラーを回避して GHCR へのプッシュを試行錯誤する様子を描いたイラスト。macOS Self-hosted Runner 環境における認証情報の問題を解決するプロセスを示しています。](https://wakatchi.dev/wp-content/uploads/2026/05/github-actions-self-hosted-runner-ghcr-macos-osxkeychain-eyecatch.webp)

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 を迂回できるはず——と思いました。

`yaml
- 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 に直接書き込みます。

`yaml
- 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つです。

1. **
docker login を呼ばない**:credential helper の解決プロセスを完全にスキップ
2. **
GITHUB_TOKEN は短命**:ジョブ実行中だけ有効なので、Base64 で平文保存しても許容範囲
3. **
chmod 0600**:Self-hosted Runner は $RUNNER_TEMP を自動クリーンアップしないため、ファイル権限を最小に

### クリーンアップ

ジョブ終了時に認証情報を確実に消します。

`yaml
- 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 の設定タイミング** でした。

`yaml
# 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 を変更すると、ビルダーが見つからなくなります。

`yaml
# 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 の順序を入れ替えるだけで解決しました。

## 最終的なワークフロー構成

`yaml
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 ジョブで自動クリーンアップしています。

`yaml
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 を列挙し、指定日数より古いものを削除します。

`bash
# 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 の組み合わせ固有の問題で、ドキュメントにも載っていません。同じ構成で困っている方の参考になれば幸いです。

## 参考リンク

- [docker/login-action - Known issues](https://github.com/docker/login-action#known-issues)
- [Docker Desktop - Credential management](https://docs.docker.com/desktop/get-started/#credentials-management)
- [GitHub Packages - Working with GHCR](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)