最近把我折腾 GitHub self-hosted runner 的一些经验,整理成了一个开源小工具 Runner Fleet(千帆)。它的目标很简单,把 Runner 当作服务来管,而不是当作一堆目录来管。

我希望它能少点手工、少点踩坑,把 1~5 台机器上自托管 Runner 的部署、扩容、维护,收敛成一套可复用的流程。前期小规模好上手,后面要提高并发度、提升利用率、扩大吞吐,也不至于失控。

写在前面

在私有化环境里跑 GitHub Actions,自托管 Runner 往往不是难在跑起来,而是难在长期稳定地管起来。

一开始你可能只是手工执行几次 config.sh(GitHub Actions Runner 的配置脚本),把 Runner 挂到某个 repo 或 org 上。 但当你同时维护多个 Runner、多个标签、多个仓库,并且把它们跑在内网宿主机上之后,问题会很快集中爆发

  • Token 分散在不同地方,过期、复用、权限范围都要反复确认
  • 宿主机或容器重启后,Runner 不一定能自动恢复,workflow 卡在 queued 等待可用 runner
  • Runner 看起来还在,但就是不接单,排错要同时翻目录、看进程、对照 GitHub 页面
  • 多 Runner 共机后环境互相污染,一个任务改了环境,另一个任务跟着中招
  • Docker 任务与非 Docker 任务边界混乱,不该碰 Docker 的 job 也能碰到,反过来该用 Docker 的又跑不起来

Runner Fleet - 开源 GitHub Runner 私有化部署方案

所以我写了一个开源项目 soulteary/runner-fleet 如果你也在用 self-hosted runner 跑 CI/CD,欢迎来试试、提 issue 或 PR。如果确实帮你省了时间,也欢迎一键三连。

Runner Fleet 做的事情很明确:提供一个轻量的 Web 管理面,把 Runner 的安装、注册、启停、编辑、状态聚合,以及自愈巡检,收拢成一套可重复的工程流程。

同时它提供两种运行模式:既可以在宿主机上直接跑多个 Runner(非容器模式),最大化复用宿主机环境;也可以做到一 Runner 一容器(容器模式),在同一台机器上通过多个容器做环境隔离,方便你在私有化环境里按需选择。

构建一套可复用的使用范式

自托管 Runner 真正容易失控的根源不是规模,而是缺少一套稳定范式。

我更推荐把 Runner 的拆分逻辑从按仓库改成按职责,让每个 Runner 的环境边界足够清晰。这样出问题能快速定位,迁移扩容也更容易。

通常可以这样拆分

  • **通用(general)**只负责拉代码、编译、单测、lint,尽量不依赖 Docker 或复杂的宿主环境
  • **容器(docker)**专门跑容器构建与镜像发布,Docker 能力明确且可控
  • **重任务(heavy)**重编译或大内存任务,独立出来避免拖慢整体队列

Runner Fleet 的设计天然适配这种范式:因为它把每一个 Runner 都当作配置项加生命周期对象,而不是你记得的某台机器上的某个目录。

这篇文章中,我们主要来聊聊“容器”场景的使用,其他的部分,有机会的时候再聊。

服务器环境准备

服务器配置参考《搭建 Ubuntu 24.04 基础开发环境指南》,把 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 Action Runner 的服务器环境就准备好啦。

Runner Fleet 全容器环境部署

容器模式不是为了“看起来更云原生”,而是为了解决真实的三个痛点:

  1. Runner 之间环境互相污染
  2. 同机多 Runner 升级回滚时容易踩到彼此目录
  3. 你需要更硬的边界来承载程序内容不完全可信的 job

我们从 GitHub Release 页面,找到最新的版本,使用 docker pull 对镜像进行下载:

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 GID 添加到刚刚的 .env 文件中:

DOCKER_GID=$(getent group docker 2>/dev/null | cut -d: -f3)
[ -n "$DOCKER_GID" ] && echo "DOCKER_GID=$DOCKER_GID" >> .env

然后,将 runner 在宿主机中的真实路径也传递到 .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,软件部署就完成了。默认情况下,我们打开 http://IP或域名:8080,输入上面配置中定义的账号和密码:

Runner Fleet - Basic Auth 登录

回车之后,就能够打开 Runner 配置界面,开始使用啦。

Runner Fleet - WebUI 界面

跑通第一个私有化的 GitHub Action

在 GitHub 项目中获取 Runner Token

GitHub - Action Runner 配置页面

当 Fleet 部署好之后,我们访问 GitHub 上任意项目的配置页面,打开 Action 菜单的 Runner 配置页面。

GitHub - 复制配置 token 指令

点击 “New self-hosted runner”,来到不同操作系统的 Runner 配置页面。我们在官方提供的 macOS / Linux 架构配置示例中,找到带有 token 的配置命令,单机该命令,命令会自动粘贴到剪贴板。

初始化 Fleet Runner

Fleet - 粘贴带有 Token 的命令到解析器

我们将命令粘贴到 fleet 的解析器中,点击“解析并填充”按钮。

Fleet - 一键解析

配置中的内容,就会都自动装填到创建 Runner 表单中,我们可以在创建之前,调整或修改各种细节(比如命名):

Fleet - 自动添加 Runner

如果你觉得上面的内容都就绪了,点击“添加 Runner” 按钮,程序将自动初始化 Runner。

Fleet - 刷新页面的 Runner 列表

添加完毕,我们在 Fleet Runner 列表中就能够看到刚刚添加的内容啦。

GitHub - Runner 列表实例就绪

与此同时,GitHub Runner 页面的 Runner 实例也就就绪啦。

修改 GitHub CI 配置,启用 Runner

到现在为止,我们已经将所有 GitHub Action CI/CD 的环境和服务都配置就绪啦。真正使用上私有化的 Runner 只差一步,就是修改 .github/workflows 中的 CI 文件,让他们使用自部署的 Runner,而非 GitHub 上的共享 Runner。

我们随便找一个之前已经启用了 GitHub Action 的项目,打开它任意的 workflows 配置文件:

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

GitHub - 使用自托管 Runner 运行任务

再次运行 CI 任务的时候,就会调用我们的私有化服务来执行任务啦,是不是很简单?

最后

Fleet 里还有一些其他的玩法,包括裸金属使用、Dind 模式使用,这些在后续有机会的时候,我会陆续分享出来。

在设计工具的时候,我希望我们能够通过写明确的、简单的配置来规避掉大量奇奇怪怪的错误,也希望 UI 操作都能够直接落盘,让排错的证据可追踪。当然,因为环境的复杂性,报错的信息也应该即时反馈在界面该有的地方。

或许,这才是工程化工具该有的样子。

–EOF