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 取得工作,並在你的硬體上執行。你可以將它註冊到單一 repository、安裝為 systemd 服務,並讓它在每次重新開機後自動恢復執行。GitHub 負責排程工作。你的伺服器負責執行工作。
在自有伺服器上執行 CI(continuous integration)有兩個主要優點。建置時間不再按用量計費,而且工作可以存取只有你的機器具備的資源,例如已預熱的建置快取或私有網路。代價是安全性風險。runner 會以你指定的使用者身分,執行 workflow 檔案所指定的任何內容。因此,workflow 檔案本質上就是刻意設計的遠端程式碼執行。在 private repository 中,這通常沒有問題,因為只有你信任的人可以新增 workflow。在 public repository 中,這會構成實際風險;fork pull request 一節會說明其運作機制。
以下內容以 Ubuntu 24.04 和 runner 版本 2.338.0 為例;截至 12 October 2026,這是目前的 release。
開始前的必要條件
請從具備一般管理員帳戶和 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.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 位元)是 2.338.0 x64 tarball 的值。GitHub 會在版本發布頁面及 New self-hosted runner 畫面顯示目前版本的值。每個版本的值都不同,因此安裝其他版本時,請從上述位置複製對應的值。
echo "af4b794c1bc41d73d40535e3fe092a39f9679cd8d965954c2aca25a05ca41d32 actions-runner-linux-x64-2.338.0.tar.gz" | sha256sum -c下載成功時會輸出一行:
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 後建立」的 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 檢查內含的程式庫,並使用 ldconfig 檢查 libicu,因此無法解析的連結會讓腳本停止,而不是稍後產生難以判讀的當機。
向存放庫註冊 runner
從存放庫取得 token。依序開啟 Settings、Actions、Runners,然後選取 New self-hosted runner。頁面會顯示 registration token。token 建立 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 指定 checkout 所放置的目錄,位置在 runner 目錄內。--unattended 會以預設值回答互動式提示;當命令位於 script 中時,這正是需要的行為。--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 會攜帶自己的工作流程檔案副本。如果公開儲存庫在你的 runner 上執行 pull request 工作流程,任何能 fork 該儲存庫的人,都可以提交一個讓其命令在你的 VPS 上執行的工作流程。他們不需要寫入權限,因為他們提交的內容本身就是會執行的內容。
核准設定只能降低風險,無法解決問題。公開儲存庫的預設政策會要求維護者核准首次貢獻者所提交的 fork 工作流程。GitHub 只會在該使用者的任何 commit 或 pull request 尚未合併至儲存庫前,將其視為首次貢獻者。之後,該使用者提交的後續 pull request 會直接執行,不再顯示提示;GitHub 也明確警告,只要有一個微小的修正被合併,就足以通過這道門檻。因此,這道防線依賴人工閱讀差異內容,而藏在建置指令碼下第三層的惡意程式碼很容易被忽略。
來自 fork 的 pull request 不會取得你的 secrets,其 GITHUB_TOKEN 具有唯讀權限。這只能限制 GitHub 內部的損害,無法保護你的伺服器。攻擊者會以 gharunner 身分取得 shell,因此可以讀取該使用者有權讀取的所有檔案、連線至 VPS 私有網路可到達的任何位置,並將持久化內容寫入 ~/.bashrc,或建立在下一個工作執行期間啟動的使用者 systemd unit。
使用 --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 預設會自行更新。它偵測到新版本後,會替換自身檔案並重新啟動服務,因此通常不需要手動處理。若要停用自動更新,請在註冊 runner 時將 --disableupdate 傳給 ./config.sh;對於已完成註冊的 runner,必須使用該 flag 重新註冊。這不代表可以永久停留在某個版本。GitHub 文件明確說明,設定 --disableupdate 的 runner 仍須手動更新,且必須在新版本發布後 30 天內完成,否則 GitHub 會停止將工作排入該 runner。
手動更新會保留註冊資訊,因為 .runner 與 .credentials 不在 tarball 中。先停止服務,以下列方式下載並驗證新 tarball: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 繼續以 Offline 狀態列在 repository 中。除非管理員按一下 Force remove this runner,或 GitHub 在沒有連線 14 天後自動移除,否則它會一直保留。
失敗情況與畫面中會看到的字串
Must not run with sudo。以 root 執行時,config.sh 會顯示這段訊息後結束。這項檢查是刻意設計的,因為 _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。註冊 token 的有效時間只有 one hour,因此可能已過期;也可能是從 Runners 頁面複製個人存取 token,誤貼到註冊 token 的位置。請產生新的 token 並重新貼上,並確認 --url 是完整的 repository URL。
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 使用者的 home 目錄中,而且不會自動清理。請監控 du -sh /home/gharunner/actions-runner/_work,並在磁碟自行決定之前加入排程清理工作。
FAQ
為什麼 sudo ./svc.sh install 會顯示 command not found?
因為 svc.sh 不在 runner tarball 中。./config.sh 完成註冊後,會在 runner 目錄中產生此檔案,並使用您的 repository 名稱與 runner 名稱組合服務名稱。請先以 runner 使用者執行 ./config.sh。之後,sudo ./svc.sh install gharunner 就能找到該 script,並在 /etc/systemd/system 中寫入名為 actions.runner.OWNER-REPO.RUNNER-NAME.service 的 unit。
自架 runner 需要開放防火牆埠嗎?
不需要。runner 會對 GitHub 建立對外 HTTPS 連線,並在等待工作時維持連線,因此 GitHub 不會主動連線到您的 VPS。允許對外連線到 443,並維持關閉對外入站規則。若 runner 的服務正在執行,但狀態顯示 Offline,請檢查對外流量過濾與 DNS,而不是入站規則。
可以在公開 repository 上使用自架 runner 嗎?
可以,但 GitHub 不建議這麼做。來自 fork 的 pull request 會攜帶自己的 workflow 檔案,因此任何能 fork 您 repository 的人,都可以提交會在您的機器上執行的命令。預設核准提示只涵蓋從未有 commit 或 pull request 合併至該 repository 的貢獻者。若您將 runner 連結至公開 repository,請在該 repository 上停用 fork pull request workflows,伺服器上不要執行其他服務,並依排程重建該機器。
為什麼註冊會因 Http response code: NotFound 而失敗?
當憑證錯誤時,註冊呼叫會回應 NotFound,不只在 URL 錯誤時如此,因此這項訊息容易造成誤解。註冊 token 會在顯示 1 hour 後過期,而且此呼叫不接受 personal access token。請再次開啟 Settings、Actions、Runners、New self-hosted runner,複製新的 token,並確認 --url 值指向您具備管理員權限的 repository。