SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor · 更新于 2026-10-13

Ubuntu 24.04 部署自托管 GitHub Actions Runner

在 Ubuntu 24.04 上注册自托管 GitHub Actions runner,涵盖专用用户、校验和、config.sh、systemd 服务及 fork pull request 执行工作流的安全风险。

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, July 30, 2026.

自托管 GitHub Actions runner 的作用

自托管 GitHub Actions runner 是安装在您自己的 VPS 上的程序。它向 GitHub 请求作业,并在您的硬件上运行这些作业。您可以将它注册到一个仓库,将其安装为 systemd 服务,并使其在每次重启后自动恢复。GitHub 负责调度作业。您的服务器负责执行作业。

在您自己的服务器上运行 CI(持续集成)有两个好处。构建时间不再按量计费。作业还可以访问只有您的机器具备的资源,例如预热的构建缓存或私有网络。代价是安全风险。runner 会以您为其指定的用户身份,执行工作流文件中的所有操作,因此工作流文件从设计上就是远程代码执行。在私有仓库中,这通常没有问题,因为只有您信任的人才能添加工作流。在公共仓库中,这会带来实际风险;“来自 fork 的拉取请求”一节会解释其工作机制。

以下内容基于 Ubuntu 24.04 和 runner 版本 2.338.0,该版本截至 12 October 2026 为当前发行版。

开始前的准备

从一台配有普通管理员账户和 sudo 的 VPS 开始,状态应达到新 VPS 上线后的前十分钟所述的阶段。无需开放入站端口。runner 会向 GitHub 建立出站 HTTPS(超文本传输协议安全版)连接,并在等待任务时保持连接。GitHub 不会连接到您的服务器。因此,防火墙可以继续对外关闭,任务仍然能够到达。

您还需要拥有仓库的管理员权限,因为注册令牌显示在仓库设置中。

为 runner 创建专用用户

不要以 root 或您自己的管理员用户运行 runner。每个作业都会继承 runner 用户的权限,因此,如果 runner 用户可以使用 sudo,调用 sudo 的工作流就会成功。创建一个非特权用户,使其除了自己的主目录外不拥有任何内容。VPS 上的最小权限用户账户介绍了通用做法。下面是针对 runner 的具体配置。

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-runner

passwd -l 会锁定密码,因此无法使用该密码以 gharunner 身份登录。runner 目录的 700 权限很重要,因为 runner 会在其中以明文存储凭据,而 checkout 目录中可能包含私有源代码。

继续之前,请检查这两个属性:

sudo passwd -S gharunner
sudo -l -U gharunner

passwd -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
ls

tarball 包含的内容,以及不包含的内容

解压后,目录中包含 config.sh、run.sh、env.sh、safe_sleep.sh、bin/ 和 externals/。bin/ 包含运行程序二进制文件和 bin/installdependencies.sh。externals/ 包含 JavaScript action 执行所使用的内置 Node 运行时。

此时还没有 svc.sh。GitHub 文档将其描述为“成功添加 runner 后创建的”脚本,因为该脚本根据模板生成,并将您的仓库名称和 runner 名称写入服务名称。因此,在 ./config.sh 之前执行 sudo ./svc.sh install 会因 sudo: ./svc.sh: command not found 失败。请先注册 runner,再安装服务。

安装 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 检查捆绑库,libicu 会使用 ldconfig 检查。因此,无法解析的链接会使脚本停止,而不是稍后产生难以判断原因的崩溃。

注册运行器

从仓库获取令牌。依次打开 Settings、Actions、Runners,然后选择 New self-hosted 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决定运行器在仓库中的显示名称,因此请选择一个六个月后仍能识别的名称。--labels添加自定义标签;运行器无需额外配置就已带有 self-hosted、Linux 和 X64。--work指定检出内容所在的目录,该目录位于运行器目录中。--unattended使用默认值回答交互式提示;当命令位于脚本中时,应使用此选项。--replace接管同名的现有注册,而不是直接失败;重建服务器时应使用此选项。

运行成功后,末尾会显示以下几行:

√ Runner successfully added
√ Runner connection is good
√ Settings Saved.

注册信息现在存储在运行器目录中,文件为 .runner、.credentials 和 .credentials_rsaparams。后两个文件用于向 GitHub 标识此运行器,因此任何能够读取它们的人都可以冒充该运行器。这就是目录权限设为 700 且该用户没有 sudo 权限的原因。

将 runner 安装为 systemd 服务

在终端中运行 ./run.sh 适合进行一次测试,但它会随 SSH 会话结束而退出。安装该服务,使 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 status

svc.sh 需要 root 权限,因为它会将 unit 写入 /etc/systemd/system 并启用该 unit。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 上,因此多出一个单词也会导致作业持续排队,而且不会在任何位置报错。请将该列表与仓库设置中 runner 旁显示的标签进行对比。

为什么自托管运行器不能与公共仓库混用

这是最容易被忽略的部分。GitHub 的指导非常明确:自托管运行器“几乎不应当用于公共仓库”,而且这类运行器“无法保证在临时的干净虚拟机中运行,并且可能被工作流中的不受信任代码持久性入侵”。

原理很简单。来自 fork 的拉取请求会携带自己的工作流文件副本。如果公共仓库在你的运行器上执行拉取请求工作流,那么任何能 fork 该仓库的人,都可以提交一个工作流,让其命令在你的 VPS 上运行。他们不需要写入权限,因为他们提交的内容本身就是将要运行的内容。

审批设置只能缓解问题,无法解决问题。公共仓库的默认策略要求维护者批准首次贡献者的 fork 工作流。在该贡献者的任意提交或拉取请求被合并到仓库之前,GitHub 才会将其视为首次贡献者。之后,他们后续的拉取请求无需再次提示即可运行。GitHub 也明确警告,只需让一个看似无害的修复被合并,就足以通过这一限制。因此,这道门槛只是由人查看差异,而隐藏在构建脚本多层调用中的恶意载荷很容易被遗漏。

来自 fork 的拉取请求不会获得你的机密,其 GITHUB_TOKEN 也处于只读状态。这可以限制 GitHub 内部的损害,但对你的服务器没有作用。攻击者拥有 gharunner 的 shell,因此可以读取该用户有权读取的所有文件,访问 VPS 在私有网络上能够访问的任何资源,并将持久化内容写入 ~/.bashrc,或写入一个会在下一次作业期间运行的用户级 systemd 单元。

使用 --ephemeral 注册后,运行器会接受一个作业,然后取消注册,因此单个作业无法读取下一个作业的工作区。只有在每个作业都重新构建机器或容器时,这种方式才有帮助,因为写入运行器用户主目录的后门会在重新注册后继续存在。

后续规则很简单。将自托管运行器用于私有仓库。如果必须将其连接到公共仓库,不要在其上运行 fork 的拉取请求,不要在该服务器上运行其他服务,并将这台机器视为一次性资源。

Docker 作业,以及实际上等同于 root 的组

容器作业、服务容器以及调用 docker build 的任何工作流步骤,都需要运行器主机上的 Docker daemon。按常规方式安装 Docker,具体步骤参见 在 VPS 上安装 Docker 和 Docker Compose,然后将运行器用户加入 docker 组。

执行此操作前,请了解其中的权衡。加入 docker 组等同于获得 root 权限,因为容器可以绑定挂载 /,并在容器内以 root 身份运行。因此,能够访问 Docker socket 的工作流可以读写 VPS 上的所有文件,包括 /etc/shadow。对于贡献者可信的私有仓库,这种风险可能可以接受。在其他场景中,这会使非特权用户失去实际意义。Rootless Docker 会将容器构建限制在运行器用户自身的权限范围内,但代价是存储驱动速度较慢,并且不支持特权容器。

更新并彻底移除 runner

Self-hosted runner 默认会自动更新。它会检测新版本,替换自身文件并重启服务,因此通常无需执行任何操作。要关闭自动更新,请在注册 runner 时向 ./config.sh 传入 --disableupdate;对于已经注册的 runner,则需要使用该标志重新注册。这样并不能永久停留在某个版本。GitHub 文档明确说明,配置了 --disableupdate 的 runner 仍必须在新版本发布后的 30 天内手动更新,否则 GitHub 会停止向其排队分配作业。

手动更新会保留注册信息,因为 .runner 和 .credentials 不在 tarball 中。停止服务,按 gharunner 下载新 tarball 并校验其校验和,使用 tar xzf 将其解压覆盖到同一目录,然后重新启动服务:

cd /home/gharunner/actions-runner
sudo ./svc.sh stop
sudo ./svc.sh start

要移除 runner,请先卸载服务,然后取消注册。移除令牌位于同一个 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,或者 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'。通常是因为令牌不是有效的注册令牌。注册令牌的有效期只有 one hour,因此可能已经过期;也可能是将个人访问令牌粘贴成了 Runners 页面中的注册令牌。生成新的令牌并重新粘贴,同时确认 --url 是完整的仓库 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 只存在于当前终端会话中。如果该单元已启用但 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?

因为 runner 压缩包中没有 svc.sh。./config.sh 完成注册后,会在 runner 目录中生成该文件,并使用您的仓库名称和 runner 名称生成服务名称。请先以 runner 用户运行 ./config.sh。之后,sudo ./svc.sh install gharunner 就能找到该脚本,并在 /etc/systemd/system 中写入名为 actions.runner.OWNER-REPO.RUNNER-NAME.service 的单元。

自托管 runner 需要开放防火墙端口吗?

不需要。runner 会向 GitHub 建立出站 HTTPS 连接,并在等待作业时保持连接,因此 GitHub 不会主动连接您的 VPS。允许出站 443 端口,并保持入站规则关闭。如果 runner 的服务正在运行但显示 Offline,请检查出站过滤和 DNS,而不是入站规则。

可以在公共仓库上使用自托管 runner 吗?

可以,但 GitHub 不建议这样做。来自 fork 的拉取请求会携带其自身的工作流文件,因此任何能够 fork 您仓库的人都可以提交在您机器上运行的命令。默认的审批提示仅适用于从未有提交或拉取请求合并到该仓库的贡献者。如果将 runner 连接到公共仓库,请在该仓库中禁用 fork 拉取请求工作流,不要在该服务器上运行其他内容,并按计划重建该机器。

为什么注册会因 Http response code: NotFound 失败?

凭据错误时,注册调用也会返回 NotFound,而不只是在 URL 错误时返回,因此该消息容易误导。注册令牌在显示 1 小时后过期,个人访问令牌不能用于此调用。再次打开 Settings、Actions、Runners、New self-hosted runner,复制新的令牌,并确认 --url 值指向您拥有管理员权限的仓库。

这篇文章对您有帮助吗?

#github-actions#ci#self-hosted#runner#ubuntu-24-04