Ubuntu 24.04でGitHub Actions runnerを自前運用
Ubuntu 24.04でセルフホスト型GitHub Actions runnerを登録する手順です。専用ユーザー、checksum、config.sh、systemdサービスの設定と、forkのpull requestが実行される危険を解説します。
セルフホスト型 GitHub Actions runner の動作
セルフホスト型 GitHub Actions runner は、自分の VPS にインストールして、GitHub にジョブを要求し、自分のハードウェア上でジョブを実行するプログラムです。1つのリポジトリに登録し、systemd サービスとしてインストールすると、再起動後も自動的に起動します。ジョブのスケジュールは GitHub が行います。処理はサーバーが実行します。
自分で所有するマシンで CI(継続的インテグレーション)を実行するメリットは2つあります。ビルド時間が従量課金の対象にならないことと、ウォームビルドキャッシュやプライベートネットワークなど、そのマシンからしかアクセスできないリソースにジョブがアクセスできることです。代わりに、セキュリティ上のリスクがあります。runner はワークフローファイルの内容を、指定したユーザーとして実行します。そのため、ワークフローファイルは設計上、リモートコード実行に相当します。プライベートリポジトリでは、信頼できるユーザーだけがワークフローファイルを追加できるため問題ありません。パブリックリポジトリでは実際のリスクになります。fork からの pull request に関するセクションで、その仕組みを説明します。
以下の内容はすべて Ubuntu 24.04 と runner version 2.336.0 を対象にしています。これは 2026年7月時点の現行リリースです。
開始する前に必要なもの
通常の管理者アカウントと sudo を持つ VPS を用意します。状態としては、新しい VPS で最初の10分間に行う設定まで完了している必要があります。受信ポートを開く必要はありません。runner は GitHub への送信 HTTPS(hypertext transfer protocol secure)接続を確立し、ジョブを待機している間その接続を維持します。そのため、GitHub がサーバーへ接続することはありません。ファイアウォールを外部に対して閉じたままでも、ジョブは到着します。
また、リポジトリの管理者権限も必要です。登録トークンはリポジトリの設定に表示されるためです。
runner専用ユーザーを作成する
runnerをrootや自分の管理者ユーザーとして実行しないでください。すべてのジョブはrunnerユーザーの権限を継承します。そのため、runnerユーザーがsudoを使用できる場合、sudoを呼び出すワークフローは成功します。自身のホームディレクトリ以外を所有しない、権限のないユーザーを1つ作成します。VPSでの最小権限ユーザーアカウントでは、一般的な方法を説明しています。ここでは、この用途に適した具体的な方法を示します。
sudo useradd -m -s /bin/bash gharunner
sudo passwd -l gharunner
sudo chmod 750 /home/gharunner
sudo install -d -m 700 -o gharunner -g gharunner /home/gharunner/actions-runnerpasswd -lはパスワードをロックします。そのため、誰もそのパスワードでgharunnerとしてログインできません。runnerディレクトリのモード700も重要です。runnerは認証情報をそのディレクトリに平文で保存し、checkoutに非公開ソースが含まれることがあるためです。
先に進む前に、両方の状態を確認します。
sudo passwd -S gharunner
sudo -l -U gharunnerpasswd -Sはgharunner Lで始まる行を出力します。Lは、パスワードがロックされていることを示します。sudo -l -U gharunnerの結果はis not allowed to run sudoになるはずです。許可されたコマンドの一覧が出力される場合、そのアカウントはsudoグループに所属しており、構築した分離が失われています。
runner をダウンロードして tarball を確認する
ここからは runner ユーザーとして作業します。
sudo -iu gharunner
cd ~/actions-runner
RUNNER_VERSION=2.336.0
curl -fL -o actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz \
"https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-x64-${RUNNER_VERSION}.tar.gz"アーキテクチャが不明な場合は、最初に uname -m を実行します。x86_64 は上記の linux-x64 ファイルを使用します。aarch64 は actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz を使用します。
次に、ダウンロードしたファイルを検証します。以下の SHA256(secure hash algorithm、256 bit)は、2.336.0 x64 tarball 用です。GitHub はリリースページと New self-hosted runner 画面に、現在のリリースの値を表示します。値はバージョンごとに変わるため、別のバージョンをインストールするときは、そこからコピーしてください。
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -cダウンロードが正常な場合は、次の1行が表示されます。
actions-runner-linux-x64-2.336.0.tar.gz: OKファイルが途中で切れているか変更されている場合は、失敗メッセージと警告が表示されます。
actions-runner-linux-x64-2.336.0.tar.gz: FAILED
sha256sum: WARNING: 1 computed checksum did NOT match確認を省略して、tar に問題を検出させないでください。途中までしか書き込まれていないアーカイブでは gzip: stdin: unexpected end of file と tar: Unexpected EOF in archive が発生します。これによりファイルが壊れていることは分かりますが、途中で切れたのか置き換えられたのかは分かりません。
tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
lstarballに含まれるものと、含まれないもの
展開後のディレクトリには config.sh、run.sh、env.sh、safe_sleep.sh、bin/、externals/ が含まれます。bin/ には runnerのバイナリと bin/installdependencies.sh が含まれます。externals/ には、JavaScript actionsの実行に使用される同梱の Node runtime が含まれます。
まだ svc.sh はありません。GitHubのドキュメントでは、これを「runnerの追加に成功した後に作成される」スクリプトと説明しています。これは、リポジトリ名と runner名が service名に組み込まれたテンプレートから作成されるためです。そのため、./config.sh の前に sudo ./svc.sh install を実行すると sudo: ./svc.sh: command not found で失敗します。先に登録してから、serviceをインストールしてください。
runnerの依存関係をインストールする
runnerは.NETアプリケーションなので、いくつかの共有ライブラリが必要です。runnerユーザーのシェルは変更せず、sudoでライブラリをインストールしてください。スクリプトがシステムのパッケージデータベースを書き換えるためです。
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.shUbuntu 24.04では、libkrb5-3、zlib1g、liblttng-ust1t64、libssl3t64、libicu74がインストールされます。スクリプトは各ライブラリについて複数のバージョン名を試し、使用中のリリースで提供されているものを選択します。そのため、同じスクリプトを古いUbuntuやDebianでも使用できます。
この手順を省略すると、./config.shは何も実行しないまま終了します。
Dependencies is missing for Dotnet Core 6.0
Execute sudo ./bin/installdependencies.sh to install any missing Dotnet Core 6.0 dependencies.libicuがない場合は、最初の行が異なるLibicu's dependencies is missing for Dotnet Core 6.0という同じ案内が表示されます。どちらも原因は同じです。config.shは起動前に同梱ライブラリに対してlddを実行するため、リンクを解決できないと、後で分かりにくいクラッシュを発生させずにスクリプトを停止します。
リポジトリに runner を登録する
リポジトリから token を取得します。Settings、Actions、Runners、New self-hosted runner の順に開きます。このページに、A で始まる registration token が表示されます。この token は作成から 1 時間で期限切れになるため、貼り付ける準備ができてから生成してください。
runner user として登録します。config.sh は sudo で実行できません。
sudo -iu gharunner
cd ~/actions-runner
./config.sh --url https://github.com/YOUR-USER/YOUR-REPO \
--token PASTE_REGISTRATION_TOKEN_HERE \
--name vps-runner-1 \
--labels vps \
--work _work \
--unattended \
--replace各 flag の動作は次のとおりです。--name はリポジトリ上での runner の表示名です。6 か月後にも識別できる名前を指定してください。--labels は独自の label を追加します。runner には、指定しなくても self-hosted、Linux、X64 が付与されます。--work は、runner directory 内で checkout の保存先となる directory を指定します。--unattended は対話型 prompt に既定値で回答します。コマンドを script 内に記述する場合は、この動作が適しています。--replace は、同じ名前の既存の登録を引き継ぎます。server を再構築する場合は、この動作が適しています。
成功すると、最後に次の行が表示されます。
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.登録情報は runner directory 内の .runner、.credentials、.credentials_rsaparams として保存されます。最後の 2 つは、この runner を GitHub に識別させる情報です。そのため、これらを読み取れるユーザーは runner になりすますことができます。これが、directory の mode を 700 にし、user に sudo を許可しない理由です。
runnerをsystemdサービスとしてインストールする
ターミナルで./run.shを実行すれば1回のテストには十分ですが、SSHセッションが終了するとrunnerも停止します。サービスをインストールして、起動時にrunnerが開始するようにします。VPS上のsystemdサービスとタイマーでは、unitファイル自体について説明しています。ここではsvc.shがunitファイルを作成します。
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.shは、/etc/systemd/systemにunitファイルを書き込み、サービスを有効化するため、root権限が必要です。installの後に指定する引数は、サービスを実行するユーザーです。gharunnerを明示的に渡してください。引数を指定しない場合、スクリプトは$SUDO_USERにフォールバックします。これは管理者アカウントであるため、すべてのジョブがsudoを使用できるユーザーとして実行されます。
unitの名前は、リポジトリとrunnerに基づき、actions.runner.YOUR-USER-YOUR-REPO.vps-runner-1.serviceの形式になります。これを手入力する必要はありません。
systemctl list-units 'actions.runner.*'
sudo journalctl -u 'actions.runner.*' -n 20 --no-pager正常なrunnerは√ Connected to GitHubをログに記録し、その後にListening for Jobsで終わる行を記録します。また、リポジトリのRunnersページではIdleと表示されます。Offlineと表示されるrunnerは、実行されていないか、ポート443でGitHubに接続できません。
runner にジョブを送信する
runs-on はラベルで runner を選択します。self-hosted と独自のラベルを指定してください。意図しない runner にジョブが割り当てられるのを防げます。
name: build
on:
push:
branches: [main]
jobs:
build:
runs-on: [self-hosted, linux, vps]
steps:
- uses: actions/checkout@v5
- run: uname -aジョブが Waiting for a runner to pick up this job で待機する場合、ラベルが一致していません。runs-on 内のすべてのラベルが runner に存在している必要があります。余分な単語が1つあるだけで、どこにもエラーが表示されないままジョブがキューに残ります。リポジトリ設定で runner の横に表示されるラベルとリストを比較してください。
セルフホストランナーとパブリックリポジトリを併用してはいけない理由
ここは見落とされがちな部分です。GitHubの案内は明確です。セルフホストランナーは「パブリックリポジトリではほとんど使用すべきではなく」、また「一時的でクリーンな仮想マシン上で実行される保証がなく、ワークフロー内の信頼できないコードによって永続的に侵害される可能性があります」。
仕組みは単純です。フォークからのプルリクエストには、ワークフローファイルの独自のコピーが含まれます。パブリックリポジトリでプルリクエストのワークフローを自分のランナー上で実行すると、リポジトリをフォークできる人なら誰でも、自分のコマンドをVPS上で実行するワークフローを提案できます。書き込みアクセスは必要ありません。提案した内容そのものが実行されるためです。
承認設定はこの問題を緩和しますが、解決はしません。パブリックリポジトリのデフォルトポリシーでは、初回コントリビューターによるフォークのワークフローについて、メンテナーの承認を求めます。その人を一度承認すると、後続のプルリクエストは新たな確認なしで実行されます。つまり、毎回差分を人間が確認することが防御策になります。しかし、ビルドスクリプトの3階層下に隠されたペイロードは見落としやすいものです。
フォークからのプルリクエストにはシークレットは渡されず、そのGITHUB_TOKENは読み取り専用です。これにより、GitHub内での被害は制限されます。しかし、サーバーは保護できません。攻撃者はgharunnerとしてシェルを取得できるため、そのユーザーが読み取れるすべてのファイルを読み取り、VPSがプライベートネットワーク経由で到達できるものにアクセスし、~/.bashrcやユーザーのsystemdユニットに次回のジョブ実行時に動作するものを残せます。
--ephemeralで登録すると、ランナーは1つのジョブを受け付けた後に登録解除されるため、あるジョブが次のジョブのワークスペースを読み取ることはできません。ただし、ジョブごとにマシンまたはコンテナを再構築する仕組みがある場合に限って有効です。ランナーユーザーのホームディレクトリに書き込まれたバックドアは、再登録だけでは残るためです。
以降のルールは簡潔です。セルフホストランナーはプライベートリポジトリで使用してください。パブリックリポジトリに接続する必要がある場合は、フォークからのプルリクエストをそのランナー上で実行しないでください。そのサーバーには他のものを置かず、マシンは破棄可能なものとして扱ってください。
Docker ジョブと、実質的に root と同等のグループ
コンテナジョブ、サービスコンテナ、および docker build を呼び出すワークフローステップには、runner ホスト上の Docker デーモンが必要です。通常の方法で Docker をインストールします。手順は VPS での Docker と Docker Compose で説明しています。次に、runner ユーザーを docker グループに追加します。
実行する前に、代償を理解してください。docker グループのメンバーになることは、root と同等です。コンテナは / を bind mount し、コンテナ内で root として実行できるためです。そのため、Docker ソケットにアクセスできるワークフローは、/etc/shadow を含め、VPS 上のすべてのファイルを読み書きできます。信頼できるコントリビューターだけが利用するプライベートリポジトリでは、許容できる場合があります。それ以外では、権限のないユーザーを使う意味がなくなります。Rootless Docker を使うと、コンテナのビルドを runner ユーザー自身の権限内に限定できます。ただし、ストレージドライバーが遅くなり、特権コンテナは使用できません。
更新と runner の完全な削除
self-hosted runner は、デフォルトで自動更新されます。新しいリリースを検知すると、自身のファイルを置き換えてサービスを再起動するため、通常は何もする必要がありません。./config.sh --disableupdate を指定すると、固定バージョンが必要な場合に自動更新を無効にできます。その後の更新は手動で行う必要があります。GitHub のドキュメントには、--disableupdate で構成した runner は手動で更新する必要があると明記されています。
手動更新でも登録情報は保持されます。.runner と .credentials は tarball に含まれていないためです。サービスを停止し、新しい tarball を gharunner としてダウンロードして checksum を確認します。次に、tar xzf を使用して同じディレクトリに展開し、サービスを再度起動します。
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startrunner を削除するには、まずサービスをアンインストールしてから登録を解除します。削除トークンは、同じ Runners ページで runner 自身の Remove ボタンの下に表示されます。
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh uninstall
sudo -iu gharunner
cd ~/actions-runner
./config.sh remove --token PASTE_REMOVAL_TOKEN_HERE登録を解除せずにディレクトリを削除すると、リポジトリには runner が Offline として残ります。GitHub が runner の削除を認識できるのは、runner 自身が削除を通知した場合、または管理者がエントリを手動で削除した場合だけです。
障害の原因と表示される文字列
Must not run with sudo。config.sh は root で実行すると、このメッセージを表示して終了します。このチェックは意図的なものです。_work にある root 所有のファイルが、サービスユーザーで後から実行されるすべてのジョブを失敗させるためです。./config.sh を gharunner として実行してください。RUNNER_ALLOW_RUNASROOT 変数でこのチェックを無効にできますが、問題が後で発生するだけです。
sudo: ./svc.sh: command not found。正しいディレクトリにいます。svc.sh はまだ存在しません。config.sh による登録が完了していないためです。runner を登録してから、サービスをインストールしてください。
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'。この token は有効な登録 token ではありません。登録 token の有効期限は1時間のため期限切れになったか、Runners ページにある登録 token の代わりに personal access token を貼り付けています。新しい token を生成して、もう一度貼り付けてください。
Dependencies is missing for Dotnet Core 6.0。root として runner ディレクトリから sudo ./bin/installdependencies.sh を実行してから、もう一度登録してください。
再起動後に Runner Offline になる。systemctl is-enabled 'actions.runner.*' を実行してください。何も表示されない場合、./svc.sh install が実行されていません。そのため、runner は端末のセッション内にしか存在していません。unit が有効で、runner がまだ Offline の場合は、journalctl -u 'actions.runner.*' を確認し、送信方向の HTTPS 接続を確認してください。
ディスクがいっぱいになる。checkout、build cache、Docker image が _work と runner ユーザーの home ディレクトリに蓄積します。自動的に削除されることはありません。du -sh /home/gharunner/actions-runner/_work を監視し、ディスクがいっぱいになる前に定期的なクリーンアップを追加してください。
FAQ
sudo ./svc.sh install で command not found と表示されるのはなぜですか?
svc.sh は runner tarball に含まれていないためです。./config.sh の登録が完了すると、リポジトリ名と runner 名からサービス名を作成し、runner directory に生成されます。まず runner user として ./config.sh を実行してください。その後、sudo ./svc.sh install gharunner がスクリプトを見つけ、/etc/systemd/system に actions.runner.OWNER-REPO.RUNNER-NAME.service という名前の unit を書き込みます。
self-hosted runner で firewall port を開く必要はありますか?
いいえ。runner は GitHub への outbound HTTPS connection を開き、job を待機している間その接続を維持します。そのため、GitHub が VPS への接続を開始することはありません。outbound 443 を許可し、inbound rule は閉じたままにしてください。service が実行中なのに runner が Offline と表示される場合は、inbound rule ではなく outbound filtering と DNS を確認してください。
public repository で self-hosted runner を使用できますか?
使用できますが、GitHub は推奨していません。fork からの pull request には独自の workflow file が含まれるため、リポジトリを fork できる人なら、マシン上で実行される command を提案できます。承認 prompt の対象は、contributor による最初の実行だけです。public repository に runner を接続する場合は、fork pull request workflow を無効にし、その server には他のものを置かず、定期的にマシンを再構築してください。
Http response code: NotFound で registration に失敗するのはなぜですか?
credential が誤っている場合、URL が誤っている場合だけでなく、registration call も NotFound を返すためです。そのため、message が分かりにくくなります。registration token は表示されてから one hour で期限切れになります。また、personal access token はこの call では受け付けられません。Settings、Actions、Runners、New self-hosted runner を再度開き、新しい token をコピーして、--url の値が管理者権限を持つ repository を指していることを確認してください。