GitHub Actionsのセルフホスト型ランナーをDocker Composeで複数組織に展開するシステムアーキテクチャ図。コンテナとネイティブの実行、セキュリティ分離、ワークフローのルーティング、Kubernetesへの移行パス、性能比較を概観する。

はじめに

GitHub Actions の self-hosted runner を使い始めると、すぐにぶつかる制限があります。
1つの runner は1つの Organization にしか登録できないという点です。

複数の Organization にまたがってリポジトリを持っている場合——たとえばクライアント案件用の org と自社開発用の org が別れている場合——、runner の管理が急に複雑になります。

本記事では、1台の Mac 上で Docker Compose を使って複数 Organization 向けの self-hosted runner を運用する設計を紹介します。
Container と Native の二刀流、ラベルによるワークフロールーティング、AI 生成コードの実行分離、将来の Kubernetes 移行パスまでを含む全体設計です。

こんな人におすすめ

  • 複数の GitHub Organization で self-hosted runner を共有したい方
  • Container と Native のどちらで runner を動かすか迷っている方
  • AI が生成したコードを安全に CI で検証するための分離設計を探している方
  • Docker Compose で runner を管理しつつ、将来 ARC/Kubernetes に移行する道筋を残しておきたい方

アーキテクチャ: Container × Native の二刀流

各 Organization に対して 2種類の runner を提供する設計にしました。

graph TD
    subgraph Mac["Mac (Docker Desktop)"]
        subgraph Container["Container Runner (Docker Compose)"]
            C1["runner-client<br/>→ client-org (3 replicas)"]
            C2["runner-internal<br/>→ internal-org (3 replicas)"]
        end
        subgraph Native["Native Runner (LaunchAgent)"]
            N1["runner-client-native<br/>→ client-org"]
            N2["runner-internal-native<br/>→ internal-org"]
        end
    end

    style Container fill:#e1f5ff,stroke:#0288d1,color:#000
    style Native fill:#e8f5e9,stroke:#388e3c,color:#000

正直なところ、最初は Container だけで十分だろうと思っていました。
しかし実測ベンチマーク(Apple Silicon / 2026-05-15)を取ってみると、ワークロードによって得意不得意がはっきり分かれることがわかりました。

観点 Container Native 速度差
総合(9 step matrix) 79s 45s Native 1.76x
大量小ファイル(20,000 create) 0.27s 3.74s Container 14x
大容量 write(dd 2GB) 4.5 GB/s 7.55 GB/s Native 1.68x
npm install(85 packages) 2.08s 1.62s Native 1.29x
メモリ上限 7.6 GiB Mac 全 RAM Native

総合では Native が速いですが、大量の小ファイル操作(npm install のキャッシュ展開、git checkout など)では Container の ext4 が圧倒的に有利です。
ワークフローの特性に応じて使い分けることで、CI 全体の実行時間を最適化できます。

Docker Compose の設計

YAML anchor で共通設定を括り出し、Organization ごとの差分だけを定義します。

x-runner-base: &runner-base
  build:
    context: ./runners/_image
    args:
      RUNNER_VERSION: "2.334.0"
  image: gh-runner-infra/runner:2.334.0
  restart: unless-stopped
  stop_grace_period: 30s
  init: true
  mem_limit: 3g
  mem_reservation: 512m
  sysctls:
    - net.ipv6.conf.all.disable_ipv6=1

services:
  runner-client:
    <<: *runner-base
    environment:
      ORG_NAME: "${GITHUB_ORG_CLIENT:-client-org}"
      ACCESS_TOKEN: "${GITHUB_PAT_CLIENT:-${GITHUB_PAT:-}}"
      RUNNER_BASE_NAME: "runner-client"
      RUNNER_GROUP: "client"
      LABELS: "self-hosted,linux,ARM64,mac-docker,client,node,php"
    volumes:
      - runner-client-cache-npm:/home/runner/.npm

  runner-internal:
    <<: *runner-base
    environment:
      ORG_NAME: "${GITHUB_ORG_INTERNAL:-internal-org}"
      ACCESS_TOKEN: "${GITHUB_PAT_INTERNAL:-${GITHUB_PAT:-}}"
      RUNNER_BASE_NAME: "runner-internal"
      RUNNER_GROUP: "internal"
      LABELS: "self-hosted,linux,ARM64,mac-docker,internal,node,ai-pr"
    volumes:
      - runner-internal-cache-npm:/home/runner/.npm

volumes:
  runner-client-cache-npm:
  runner-internal-cache-npm:

設計のポイントは4つです。

環境変数の優先度チェーン: GITHUB_PAT_CLIENTGITHUB_PAT の順でフォールバックします。
1つの PAT で全 org をカバーできる場合と、org ごとに PAT を分ける場合の両方に対応できます。

npm キャッシュの org 別分離: runner-client-cache-npmrunner-internal-cache-npm を別 volume にしています。
同一 volume を共有すると、悪意あるパッケージが org 境界を越えて伝播するリスクがあります。

--scale によるレプリカ制御: docker compose up -d --scale runner-client=3 --scale runner-internal=3 で起動数を調整できます。
起動スクリプトでは 1〜10 の range validation を入れ、リソース枯渇を防いでいます。

mem_limit: 3g: 1 container の暴走でホスト全体が巻き込まれるのを防ぎます。
Vitest + coverage のピーク時を考慮した値です。

ラベル設計: ワークフロールーティングの鍵

GitHub Actions の runs-onラベルの AND 条件で runner を選択します。
ラベル設計がルーティングの鍵になります。

共通:       self-hosted, ARM64
OS:         linux(container)/ macOS(native)
実行環境:   mac-docker(container)/ native(Mac直接)
案件:       client / internal
用途:       node, php, ai-pr, playwright

ワークフロー側の runs-on 指定例です。

# Container を明示(service container が必要な場合)
runs-on: [self-hosted, mac-docker, client, node]

# Native を明示(E2E テストやメモリ大量使用の場合)
runs-on: [self-hosted, native, client]

# AI 生成 PR の検証(必ず Container 側 = isolation 確保)
runs-on: [self-hosted, mac-docker, internal, ai-pr]

mac-dockernative のどちらも書かない場合、両方の runner にマッチして早い者勝ちになります。
意図しない runner への振り分けを防ぐため、実行環境ラベルは必ず明示するのがルールです。

ワークロード別の早見表

ワークロード 推奨
service container(postgres / redis)を使う mac-docker
大量の小ファイル展開 mac-docker
AI 生成 PR の検証 mac-docker + ai-pr
Playwright / Cypress E2E native
Docker build native
メモリ 6GB+ native
軽量 lint / changes filter ubuntu-latest(GitHub-hosted)

セキュリティ: 3つの分離原則

原則1: Docker Socket は渡さない

Container runner には Docker socket をマウントしません。
socket を渡すと、container 内から host の Docker daemon を操作でき、事実上 root 相当の権限を持つことになります。

Docker build が必要なワークフローは Native runner で Mac の Docker Desktop を直接使います。

原則2: AI 生成コードは Container で隔離

AI エージェントが生成した PR のコードは、Native runner で実行しません。
Native は Mac のホームディレクトリ(~/.gitconfig~/.ssh/ など)が見えるため、isolation が弱いからです。

ai-pr ラベルは Container 側にのみ付与し、AI 生成コードは常に Container の隔離環境で実行します。

原則3: Public repo では self-hosted を使わない

Public repository の fork PR から self-hosted runner を使わせると、任意のコードが runner 上で実行されるリスクがあります。

if: >-
  github.event_name != 'pull_request' ||
  github.event.pull_request.head.repo.full_name == github.repository

この条件に加えて、runner group 設定で Allow public repositories を無効化しています。

つまづいたポイント

RUNNER_GROUP は単数形

コミュニティ製の runner image を使う場合、環境変数名が RUNNER_GROUPS(複数形)ではなく RUNNER_GROUP(単数形)であることがあります。
複数形を指定しても黙って無視されるので、30分ほどハマりました。

--replace は runner group を移動しない

同名の runner を config.sh --replace で再登録しても、以前の runner group を引き継ぎます。
group を変更するには、GitHub の管理画面で一度 DELETE してから起動し直す必要があります。

Mac のスリープで runner がオフラインになる

Docker Desktop ごとスリープします。
caffeinate コマンドや「ディスプレイがオフでもスリープしない」設定で回避できますが、24時間運用には限界があります。

将来: ARC/Kubernetes への移行パス

現状の Docker Compose は MVP です。
以下のいずれかに該当したら、Actions Runner Controller(ARC)への移行を検討します。

  • 並列ジョブ数が常時5を超える
  • 夜間の CI 需要が増え、Mac スリープが運用ネックになる
  • runner group が10を超えてポリシー管理が追いつかなくなる

移行のポイントは、ラベル設計と runner group 設計はそのまま引き継げることです。
ARC の RunnerScaleSet にラベルを付け替えるだけで、ワークフロー側の変更は不要です。

段階的に ARC と Docker Compose を並行稼働させ、安定したら順次移行する戦略が現実的です。

まとめ

GitHub Actions の self-hosted runner を複数 Organization で運用する設計のポイントをまとめます。

  • 1 runner = 1 org の制限を Docker Compose の service 分割で吸収する
  • Container + Native の二刀流で、ワークロードの特性に応じた runner を選択する
  • ラベルの AND 条件を活用し、mac-docker / native で実行環境を明示的にルーティングする
  • Docker socket を渡さないAI 生成コードは Container で隔離public repo では使わないの3原則でセキュリティを確保する
  • ラベル設計を最初から ARC 互換にしておけば、Kubernetes への移行がスムーズになる