開発のこと
Github Actions の無料枠が足りなくなったので、runner だけ自前の機械に移した
2026.10.27
GitHub Actions の枠を使い切りそうになった
GitHub Actions には、非公開リポジトリで使える時間の枠があります。
| プラン | 1か月の枠 |
|---|---|
| Free | 2,000分 |
| Pro | 3,000分 |
小さなプロダクトなら十分に見えますが、PR に push するたびに CI が走るので、思ったより早く減ります。私たちのリポジトリで、直近40回の CI がどこで時間を使っていたかを数えました。
| ジョブ | 回数 | 合計 | 1回あたり |
|---|---|---|---|
| 単体テスト(Vitest) | 40 | 184分 | 4.6分 |
| 変更ファイルの判定 | 40 | 40分 | 1分 |
| Lint(Biome) | 40 | 40分 | 1分 |
| E2E(Playwright) | 1 | 8分 | 8分 |
分かったことは2つです。
- 一番使っているのは単体テスト。 push のたびに走るためです。E2E は1回が長くても、デプロイのときにしか走らないので合計は小さい
- 数秒で終わるジョブも1分ずつ使う。 GitHub はジョブごとに1分単位で切り上げて数えます
テストはこれからも増える
今は枠に収まっていても、この先は増える一方だと考えています。
E2E には、目的の違う2種類があると思っています。
- エンジニアの視点の E2E: 作ったものが壊れていないことを確かめる。機能を足したら、その分を足す
- QA の視点の E2E: アプリをいじめる。想定外の操作や組み合わせで、壊れる所を探す
後者は、必ずしもエンジニアが書かなくてよいはずです。書く人が増えれば、テストの数も CI の時間も増えます。単体テストも、機能が増えれば同じように増えます。
枠を節約する工夫(関係するテストだけ流す、重なった実行を止める など)もできます。ただ、テストが増えていく前提なら、上限そのものを外したくなります。
runner だけを外に出せる
GitHub Actions は「ワークフローの管理」と「ジョブを実際に動かす機械(runner)」が分かれています。runner だけを自分の機械に置くことができます(self-hosted runner)。
self-hosted runner で動いたジョブは、GitHub の枠を使いません。リポジトリも PR も Actions の画面も今のままで、ジョブが動く場所だけが変わります。
GitLab などへ移ることも考えました。ただ、自前のサーバーで動かして上限を外すことは GitHub のままでもできます。それなら、issue・PR・gh 前提の運用を作り直す理由がありません。
どうやって自分の機械と GitHub を繋ぐのか
最初に気になったのはここでした。答えは、機械の側から GitHub に取りに行くです。
- 機械に GitHub 公式の runner アプリを入れて、起動しておく
- runner は HTTPS で GitHub に「自分向けのジョブはあるか」と問い合わせ続ける
- ワークフローの
runs-onのラベルが runner のラベルと一致すると、GitHub がそのジョブを渡す - runner がコードを checkout して手順を実行し、ログと結果を GitHub に返す
通信は機械から外に出ていくものだけです。ポートを開ける必要も、固定 IP も要りません。社内ネットワークの内側の機械でも動きます。
登録は、リポジトリの Settings → Actions → Runners → New self-hosted runner に出るコマンドを流すだけです。
./config.sh --url https://github.com/<owner>/<repo> --token <TOKEN> --labels my-runner
./run.sh
ワークフローは runs-on を書き換えます。
jobs:
unit-tests:
runs-on: [self-hosted, linux, my-runner]
自分の PC か、専用の機械か
手元の Mac でも動かせます。ただし次の点が気になりました。
- スリープや電源オフの間は、CI が待ちになる(別の runner に自動では切り替わらない)
- PR のコードが自分の PC で、自分の権限で動く
- CI の間は手元の作業が重くなる。E2E は手元の開発サーバーとポートや DB を取り合う
私たちは、社内で常時動いている機械に置かせてもらうことにしました。runner はジョブごとに新しいコンテナで動かし、終わったら消します。CPU とメモリには上限を付け、その機械の本来の仕事に影響しないようにしています。
移してみて引っかかったこと
1. コンテナの中では、CPU が1つに見えていた
移した直後、単体テストの時間が GitHub の機械とほとんど変わりませんでした。手元では数十秒で終わるテストです。
調べると、コンテナの中で次のようになっていました。
nproc=16 availableParallelism=1
nproc は機械全体の16コアを返します。一方で Node.js の os.availableParallelism() は、コンテナに掛けた CPU の上限を見て 1 を返していました。Vitest は既定で「使えるコア数 − 1」本のワーカーを立てるため、テストが1本ずつしか動いていませんでした。
CI のときだけ並列数を決め打ちして解決しました。
// vitest.config.ts
export default defineConfig({
test: {
maxWorkers: process.env.CI ? 4 : undefined,
},
})
単体テストのジョブは 4.6分から 2分になりました。
ついでに、もともと書いていた poolOptions.threads の設定が効いていなかったことも分かりました。Vitest の既定の pool は forks なので、threads 向けの設定は読まれていませんでした。
2. E2E で、ブラウザの OS ライブラリが入れられない
Playwright の npx playwright install --with-deps chromium は、ブラウザが使う OS のライブラリを apt で入れます。そのために sudo で root になろうとします。runner のコンテナは安全のために root になれない設定にしてあるので、ここで止まりました。
sudo: The "no new privileges" flag is set, which prevents sudo from running as root.
--with-deps を外すとブラウザ本体は入りますが、起動できません。
error while loading shared libraries: libnspr4.so: cannot open shared object file
OS のライブラリは、ジョブの中ではなく runner のイメージに最初から入れておく必要があります。イメージを作るときに root で npx playwright install-deps chromium を流すか、Playwright 公式のイメージ(mcr.microsoft.com/playwright)を元にします。Playwright の版を上げたときは、イメージも同じ版で作り直します。
ワークフローは、GitHub の機械とどちらでも動くように分けておきました。
- name: Install Playwright browsers
run: npx playwright install ${{ runner.environment == 'github-hosted' && '--with-deps' || '' }} chromium
まとめ
- GitHub Actions の枠は、テストが増えるほど足りなくなる
- runner だけを自前の機械に移せば、GitHub の使い勝手はそのままで枠を使わなくなる
- 繋ぎ方は、機械から GitHub に問い合わせるだけ。ポートの開放は要らない
- コンテナで動かすときは、CPU の数の見え方と、OS ライブラリの入れ方に気を付ける