Ubuntu 24.04 VPS 自架 GitHub Actions runner 設定
了解如何在 Ubuntu 24.04 VPS 註冊自架 GitHub Actions runner:建立專用使用者、驗證 checksum、執行 config.sh、設定 systemd 服務,並掌握 fork pull request 的安全風險。
自架 GitHub Actions runner 的功能
自架 GitHub Actions runner 是安裝在您自己的 VPS 上的程式,會向 GitHub 要求工作,並在您的硬體上執行這些工作。您可以將它註冊至單一儲存庫、將它安裝為 systemd 服務,並讓它在每次重新開機後自動恢復運作。GitHub 會排程工作。您的伺服器負責執行工作。
在您擁有的伺服器上執行 CI(持續整合)有兩個優點。建置分鐘數不再受計量限制,工作也能存取只有您的機器才有的資源,例如現成的建置快取或私有網路。代價是安全性。runner 會以您指定的使用者身分,執行工作流程檔案所指定的任何內容,因此工作流程檔案本質上就是遠端程式碼執行。在私有儲存庫中,這通常沒有問題,因為只有您信任的人員可以新增工作流程檔案。在公開儲存庫中,這會帶來實際風險;關於 fork 提取要求的章節會說明其運作機制。
以下內容以 Ubuntu 24.04 和 runner 版本 2.336.0 為準;截至 2026 年 7 月,這是目前的版本。
開始前的必要條件
請從具備一般管理員帳戶和 sudo 的 VPS 開始,狀態應如 新 VPS 的前 10 分鐘 所述。您不需要開放入站連接埠。runner 會向 GitHub 建立出站 HTTPS(安全超文字傳輸通訊協定)連線,並在等待工作時保持連線,因此 GitHub 不會連線到您的伺服器。您的防火牆可以維持對外封閉,工作仍會送達。
此外,您需要具有儲存庫的管理員權限,因為註冊權杖會顯示在儲存庫設定中。
為 runner 建立專用使用者
請勿以 root 或您自己的管理員使用者執行 runner。每個工作都會繼承 runner 使用者的權限,因此如果 runner 使用者可以使用 sudo,呼叫 sudo 的工作流程就能成功。請建立一個非特權使用者,除了自己的主目錄外不擁有任何資源。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(安全雜湊演算法,256 位元)值適用於 2.336.0 x64 tarball。GitHub 會在 release 頁面和 New self-hosted runner 畫面顯示目前 release 的值。每個版本的值都不同,因此安裝其他版本時,請從上述位置複製該值。
echo "04cf0be1aff4c3ec3554466c39124ca250e3effd8873bb7e8d68535aa9505d5d actions-runner-linux-x64-2.336.0.tar.gz" | sha256sum -c下載正確時,會輸出一行:
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 之後才發現問題。未完整寫入的 archive 會使 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 後建立」的 script,因為該檔案會根據範本產生,並將您的 repository 和 runner 名稱寫入 service name。因此,在執行 ./config.sh 前執行 sudo ./svc.sh install 會因 sudo: ./svc.sh: command not found 而失敗。請先註冊,再安裝 service。
安裝 runner 相依元件
runner 是 .NET 應用程式,因此需要一些共用程式庫。保留 runner 使用者的 shell,並使用 sudo 安裝這些元件,因為指令碼會寫入系統套件資料庫。
exit
cd /home/gharunner/actions-runner
sudo ./bin/installdependencies.sh在 Ubuntu 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
從存放庫取得權杖。依序開啟 Settings、Actions、Runners,然後選取 New self-hosted runner。頁面會顯示以 A 開頭的註冊權杖。權杖會在建立 1 小時後過期,因此請在準備貼上時再產生。
請以 runner 使用者身分註冊。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這些旗標的作用如下。--name 是 runner 在存放庫中顯示的名稱,請選擇 6 個月後仍能辨識的名稱。--labels 會加入自訂標籤;runner 不需額外設定就已具備 self-hosted、Linux 和 X64。--work 指定簽出的目錄位置,該目錄位於 runner 目錄內。--unattended 會使用互動式提示的預設值回答提示;當命令位於指令碼中時,這正是所需的行為。--replace 會接管相同名稱的現有註冊,而不是失敗;重建伺服器時需要這項行為。
成功執行後,結尾會顯示以下行:
√ Runner successfully added
√ Runner connection is good
√ Settings Saved.註冊資訊現在位於 runner 目錄中,檔名為 .runner、.credentials 和 .credentials_rsaparams。後兩者可用來向 GitHub 識別此 runner,因此任何能讀取這些檔案的人都能冒充此 runner。這就是目錄權限設為 mode 700,且該使用者沒有 sudo 權限的原因。
將 runner 安裝為 systemd 服務
在終端機中執行 ./run.sh 適合進行單次測試,但 SSH 工作階段結束後,該程序也會終止。請安裝服務,讓 runner 在開機時啟動。VPS 上的 systemd 服務與計時器說明 unit 檔案本身的內容。此處由 svc.sh 為您建立檔案。
exit
cd /home/gharunner/actions-runner
sudo ./svc.sh install gharunner
sudo ./svc.sh start
sudo ./svc.sh statussvc.sh 需要 root 權限,因為它會將 unit 寫入 /etc/systemd/system 並啟用該 unit。install 後面的引數是服務執行時所使用的使用者。請明確傳入 gharunner。若未提供引數,該 script 會改用 $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 上;只要多出一個字詞,工作就會持續排入佇列,且任何位置都不會顯示錯誤。請將該清單與 repository 設定中 runner 旁顯示的標籤進行比對。
自行代管的 runner 不適合搭配公開儲存庫
這是最容易被忽略的部分。GitHub 的指引說得很明確:自行代管的 runner「幾乎不應用於公開儲存庫」,而且這類 runner「不保證會在暫時性的乾淨虛擬機器中執行,也可能因工作流程中的不受信任程式碼而遭到持續入侵」。
其運作方式很簡單。來自 fork 的 pull request 會帶有該 fork 自己的工作流程檔案副本。如果您的公開儲存庫在自行代管的 runner 上執行 pull request 工作流程,任何能 fork 該儲存庫的人,都可以提交一個工作流程,讓其命令在您的 VPS 上執行。他們不需要寫入權限,因為他們提交的內容本身就是會執行的內容。
核准設定只能降低風險,無法解決問題。公開儲存庫的預設政策會要求維護者核准首次貢獻者的 fork 工作流程。您核准該使用者一次後,該使用者後續的 pull request 就會直接執行,不會再次提示。因此,防線是每次由人員閱讀差異內容,而建置指令碼中向下隱藏三層的有效負載很容易被忽略。
來自 fork 的 pull request 不會取得您的 secrets,而其 GITHUB_TOKEN 為唯讀。這只能限制 GitHub 內部的損害,無法保護您的伺服器。攻擊者取得的是 gharunner 的 shell,因此可以讀取該使用者有權讀取的所有檔案、連線至 VPS 在其私有網路上可連線的任何位置,並在 ~/.bashrc 或使用者的 systemd 單元中留下會在下一個工作期間執行的內容。
使用 --ephemeral 註冊後,runner 執行一個工作後就會取消註冊,因此單一工作無法讀取下一個工作的工作區。但只有在每個工作都會重新建立機器或容器時,這項措施才有幫助,因為寫入 runner 使用者主目錄的後門會在重新註冊後繼續存在。
接下來的規則很簡短。自行代管的 runner 僅用於私有儲存庫。如果您必須將 runner 連接至公開儲存庫,請勿在其上執行 fork 的 pull request,不要在該伺服器上放置其他內容,並將該機器視為可拋棄資源。
Docker 工作,以及實際上等同於 root 的群組
容器工作、服務容器,以及任何呼叫 docker build 的工作流程步驟,都需要 runner 主機上的 Docker daemon。依照一般方式安裝 Docker,VPS 上的 Docker 與 Docker Compose 涵蓋了相關步驟,然後將 runner 使用者加入 docker 群組。
執行前,請先了解其中的取捨。加入 docker 群組等同於擁有 root 權限,因為容器可以繫結掛載 /,並在容器內以 root 身分執行。因此,能夠與 Docker socket 通訊的工作流程,可以讀取及寫入 VPS 上的所有檔案,包括 /etc/shadow。對於擁有受信任貢獻者的私人 repository,這可能是可接受的代價。在其他環境中,這會使使用非特權使用者失去意義。Rootless Docker 會將容器建置限制在 runner 使用者本身的權限內,但代價是儲存驅動程式較慢,且無法使用特權容器。
更新,以及正確移除 runner
Self-hosted runner 預設會自行更新。它會偵測新版本、替換自身檔案並重新啟動服務,因此通常不需要執行任何操作。./config.sh --disableupdate 會在需要固定版本時停用自動更新。停用後,更新工作由您負責:GitHub 文件明確指出,使用 --disableupdate 設定的 runner 必須手動更新。
手動更新會保留註冊資訊,因為 tarball 中不包含 .runner 和 .credentials。先停止服務,將新 tarball 下載並計算其 checksum,存為 gharunner,再使用 tar xzf 將其解壓縮到相同目錄,最後重新啟動服務:
cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start若要移除 runner,請先解除安裝服務,再取消註冊。移除 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 仍會在 repository 中列為 Offline,因為 GitHub 只有在 runner 回報已移除,或管理員手動刪除該項目時,才會得知它已不存在。
失敗模式與您將看到的字串
Must not run with sudo。config.sh 以 root 執行時會列印此訊息並結束。此檢查是刻意設計的,因為 _work 中由 root 擁有的檔案會使之後以服務使用者身分執行的所有工作失敗。以 gharunner 身分執行 ./config.sh。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。它可能已過期,因為有效期限只有 1 小時;也可能是將 personal access token 貼上,取代了 Runners 頁面中的註冊 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、組建快取和 Docker 映像會累積在 _work 及 runner 使用者的家目錄中,而且不會自動清理。請監控 du -sh /home/gharunner/actions-runner/_work,並在磁碟自行決定前,加入排程清理工作。
FAQ
為什麼 sudo ./svc.sh install 會顯示 command not found?
因為 runner tarball 中沒有 svc.sh。./config.sh 完成註冊後,會在 runner 目錄中產生該檔案,並使用您的 repository 名稱和 runner 名稱建立服務名稱。請先以 runner 使用者執行 ./config.sh。之後,sudo ./svc.sh install gharunner 就能找到該指令碼,並在 /etc/systemd/system 中寫入名為 actions.runner.OWNER-REPO.RUNNER-NAME.service 的 unit。
自行裝載的 runner 需要開放防火牆連接埠嗎?
不需要。runner 會對 GitHub 建立輸出 HTTPS 連線,並在等待工作時保持連線,因此 GitHub 不會主動連線到您的 VPS。允許輸出 443,並維持關閉輸入規則。如果 runner 的服務正在執行,但狀態顯示 Offline,請檢查輸出流量過濾和 DNS,而不是輸入規則。
我可以在 public repository 上使用自行裝載的 runner 嗎?
可以,但 GitHub 不建議這麼做。來自 fork 的 pull request 會攜帶自己的 workflow 檔案,因此任何能 fork 您 repository 的人,都可以提出在您機器上執行的命令。核准提示只涵蓋貢獻者的第一次執行。如果您將 runner 連結至 public repository,請停用其 fork pull request workflows,不要在該伺服器上放置其他內容,並依排程重建該機器。
為什麼註冊會因 Http response code: NotFound 而失敗?
當認證資料錯誤時,註冊呼叫會回應 NotFound,而不只是在 URL 錯誤時回應,因此錯誤訊息容易誤導。註冊 token 會在顯示後一小時過期,而 personal access token 不接受用於此呼叫。請再次開啟 Settings、Actions、Runners、New self-hosted runner,複製新的 token,並確認 --url 值指向您具有管理員權限的 repository。