
はじめに
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_CLIENT → GITHUB_PAT の順でフォールバックします。
1つの PAT で全 org をカバーできる場合と、org ごとに PAT を分ける場合の両方に対応できます。
npm キャッシュの org 別分離: runner-client-cache-npm と runner-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-docker と native のどちらも書かない場合、両方の 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 への移行がスムーズになる

