本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2026年02月18日 统计字数: 7122字 阅读时间: 15分钟阅读 本文链接: https://soulteary.com/2026/02/18/use-docker-to-natively-deploy-github-actions-runner-by-fleet.html ----- # 用 Docker 私有化部署 GitHub Actions Runner(Runner Fleet) 最近把我折腾 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 私有化部署方案](https://attachment.soulteary.com/2026/02/18/fleet-project.jpg) 所以我写了一个开源项目 [soulteary/runner-fleet](https://github.com/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 基础开发环境指南](https://soulteary.com/2025/01/17/guide-to-setting-up-ubuntu-24-04-basic-development-environment.html)》,把 Ubuntu 更新到最新状态,安装 Docker Engine 与 Compose 插件,并让当前用户可以免 `sudo` 使用 Docker 即可: ```bash 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 ``` 安装完毕,通常服务器会提示“需要重启”: ```bash *** System restart required *** ``` 输入 `sudo reboot`,完成服务器重启,一个能够运行 GitHub Action Runner 的服务器环境就准备好啦。 ## Runner Fleet 全容器环境部署 容器模式不是为了“看起来更云原生”,而是为了解决真实的三个痛点: 1. Runner 之间环境互相污染 2. 同机多 Runner 升级回滚时容易踩到彼此目录 3. 你需要更硬的边界来承载程序内容不完全可信的 job 我们从 [GitHub Release 页面](https://github.com/soulteary/runner-fleet/releases),找到最新的版本,使用 `docker pull` 对镜像进行下载: ```bash docker pull ghcr.io/soulteary/runner-fleet:v1.1.0 docker pull ghcr.io/soulteary/runner-fleet:v1.1.0-runner ``` 创建独立的 Docker 容器网络: ```bash docker network create runner-net ``` 创建两个目录: ```bash mkdir -p config && chown 1001:1001 config mkdir -p runners && chown 1001:1001 runners ``` 然后,创建容器使用的 `.env` 基础配置: ```bash # 此处账号/密码换成自己需要的,仅为示例 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` 文件中: ```bash DOCKER_GID=$(getent group docker 2>/dev/null | cut -d: -f3) [ -n "$DOCKER_GID" ] && echo "DOCKER_GID=$DOCKER_GID" >> .env ``` 然后,将 `runner` 在宿主机中的真实路径也传递到 `.env` 中: ```bash 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`: ```yaml 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 登录](https://attachment.soulteary.com/2026/02/18/fleet-login.jpg) 回车之后,就能够打开 Runner 配置界面,开始使用啦。 ![Runner Fleet - WebUI 界面](https://attachment.soulteary.com/2026/02/18/fleet-preview.jpg) ## 跑通第一个私有化的 GitHub Action ### 在 GitHub 项目中获取 Runner Token ![GitHub - Action Runner 配置页面](https://attachment.soulteary.com/2026/02/18/github-repo-action-runners-page.jpg) 当 Fleet 部署好之后,我们访问 GitHub 上任意项目的配置页面,打开 Action 菜单的 Runner 配置页面。 ![GitHub - 复制配置 token 指令](https://attachment.soulteary.com/2026/02/18/github-copy-token.jpg) 点击 “New self-hosted runner”,来到不同操作系统的 Runner 配置页面。我们在官方提供的 macOS / Linux 架构配置示例中,找到带有 `token` 的配置命令,单机该命令,命令会自动粘贴到剪贴板。 ### 初始化 Fleet Runner ![Fleet - 粘贴带有 Token 的命令到解析器](https://attachment.soulteary.com/2026/02/18/fleet-add-parser.jpg) 我们将命令粘贴到 fleet 的解析器中,点击“解析并填充”按钮。 ![Fleet - 一键解析](https://attachment.soulteary.com/2026/02/18/fleet-one-click-parsr.jpg) 配置中的内容,就会都自动装填到创建 Runner 表单中,我们可以在创建之前,调整或修改各种细节(比如命名): ![Fleet - 自动添加 Runner](https://attachment.soulteary.com/2026/02/18/fleet-added.jpg) 如果你觉得上面的内容都就绪了,点击“添加 Runner” 按钮,程序将自动初始化 Runner。 ![Fleet - 刷新页面的 Runner 列表](https://attachment.soulteary.com/2026/02/18/fleet-list.jpg) 添加完毕,我们在 Fleet Runner 列表中就能够看到刚刚添加的内容啦。 ![GitHub - Runner 列表实例就绪](https://attachment.soulteary.com/2026/02/18/github-runner-ready.jpg) 与此同时,GitHub Runner 页面的 Runner 实例也就就绪啦。 ### 修改 GitHub CI 配置,启用 Runner 到现在为止,我们已经将所有 GitHub Action CI/CD 的环境和服务都配置就绪啦。真正使用上私有化的 Runner 只差一步,就是修改 `.github/workflows` 中的 CI 文件,让他们使用自部署的 Runner,而非 GitHub 上的共享 Runner。 我们随便找一个之前已经启用了 GitHub Action 的项目,打开它任意的 workflows 配置文件: ```yaml 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 运行任务](https://attachment.soulteary.com/2026/02/18/github-run-with-self-hosted.jpg) 再次运行 CI 任务的时候,就会调用我们的私有化服务来执行任务啦,是不是很简单? ## 最后 Fleet 里还有一些其他的玩法,包括裸金属使用、Dind 模式使用,这些在后续有机会的时候,我会陆续分享出来。 在设计工具的时候,我希望我们能够通过写明确的、简单的配置来规避掉大量奇奇怪怪的错误,也希望 UI 操作都能够直接落盘,让排错的证据可追踪。当然,因为环境的复杂性,报错的信息也应该即时反馈在界面该有的地方。 或许,这才是工程化工具该有的样子。 --EOF