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 (continuous integration) を実行するメリットは 2 つあります。ビルド時間に対する従量課金がなくなります。また、ウォームビルドキャッシュやプライベートネットワークなど、そのサーバーからしか利用できないリソースにジョブからアクセスできます。代わりに、セキュリティ上のリスクがあります。runner は workflow ファイルの内容を、指定したユーザーとして実行します。そのため、workflow ファイルは設計上、リモートコード実行に相当します。プライベートリポジトリでは、信頼できるユーザーだけが workflow を追加できるため、問題ありません。パブリックリポジトリでは現実的なリスクがあります。fork からの pull request に関するセクションで、その仕組みを説明します。
以下の手順はすべて Ubuntu 24.04 と runner version 2.338.0 を前提としています。これは 12 October 2026 時点の現行リリースです。
開始する前に必要なもの
通常の管理者アカウントと 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.338.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(セキュアハッシュアルゴリズム、256 bit)は、2.338.0 x64 tarball 用です。GitHub では、リリースページと New self-hosted runner 画面に、現在のリリースの値が表示されます。値はバージョンごとに変わるため、別のバージョンをインストールする場合は、そこからコピーしてください。
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -cダウンロードが正常であれば、1 行表示されます。
actions-runner-linux-x64-2.338.0.tar.gz: OKファイルが途中で切れているか変更されている場合は、失敗メッセージと警告が表示されます。
actions-runner-linux-x64-2.338.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.338.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 で確認し、libicu が ldconfig で確認します。そのため、解決できないリンクがあるとスクリプトは停止し、後で分かりにくいクラッシュが発生することを防げます。
リポジトリに runner を登録する
リポジトリから token を取得します。Settings、Actions、Runners、New self-hosted runner の順に開きます。表示されたページに 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 は checkout 先のディレクトリを指定します。runner ディレクトリ内に作成されます。--unattended は対話形式の prompt に既定値で回答します。コマンドを script 内で実行する場合に適しています。--replace は、同じ名前の既存の登録を引き継ぎます。登録に失敗しないため、server を再構築する場合に適しています。
実行が成功すると、最後に次の行が表示されます。
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.登録情報は runner ディレクトリ内の .runner、.credentials、.credentials_rsaparams として保存されます。最後の 2 つは、この runner を GitHub に識別させる情報です。これらを読み取れるユーザーは runner になりすませます。そのため、ディレクトリの mode は 700 で、ユーザーには 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 の横に表示されるラベルとリストを比較してください。
自己ホスト型 runner とパブリックリポジトリを併用してはいけない理由
ここは見落とされがちな部分です。GitHub の指針は明確です。自己ホスト型 runner は「パブリックリポジトリではほぼ使用すべきではなく」、エフェメラルでクリーンな仮想マシン上で実行される保証がないため、「workflow 内の信頼できないコードによって永続的に侵害される可能性があります」。
仕組みは単純です。fork からの pull request には、workflow ファイルの独自のコピーが含まれます。パブリックリポジトリが runner 上で pull request の workflow を実行する場合、そのリポジトリを fork できる人なら誰でも、自分のコマンドを VPS 上で実行する workflow を提案できます。書き込み権限は必要ありません。提案したもの自体が実行されるためです。
承認設定によってリスクは軽減できますが、解消はできません。パブリックリポジトリのデフォルトポリシーでは、初回コントリビューターによる fork の workflow に対して、メンテナーの承認を求めます。GitHub が初回として扱うのは、その人の commit または pull request がリポジトリにマージされるまでです。その後の pull request は確認なしで実行されます。GitHub 自身も、単純な修正を 1 つマージするだけでこの状態に到達できると警告しています。つまり、ゲートは人間による差分の確認です。build script の 3 段階下に隠された payload は、簡単に見落とされます。
fork からの pull request には Secret は渡されず、その GITHUB_TOKEN は読み取り専用です。これにより GitHub 内での被害は制限されます。しかし、サーバーは保護されません。攻撃者は gharunner として shell を取得するため、そのユーザーが読み取れるすべてのファイルを読み取れます。また、VPS のプライベートネットワークから到達できる対象にアクセスし、~/.bashrc やユーザーの systemd unit に痕跡を残して、次の job 実行時に起動させることもできます。
--ephemeral に登録すると、runner は 1 job を受け付けた後に登録解除されます。そのため、ある job が次の job の workspace を読み取ることはできません。ただし、job ごとにマシンまたはコンテナを再構築する仕組みがある場合に限って有効です。runner ユーザーの home directory に書き込まれた backdoor は、新規登録を行っても残るためです。
続くルールは簡潔です。自己ホスト型 runner はプライベートリポジトリで使用してください。パブリックリポジトリに接続する必要がある場合は、fork の pull request をその runner で実行しないでください。そのサーバーでは他の処理を実行せず、マシンを使い捨てとして扱ってください。
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 は、デフォルトで自身を更新します。新しいリリースを検知すると、自身のファイルを置き換えてサービスを再起動するため、通常は何もする必要がありません。自動更新を無効にするには、runner の登録時に --disableupdate を ./config.sh に渡します。すでに登録済みの runner では、同じフラグを付けて再登録します。この設定だけで、特定のバージョンに永久に固定できるわけではありません。GitHub のドキュメントには、--disableupdate を設定した runner でも、新しいリリースから 30 日以内に手動で更新する必要があり、更新しなければ GitHub がその runner へのジョブのキュー投入を停止すると明記されています。
手動更新でも登録情報は保持されます。.runner と .credentials は tarball に含まれていないためです。サービスを停止し、新しい tarball を gharunner としてダウンロードして checksum を確認します。次に、tar xzf を使用して同じディレクトリに展開し、サービスを再度起動します。
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh startrunner を削除するには、まずサービスをアンインストールし、その後で登録を解除します。削除 token は、同じ 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 として表示され続けます。管理者が Force remove this runner をクリックするか、接続がない状態が 14 日間続いて GitHub が自動的に削除するまで、その状態は残ります。
障害時に表示されるメッセージ
Must not run with sudo。root で実行すると、config.sh はこのメッセージを表示して終了します。このチェックは意図的なものです。_work に root 所有のファイルがあると、サービスユーザーで実行する以降のジョブがすべて失敗するためです。./config.sh は gharunner として実行してください。RUNNER_ALLOW_RUNASROOT 変数でこのチェックを無効にできますが、問題が後で発生するだけです。
sudo: ./svc.sh: command not found。正しいディレクトリにいます。config.sh による登録が完了していないため、svc.sh はまだ存在しません。runner を登録してから、サービスをインストールしてください。
Http response code: NotFound from 'POST https://api.github.com/actions/runner-registration'。通常、登録トークンが有効ではありません。登録トークンの有効期限は 1 時間であるため期限切れになったか、Runners ページにある登録トークンの代わりに personal access token を貼り付けています。新しいトークンを生成して再度貼り付け、--url が完全なリポジトリ URL になっていることを確認してください。
Dependencies is missing for Dotnet Core 6.0。runner ディレクトリから sudo ./bin/installdependencies.sh を root として実行し、その後もう一度登録してください。
再起動後に Runner が Offline になる。systemctl is-enabled 'actions.runner.*' を実行してください。何も表示されない場合、./svc.sh install が一度も実行されていません。runner はターミナルセッション内でしか存在していなかったことになります。unit が有効で、それでも runner が Offline の場合は、journalctl -u 'actions.runner.*' を確認し、送信方向の HTTPS 接続を調べてください。
ディスクが満杯になる。チェックアウト、ビルドキャッシュ、Docker イメージが _work と runner ユーザーのホームディレクトリに蓄積します。自動的に削除されることはありません。du -sh /home/gharunner/actions-runner/_work を監視し、ディスク容量が枯渇する前に定期的なクリーンアップを追加してください。
FAQ
sudo ./svc.sh install で command not found と表示されるのはなぜですか?
svc.sh が runner tarball に含まれていないためです。./config.sh の登録が完了すると、リポジトリ名と runner 名からサービス名を組み立て、このスクリプトが runner ディレクトリ内に生成されます。まず runner ユーザーとして ./config.sh を実行してください。その後、sudo ./svc.sh install gharunner がスクリプトを見つけ、/etc/systemd/system 内に actions.runner.OWNER-REPO.RUNNER-NAME.service という unit を書き込みます。
self-hosted runner 用にファイアウォールのポートを開く必要はありますか?
いいえ。runner は GitHub への送信 HTTPS 接続を開き、ジョブを待機している間その接続を維持します。そのため、GitHub が VPS への接続を開始することはありません。送信方向の 443 を許可し、受信方向のルールは閉じたままにしてください。サービスが実行中なのに runner が Offline と表示される場合は、受信ルールではなく、送信方向のフィルタリングと DNS を確認してください。
公開リポジトリで self-hosted runner を使用できますか?
使用できますが、GitHub は推奨していません。fork からの pull request には独自の workflow ファイルが含まれるため、リポジトリを fork できる人は、あなたのマシン上で実行されるコマンドを提案できます。デフォルトの承認プロンプトが対象とするのは、リポジトリに commit または pull request をまだ一度もマージされたことがないコントリビューターだけです。公開リポジトリに runner を接続する場合は、その runner で fork の pull request workflow を無効にし、そのサーバーでは他の処理を実行せず、定期的にマシンを再構築してください。
Http response code: NotFound で登録に失敗するのはなぜですか?
認証情報が正しくない場合、登録リクエストは URL が正しくない場合に限らず NotFound を返すため、メッセージが分かりにくくなります。登録トークンは表示されてから 1 hour で期限切れになり、personal access token はこのリクエストでは使用できません。Settings、Actions、Runners、New self-hosted runner を再度開き、新しいトークンをコピーしてください。--url の値が、自分に admin 権限のあるリポジトリを指していることも確認してください。