プログラマーがデジタルなワークフロー上で credsStore を操作し、osxkeychain エラーを回避して GHCR へのプッシュを試行錯誤する様子を描いたイラスト。macOS Self-hosted Runner 環境における認証情報の問題を解決するプロセスを示しています。

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

  1. docker login を呼ばない:credential helper の解決プロセスを完全にスキップ
  2. GITHUB_TOKEN は短命:ジョブ実行中だけ有効なので、Base64 で平文保存しても許容範囲
  3. 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 の組み合わせ固有の問題で、ドキュメントにも載っていません。同じ構成で困っている方の参考になれば幸いです。

参考リンク