GitHub Actions 自托管 Runner 运维实战
GitHub 官方 Runner 有两个硬伤:排队要等、访问不了内网依赖与私有镜像仓库。自托管 Runner 把执行环境放在自己的机器上,构建直接访问内网服务、缓存可以预热、并发可以自己控制,是很多团队的必选项。本文讲 Runner 的部署、隔离、缓存与弹性伸缩,以及最容易被忽视的安全问题。
先说安全边界:Runner 所在的机器能拿到仓库的 secret,Runner 被攻破等于仓库密钥泄露。所以 Runner 必须做隔离:构建任务不要跑在装有生产密钥的机器上,Runner 进程用专用账号运行,构建产物与宿主机文件系统要隔离。
一、部署 Runner:注册与运行
Runner 的注册分两步:先在仓库或组织页面生成注册 token,再在目标机器上下载 Runner 包、执行 config 脚本。token 只有一次有效,注册完就失效,不要把它写进脚本或版本库。
<pre class="wp-block-syntaxhighlighter-code"># 创建专用账号(不要用 root 跑 Runner)
useradd -m -s /bin/bash gh-runner
mkdir -p /opt/actions-runner && chown gh-runner:gh-runner /opt/actions-runner
su - gh-runner
cd /opt/actions-runner
# 下载 Runner 包(版本号以 GitHub 官方最新为准)
curl -o actions-runner-linux-x64.tar.gz -L https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-linux-x64-2.321.0.tar.gz
tar xzf actions-runner-linux-x64.tar.gz
# 注册(URL 与 token 从仓库 Settings → Actions → Runners 获取)
./config.sh --url https://github.com/<org>/<repo> --token <registration-token> --labels linux,selfhosted,build
# 以服务方式运行(systemd)
sudo ./svc.sh install gh-runner
sudo ./svc.sh start
sudo ./svc.sh status</pre>
Runner 的标签(labels)是调度依据:工作流里 runs-on: [self-hosted, linux, build] 才会调度到这台机器。标签要按用途分:构建、测试、部署各用不同的标签与 Runner 组,避免部署任务跑到构建机上。
二、隔离:容器化 Runner 与最小权限
Runner 直接跑在宿主机上时,构建脚本能访问宿主机的所有文件与网络,风险很高。推荐做法:Runner 进程跑在宿主机、构建步骤跑在容器里(container 关键字),每个 Job 用完即销毁容器,宿主机只留 Runner 进程。
# 工作流示例:构建步骤跑在容器里,宿主机只做调度
jobs:
build:
runs-on: [self-hosted, linux, build]
container:
image: golang:1.22
volumes:
- /opt/build-cache:/cache
steps:
- uses: actions/checkout@v4
- name: Build
run: go build -o app ./...
另外三项隔离措施:Runner 账号只给必要的 sudo 权限(最好不给)、构建缓存目录与宿主机敏感目录分开、Runner 机器不要与生产服务混部。组织级 Runner 要配 Runner Group,按仓库分配权限,不要让所有仓库共用一个 Runner 池。
三、缓存预热与构建提速
自托管 Runner 的最大优势是缓存可以常驻:依赖包、Docker 镜像层、构建产物都可以预热在 Runner 机器上,Job 一来就能用,不用每次从零下载。缓存策略分三层:依赖缓存、镜像缓存、构建缓存。
# 依赖缓存:用 actions/cache,按 lock 文件做 key
# 工作流片段(YAML)
# - uses: actions/cache@v4
# with:
# path: ~/.cache/go-build
# key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
# 镜像缓存:Runner 机器上预热常用基础镜像
docker pull golang:1.22
docker pull node:20
docker pull maven:3.9-eclipse-temurin-17
# 定期清理无用镜像,避免磁盘被打满
docker image prune -a -f --filter "until=720h"
df -h /var/lib/docker | tail -1
缓存 key 要按 lock 文件的哈希生成,lock 文件变了缓存自动失效。缓存目录要定期清理,Runner 机器的磁盘监控要单独做,缓存把盘打满会导致所有 Job 失败。
四、弹性伸缩:按队列长度自动扩缩 Runner
固定数量的 Runner 在业务高峰会排队、低峰又浪费。弹性伸缩的思路:监控待处理 Job 的队列长度,超过阈值自动加 Runner、空闲时自动缩。GitHub 官方的 Actions Runner Controller(ARC)是 K8s 上的标准方案,虚拟机上可以用脚本 + 云 API 实现。
<pre class="wp-block-syntaxhighlighter-code"># 看 Runner 队列与状态(组织级)
# GitHub API:列出 Runner 与待处理 Job 数
curl -H "Authorization: token <PAT>" \
https://api.github.com/orgs/<org>/actions/runners
# ARC 扩缩容配置要点(K8s 部署时):
# - minRunners: 1(保底一台,避免冷启动)
# - maxRunners: 10(按预算与并发上限设)
# - 扩容触发:待处理 Job 数 > 0 且持续 60 秒
kubectl -n actions-runner-system get pods -l app.kubernetes.io/name=gha-runner-scale-set</pre>
五、Runner 日常运维:升级、磁盘与健康检查
Runner 机器是无人值守的,磁盘被缓存打满、Runner 版本落后、服务悄悄停掉,都是常见的静默故障。日常运维要做三件事:定期清理构建缓存与无用镜像、确认 Runner 服务在运行且版本是最新的、监控磁盘与队列长度。把这三项写进巡检脚本,比出了故障再去查要省事得多。
<pre class="wp-block-syntaxhighlighter-code"># 看 Runner 服务状态与版本
sudo ./svc.sh status
cat /opt/actions-runner/.runner 2>/dev/null | head -5
# 磁盘巡检:缓存目录与 Docker 数据目录是重灾区
df -h /opt/actions-runner /var/lib/docker
du -sh /opt/actions-runner/_work/* 2>/dev/null | sort -rh | head -10
# 清理:删掉 7 天前的构建工作目录与无用镜像
find /opt/actions-runner/_work -maxdepth 1 -type d -mtime +7 -exec rm -rf {} +
docker image prune -a -f --filter "until=720h"
docker builder prune -f --filter "until=720h"
# Runner 更新:官方会自动更新,失败时先看磁盘与网络
# 更新失败的常见原因:磁盘满、到 github.com 的网络不通
curl -sI https://github.com | head -1
# 健康检查脚本思路:服务状态 + 磁盘余量 + 队列长度,异常就告警
# systemctl is-active actions.runner.<org>-<repo>.service || echo "Runner 服务异常"</pre>
缓存清理要留安全边界:不要删正在运行的 Job 的工作目录,清理前先看有没有 Job 在跑。Runner 机器的磁盘告警阈值建议设为 80%,留出缓冲时间。版本更新不要手动降级,降级后 Runner 可能无法注册到组织。
六、组织级管理:Runner Group 与并发控制
Runner 池变大之后,管理重点就从“单台 Runner 怎么配”变成“谁能用哪个池、并发怎么控”。Runner Group 可以把 Runner 按用途分组,再按仓库分配访问权限;并发控制则防止某个仓库的 Job 把整个池占满。组织级 Runner 要配专人负责权限,仓库级 Runner 由各仓库自己管,两层不要混在一起。
<pre class="wp-block-syntaxhighlighter-code"># 看组织级 Runner 列表与状态(需要有权限的 PAT)
curl -s -H "Authorization: token <PAT>" \
https://api.github.com/orgs/<org>/actions/runners | grep -E '"name"|"status"|"busy"'
# 看 Runner Group(组织 Settings → Actions → Runner groups 里配置)
curl -s -H "Authorization: token <PAT>" \
https://api.github.com/orgs/<org>/actions/runner-groups
# 并发控制:工作流里用 concurrency 限制同类 Job 并发
# jobs:
# deploy:
# runs-on: [self-hosted, linux, deploy]
# concurrency:
# group: deploy-prod
# cancel-in-progress: false
# 下线异常 Runner:先在组织页面点 Remove,再在机器上卸载服务
# sudo ./svc.sh stop && sudo ./svc.sh uninstall
# ./config.sh remove --token <removal-token></pre>
Runner Group 的权限分配要遵循最小权限:部署组只给需要部署的仓库,构建组不要挂到生产部署的仓库上。PAT 的权限也要最小化,只给管理 Runner 需要的权限,过期时间设短一点。Runner 下线后要及时从组织页面移除,长期 offline 的 Runner 占着名额、还容易被误调度。
七、故障排查:Runner 离线、Job 卡住、日志定位
Runner 出故障时,现象分三类:Runner 在组织页面显示 offline、Job 一直排队没人接、Job 跑起来后卡住不动。每类的排查路径不一样:offline 先看 Runner 服务与网络,排队先看标签与并发,卡住先看 Job 日志与 Runner 机器的负载。不要一上来就重启 Runner,先定位再动手。
<pre class="wp-block-syntaxhighlighter-code"># 1. Runner offline:服务、网络、时间三项
sudo ./svc.sh status
curl -sI https://github.com | head -1
date && timedatectl | grep -i 'System clock'
# 2. Job 排队没人接:标签是否匹配、Runner 是否 busy、并发是否打满
curl -s -H "Authorization: token <PAT>" \
https://api.github.com/orgs/<org>/actions/runners | grep -B2 -A2 '"busy": true'
# 3. Job 卡住:看 Runner 本地日志(_diag 目录)与 Job 日志
tail -100 /opt/actions-runner/_diag/Runner_*.log
# 4. 机器负载高导致 Job 变慢:看 CPU、内存、磁盘 I/O
uptime && free -h && iostat -x 1 3 | tail -5
# 5. 兜底:确认无 Job 在跑后再重启 Runner 服务
# sudo ./svc.sh stop && sudo ./svc.sh start</pre>
时间不同步是 Runner offline 的常见原因:Runner 与 GitHub 服务器的时间差太大会导致认证失败,NTP 一定要配好。Job 卡住时先看 Job 日志的最后几行,大多数情况下是某一步在等网络或等锁,重启 Runner 解决不了这类问题。故障处理完要把原因与处理过程记下来,Runner 故障的复发率很高。
八、安全清单:token、secret 与日志
- 注册 token:一次有效,用完即废,不要写进脚本、文档或版本库。
- secret 隔离:构建 Job 只能拿到它自己的 secret,Runner 机器上不要存长期有效的 PAT,PAT 要设最小权限与过期时间。
- 日志脱敏:构建日志里不要打印 secret,GitHub 会自动打码以
***显示的 secret,但自定义输出要自己注意。 - Runner 更新:Runner 会自动更新到最新版,更新失败时先看磁盘与网络,不要手动降级。
- 离线 Runner:Runner 长时间离线会被 GitHub 标记为 offline,超过 30 天未连接需要重新注册。
- 审计:定期看组织审计日志里的 Runner 注册、删除与 Job 执行记录,异常 Runner 立即下线。







