在私有化环境里稳定运行 GitHub Actions 自托管 Runner,难的不是让它启动,而是如何长期、有序地管理。手动执行几次 config.sh 脚本将 Runner 挂载到仓库或组织,在早期可能可行。但当你需要同时维护多个 Runner、不同标签和多个仓库,尤其是在内网宿主机或容器环境中运行时,管理上的挑战会迅速凸显:
- Token 分散各处,其过期时间、复用性及权限范围需要不断确认。
- 宿主机或容器重启后,Runner 可能无法自动恢复,导致 Workflow 在队列中等待可用的 Runner。
- Runner 进程看起来还在,但就是不接收任务,排查时需要同时检查目录、进程状态并对照 GitHub 界面。
- 多个 Runner 共享宿主机环境可能导致互相污染,一个任务对环境变量的修改可能影响另一个任务。
- Docker 任务与非 Docker 任务的边界变得模糊,不该使用 Docker 的任务可能意外调用,而需要 Docker 的任务又可能无法运行。
这些痛点促使我将管理 GitHub 自托管 Runner 的经验沉淀下来,整理成了一个名为 Runner Fleet 的开源工具。它的核心目标是:将 Runner 视为服务来管理,而非一堆零散的目录。
我希望它能减少手动操作和踩坑,将 1~5 台机器上的自托管 Runner 部署、扩容与维护工作,收敛为一套可复用的工程流程。前期易于上手,后期在需要提高并发度、提升资源利用率或扩大吞吐量时,也不至于失去控制。

Runner Fleet 的功能非常明确:提供一个轻量级的 Web 管理界面,将 Runner 的安装、注册、启动/停止、编辑、状态监控以及自愈巡检,统一收归到一套可重复的流程中。
同时,它提供两种运行模式:既可以直接在宿主机上运行多个 Runner(非容器模式),以最大化复用宿主机环境;也可以实现“一个 Runner 一个容器”(容器模式),在同一台物理机上通过容器实现环境隔离。你可以根据私有化环境的具体需求灵活选择。如果你也在使用自托管 Runner 来运行 CI/CD,并希望提升其管理效率,欢迎在 开源实战 板块探索更多类似的项目与最佳实践。
构建一套可复用的管理范式
自托管 Runner 容易失控的根源往往不在于规模大小,而在于缺乏一套稳定的管理范式。我推荐将 Runner 的划分逻辑从“按仓库”改为“按职责”,让每个 Runner 的环境边界足够清晰。这样在出现问题时可以快速定位,迁移或扩容也会更加容易。
通常可以按职责进行如下拆分:
- 通用型 (General): 仅负责拉取代码、编译、单元测试、代码检查 (Lint) 等任务,尽量不依赖 Docker 或复杂的宿主环境。
- 容器型 (Docker): 专门用于运行容器构建与镜像发布任务,明确且可控地提供 Docker 能力。
- 重型任务型 (Heavy): 处理重型编译或大内存消耗的任务,将其独立出来可以避免拖慢整体 CI/CD 队列的速度。
Runner Fleet 的设计天然适配这种范式,因为它将每一个 Runner 都视为一个具有完整生命周期的配置对象,而不仅仅是你记忆中的某台机器上的某个目录。
本文将重点介绍在 容器场景 下的使用方法,其他模式(如裸金属部署、DinD 模式)我们将在后续有机会时分享。
服务器环境准备
服务器配置可参考相关环境搭建指南。核心步骤是更新 Ubuntu 系统,安装 Docker Engine 与 Compose 插件,并配置当前用户免 sudo 使用 Docker:
sudo apt update && sudo apt upgrade -y
sudo apt install -y ca-certificates curl gnupg lsb-release
curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/ \
$(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo gpasswd -a ${USER} docker
安装完成后,系统可能会提示需要重启:
*** System restart required ***
执行 sudo reboot 完成重启,一个能够运行 GitHub Actions Runner 的基础服务器环境就准备就绪了。
Runner Fleet 全容器环境部署
选择容器模式并非为了追求“云原生”的外观,而是为了解决以下三个实际痛点:
- 避免不同 Runner 之间的环境互相污染。
- 避免在同一台机器上升级或回滚多个 Runner 时,误操作彼此的目录。
- 为运行可能不完全可信的 Job 提供更严格的隔离边界。
部署的第一步是从 GitHub Release 页面获取最新版本的镜像并下载:
docker pull ghcr.io/soulteary/runner-fleet:v1.1.0
docker pull ghcr.io/soulteary/runner-fleet:v1.1.0-runner
创建独立的 Docker 网络:
docker network create runner-net
创建两个必要的目录并设置正确的权限:
mkdir -p config && chown 1001:1001 config
mkdir -p runners && chown 1001:1001 runners
接着,创建容器使用的基础环境变量配置文件 .env:
# 此处账号/密码换成自己需要的,仅为示例
BASIC_AUTH_USER=soulteary
BASIC_AUTH_PASSWORD=soulteary
# 使用镜像
MANAGER_IMAGE=ghcr.io/soulteary/runner-fleet:v1.1.0
RUNNER_IMAGE=ghcr.io/soulteary/runner-fleet:v1.1.0-runner
# 固定配置
CONTAINER_MODE=true
CONTAINER_NETWORK=runner-net
JOB_DOCKER_BACKEND=host-socket
RUNNERS_BASE_PATH=/app/runners
SERVER_PORT=8080
SERVER_ADDR=0.0.0.0
然后,执行命令将宿主机的 Docker 组 ID (GID) 添加到 .env 文件中:
DOCKER_GID=$(getent group docker 2>/dev/null | cut -d: -f3)
[ -n "$DOCKER_GID" ] && echo "DOCKER_GID=$DOCKER_GID" >> .env
再将宿主机上 runners 目录的绝对路径也写入 .env:
VOLUME_HOST_PATH=$(realpath runners 2>/dev/null || (cd runners 2>/dev/null && pwd -P))
echo "VOLUME_HOST_PATH=$VOLUME_HOST_PATH" >> .env
最后,编写核心的 docker-compose.yml 文件:
services:
runner-manager:
# 默认使用稳定版 v1.0.0;开发/尝鲜可设 .env:MANAGER_IMAGE=ghcr.io/soulteary/runner-fleet:main
image: ${MANAGER_IMAGE:-ghcr.io/soulteary/runner-fleet:v1.0.0}
container_name: runner-manager
# 使用 job_docker_backend: dind 时请先启动 DinD:docker compose --profile dind up -d
# 容器模式下必须用宿主机 socket,否则 Manager 无法创建 Runner 容器;默认 unix,勿改为 tcp://runner-dind:2375
# 全容器时可通过 .env 传入以下变量覆盖 config/config.yaml,无需改 config 文件:CONTAINER_MODE、VOLUME_HOST_PATH、JOB_DOCKER_BACKEND、CONTAINER_NETWORK、RUNNER_IMAGE、RUNNERS_BASE_PATH 等
environment:
DOCKER_HOST: ${DOCKER_HOST:-unix:///var/run/docker.sock}
BASIC_AUTH_USER: ${BASIC_AUTH_USER:-}
BASIC_AUTH_PASSWORD: ${BASIC_AUTH_PASSWORD:-}
# 以下可选:覆盖 config/config.yaml,全容器时无需改 config 文件(见 .env.example)
CONTAINER_MODE: ${CONTAINER_MODE:-}
VOLUME_HOST_PATH: ${VOLUME_HOST_PATH:-}
JOB_DOCKER_BACKEND: ${JOB_DOCKER_BACKEND:-}
CONTAINER_NETWORK: ${CONTAINER_NETWORK:-}
RUNNER_IMAGE: ${RUNNER_IMAGE:-}
RUNNERS_BASE_PATH: ${RUNNERS_BASE_PATH:-}
ports:
- "${MANAGER_PORT:-8080}:8080"
# 仅挂载 config 目录(勿挂载 config/config.yaml 单文件,否则宿主机无该文件时 Docker 会创建空文件导致启动失败)
volumes:
- ./config:/app/config
- ./runners:/app/runners
- /var/run/docker.sock:/var/run/docker.sock
# 容器模式需 Manager 访问宿主机 Docker:加入宿主机 docker 组(GID 用 getent group docker 查看,常见为 999)
group_add:
- "${DOCKER_GID:-999}"
networks:
- runner-net
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 5s
start_period: 15s
retries: 3
# runner-net 设为 external,避免 compose down 时删除网络导致已注册的 Runner 容器
#(由 Manager 动态创建、未在 compose 中定义)无法启动。首次使用请执行:docker network create runner-net
networks:
runner-net:
external: true
所有配置就绪后,执行 docker compose up -d 即可完成部署。这种将复杂环境封装为可编排配置的方式,正是 云原生/IaaS 理念的典型实践。默认情况下,访问 http://<你的IP或域名>:8080,使用上面 .env 文件中定义的账号密码登录。

登录成功后,即可进入 Runner 的管理界面。

跑通第一个私有化的 GitHub Action
在 GitHub 项目中获取 Runner Token
部署好 Runner Fleet 后,需要为它配置要服务的 GitHub 仓库或组织。访问 GitHub 上任意项目的设置页面,进入 Actions 菜单下的 Runners 配置页面。

点击 “New self-hosted runner” 按钮,选择操作系统和架构。在官方提供的配置示例中,找到包含 --token 参数的那条 ./config.sh 命令,点击该命令,它会自动复制到剪贴板。

在 Fleet 中初始化 Runner
回到 Runner Fleet 的管理界面,将刚才复制的命令粘贴到“快速添加 Runner”区域的解析框中,然后点击“解析并填充”按钮。

解析器会自动提取命令中的 URL 和 Token 等信息,并填充到下方的表单中。你可以在此处调整 Runner 的名称、标签等细节。

确认无误后,点击“添加 Runner”按钮,系统将在后台自动创建并注册该 Runner。

添加完成后,刷新页面即可在 Runner 列表中看到新添加的 Runner 及其状态。

同时,在 GitHub 对应仓库或组织的 Runners 设置页面,也能看到这个自托管的 Runner 已处于可用状态。

修改 GitHub CI 配置,启用私有 Runner
至此,私有化 Runner 的服务端已完全就绪。要真正使用它,只需最后一步:修改项目中的 GitHub Actions Workflow 配置文件,指定其使用自托管的 Runner。
找到项目 .github/workflows/ 目录下的任意 CI 配置文件,修改 jobs 部分。例如,将原先使用 GitHub 托管 Runner 的配置:
jobs:
# Code formatting check
fmt:
name: Code Formatting Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v6
将 runs-on: ubuntu-latest 修改为 runs-on: self-hosted。你还可以通过标签进行更精确的匹配,例如 runs-on: [self-hosted, linux, docker] 来指定使用带有 docker 标签的 Runner。

再次触发 CI/CD 流水线时,任务就会被分发到你刚刚部署的私有化 Runner 上执行了。通过这种方式,你可以将 CI/CD 的负载从 GitHub 的共享基础设施转移到可控的私有环境中,这在涉及敏感代码或需要特定依赖的场景下尤为重要。有效的 Runner 管理是保障 运维 & 测试 流程稳定与高效的关键一环。
最后
Runner Fleet 还支持裸金属部署、Docker-in-Docker (DinD) 模式等更多进阶玩法,这些内容我们将在后续分享。
在设计这个工具时,我希望能通过清晰、简单的配置来规避掉大量隐晦的错误,也希望所有 UI 操作都能被记录和追溯,让问题排查有据可依。当然,考虑到环境的复杂性,错误信息也应该被及时、清晰地反馈在界面上。或许,这才是一个工程化工具应有的样子。
在 云栈社区 ,我们持续关注和分享这类能提升开发与运维效率的工具与实践。