SSD Nodes Learn 8GB 内存 — 每年 $66
指南 Matt Connor作者: Matt Connor · 更新于 2026-08-01

Ubuntu 24.04 部署自托管 GitHub Actions Runner

在 Ubuntu 24.04 上部署 GitHub Actions Runner 2.336.0:创建专用用户、校验 tarball、运行 config.sh、配置 systemd,并说明 fork 拉取请求的安全风险。

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.336.0。该版本是截至 July 2026 的当前版本。

开始前需要准备的内容

从具有普通管理员账户和 sudo 权限的 VPS 开始,状态应达到新 VPS 的前 10 分钟所述的程度。您不需要开放入站端口。runner 会向 GitHub 建立出站 HTTPS(超文本传输协议安全)连接,并在等待任务时保持连接,因此 GitHub 从不会连接到您的服务器。您的防火墙可以继续对外关闭,任务仍会到达。

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

为运行程序创建专用用户

不要以 root 或您自己的管理员用户运行程序。每个作业都会继承运行程序用户的权限,因此,如果运行程序用户可以使用 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-runner

passwd -l 会锁定密码,因此无法使用密码以 gharunner 身份登录。运行程序目录必须使用 700 权限,因为运行程序会在其中以明文存储凭据,检出内容也可能包含私有源代码。

继续操作前,检查这两个属性:

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.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 -mx86_64 使用上面的 linux-x64 文件。aarch64 使用 actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz

现在验证下载的文件。下面的 SHA256(安全哈希算法,256 位)值对应 2.336.0 x64 tarball。GitHub 会在发布页面和 New self-hosted runner 页面显示当前版本的值。该值会随每个版本变化,因此安装其他版本时,请从这些页面复制对应值。

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 才发现问题。写入不完整的归档会导致 gzip: stdin: unexpected end of filetar: Unexpected EOF in archive 失败。这只能说明文件已损坏,不能说明文件是被截断还是被替换。

tar xzf ./actions-runner-linux-x64-2.336.0.tar.gz
ls

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

提取后,该目录包含 config.shrun.shenv.shsafe_sleep.shbin/externals/bin/ 包含 runner 二进制文件和 bin/installdependencies.shexternals/ 包含 JavaScript 操作所执行的内置 Node runtime。

此时还没有 svc.sh。GitHub 文档将其描述为“成功添加 runner 后创建的”脚本,因为该脚本基于模板生成,并将您的 repository 名称和 runner 名称写入 service 名称。因此,在 ./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-3zlib1gliblttng-ust1t64libssl3t64libicu74。脚本会为每个库尝试多个版本名称,并保留您的发行版提供的版本,因此同一个脚本也适用于旧版 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,因此链接无法解析时,脚本会停止,而不是稍后产生令人困惑的崩溃。

将运行器注册到您的存储库

从存储库获取令牌。依次打开 Settings、Actions、Runners,然后选择 New self-hosted runner。页面会显示一个以 A 开头的注册令牌。令牌在创建 1 小时后过期,因此请在准备粘贴时生成令牌。

以运行器用户身份注册。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 是运行器在存储库中显示的名称,因此请选择一个在 6 个月后仍能识别的名称。--labels 添加您自己的标签;运行器无需额外配置就已经带有 self-hostedLinuxX64--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 并启用该服务。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 的指导非常明确:自托管运行器“几乎不应当用于公共仓库”,而且它们“无法保证在临时的干净虚拟机中运行,并且可能被工作流中的不受信任代码持续入侵”。

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

审批设置只能缓解问题,不能解决问题。公共仓库的默认策略要求维护者审批首次贡献者的复刻工作流。你批准某人一次后,该用户之后的拉取请求就无需再次提示即可运行。因此,安全闸门就是每次由人工阅读差异内容,而隐藏在构建脚本下三层的有效负载很容易被忽略。

复刻仓库的拉取请求不会获得你的机密,其 GITHUB_TOKEN 也是只读的。这会限制 GitHub 内部的损害,但对你的服务器没有任何帮助。攻击者拥有 gharunner 的 shell 访问权限,因此可以读取该用户有权读取的所有文件,访问 VPS 在私有网络上能够访问的任何资源,并在 ~/.bashrc 中留下后门,或创建一个会在下次作业期间运行的用户 systemd 单元。

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

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

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

容器作业、服务容器,以及调用 docker build 的任何工作流步骤,都需要 runner 主机上运行 Docker 守护进程。按通常方式安装 Docker,VPS 上的 Docker 和 Docker Compose介绍了具体方法,然后将 runner 用户添加到 docker 组。

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

更新并正确移除 runner

自托管 runner 默认会自行更新。它会检测新版本,替换自身文件并重启服务,因此通常无需执行任何操作。./config.sh --disableupdate 会在需要固定版本时关闭自动更新。此后需要手动更新:GitHub 文档明确说明,配置了 --disableupdate 的 runner 必须手动更新。

手动更新会保留注册信息,因为 .runner.credentials 不包含在 tarball 中。停止服务,将新 tarball 下载并校验为 gharunner,使用 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,因为只有 runner 主动报告已移除,或管理员手动删除该条目时,GitHub 才会获知它已不存在。

失败模式及其对应的提示信息

Must not run with sudo。以 root 身份运行时,config.sh 会打印此信息并退出。此检查是有意设计的,因为 _work 中由 root 所有的文件会导致之后以服务用户身份运行的所有作业失败。以 gharunner 身份运行 ./config.shRUNNER_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'。该令牌不是有效的注册令牌。它可能已经过期,因为令牌有效期只有 1 小时;也可能是从 Runners 页面复制个人访问令牌时误替换了注册令牌。请生成新令牌并再次粘贴。

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?

因为 svc.sh 不在 runner tarball 中。./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,而不是入站规则。

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

可以,但 GitHub 不建议这样做。来自 fork 的 pull request 会携带其自身的 workflow 文件,因此任何能够 fork 您的 repository 的人,都可以提交在您机器上运行的命令。审批提示只涵盖贡献者的首次运行。如果将 runner 连接到公共 repository,请在其中禁用 fork pull request workflow,不要在该服务器上运行其他内容,并按计划重建该机器。

为什么使用 Http response code: NotFound 注册失败?

当凭据错误时,注册调用也会返回 NotFound,而不仅仅是在 URL 错误时返回,因此该消息容易误导。注册 token 会在显示 1 hour 后过期,并且此调用不接受 personal access token。请重新打开 Settings、Actions、Runners、New self-hosted runner,复制新的 token,并确认 --url 的值指向您拥有管理员权限的 repository。

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