一、为什么你需要自己动手做 Runner 镜像
用 GitHub Actions 做自动化构建,默认用的是 GitHub 官方提供的 Runner 环境。这些环境虽然很省事,但有时候真心不够用。比如你要编译一个特殊版本的 .NET 程序,或者需要预装一个冷门的编译器,官方镜像里根本就没有。每次跑脚本前先要 apt-get install 一大堆东西,构建时间白白浪费掉。更烦的是,官方 Runner 还限制磁盘空间和内存,遇上大型项目动不动就超限。
自己搭个自托管 Runner(Self‑hosted Runner)就能解决这些烦恼。你可以在自己的服务器或者云主机上跑 Runner,想装什么就装什么。但问题又来了——如果每次注册新的 Runner 都要手动装环境,维护起来比官方还累。这时候就需要“自定义镜像”这个神器:把 Runner 软件和所有依赖打包到一个 Docker 镜像里,启动容器就能直接干活。镜像是不可变的基础设施,换机器、扩容都很简单。
二、打造自己 Runner 镜像的完整流程
2.1 选一个好养的“底子”
镜像的基础操作系统决定了你后续能装什么工具。大多数人用 Ubuntu,因为包多、文档全。如果你想省空间,也可以试试 Alpine,但装软件可能要多敲几行命令。新手建议从 Ubuntu 22.04 LTS 开始。
2.2 安装 Runner 并配置自启动
GitHub 官方提供了 actions/runner 的安装包,里面有个 config.sh 脚本用于注册 Runner。通常我们把它装在 /actions-runner 目录下。为了让容器里的 Runner 自动注册并长期运行,可以用一个启动脚本包装一下。
下面的 Dockerfile 做了几件事:
- 基于 Ubuntu 22.04
- 创建
runner用户(不推荐用 root 跑 Runner) - 下载并解压最新版 Runner
- 安装 Node.js 18(作为演示,因为很多项目需要它)
- 启动时用环境变量传入 GitHub Token 和仓库地址,自动注册并拉起 Runner
技术栈:Docker + Shell
# ============================================
# 文件名: Dockerfile
# 构建命令: docker build -t my-custom-runner .
# 运行命令: docker run -e REPO_URL=https://github.com/你的用户名/你的仓库 \
# -e TOKEN=你的GitHubPersonalAccessToken \
# -e RUNNER_NAME=my-runner-01 \
# --name runner my-custom-runner
# ============================================
# 步骤1: 基于 Ubuntu 22.04 精简版
FROM ubuntu:22.04
# 步骤2: 设置环境变量,避免安装时交互弹窗
ENV DEBIAN_FRONTEND=noninteractive
# 步骤3: 安装必要的系统工具和 build 基座
RUN apt-get update && apt-get install -y \
curl \
jq \
git \
sudo \
# 清理缓存,减小镜像体积
&& rm -rf /var/lib/apt/lists/*
# 步骤4: 下载并安装 Node.js 18 LTS
# 这里是为了演示预装一个常用工具
RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \
&& apt-get install -y nodejs \
&& node --version \
&& npm --version
# 步骤5: 创建非 root 用户 runner,用于安全运行 Action
RUN useradd -m -s /bin/bash runner \
&& echo "runner ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers
# 步骤6: 创建 Runner 安装目录并下载最新版 actions-runner
WORKDIR /actions-runner
RUN curl -o actions-runner-linux-x64-2.311.0.tar.gz \
-L https://github.com/actions/runner/releases/download/v2.311.0/actions-runner-linux-x64-2.311.0.tar.gz \
&& tar xzf actions-runner-linux-x64-2.311.0.tar.gz \
&& rm actions-runner-linux-x64-2.311.0.tar.gz \
# 设置目录权限,让 runner 用户可写
&& chown -R runner:runner /actions-runner
# 步骤7: 复制启动脚本
# 脚本会在容器启动时使用传入的环境变量注册 Runner
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# 步骤8: 切换到 runner 用户并设置启动命令
USER runner
ENTRYPOINT ["/entrypoint.sh"]
为了让上面的 Dockerfile 能工作,还需要一个 entrypoint.sh 脚本:
#!/bin/bash
# ============================================
# 文件名: entrypoint.sh
# 功能: 使用环境变量注册并启动 Runner 服务
# 环境变量:
# REPO_URL - 仓库 URL,如 https://github.com/user/repo
# TOKEN - GitHub Personal Access Token
# RUNNER_NAME - 自定义 Runner 名称,默认为 "custom-runner"
# ============================================
# 步骤1: 检查必需变量
if [ -z "$REPO_URL" ] || [ -z "$TOKEN" ]; then
echo "错误:必须设置 REPO_URL 和 TOKEN 环境变量" >&2
exit 1
fi
# 步骤2: 设置默认名称
RUNNER_NAME=${RUNNER_NAME:-"custom-runner"}
# 步骤3: 使用 GitHub API 获取注册 Token(另一种方式)
# 这里演示直接使用 PAT,实际生产更推荐用 GITHUB_TOKEN
# 但为了演示简单,我们用 PAT 直接注册
cd /actions-runner
# 步骤4: 配置 Runner(首次注册)
# --unattended 表示无需交互,--disableupdate 避免自动升级导致容器退出
./config.sh --url "$REPO_URL" \
--token "$TOKEN" \
--name "$RUNNER_NAME" \
--unattended \
--disableupdate \
--replace
# 步骤5: 启动 Runner 主进程
# 注意:不能用 nohup 或 &,必须前台运行以保证容器不退出
./run.sh
这个镜像构建好之后,每次启动容器都会自动注册一个新的 Runner,跑完任务后可以用 docker stop 停掉。如果想反复使用同一个 Runner 名称,记得加上 --replace 参数。
2.3 在 GitHub 仓库里如何引用这个 Runner
自托管 Runner 的标签名默认是 self-hosted,你可以再加上自己定义的标签(比如 ubuntu-custom)。在 .github/workflows/ 下的 YAML 里写:
name: 自定义 Runner 示例
on: [push]
jobs:
build:
runs-on: [self-hosted, ubuntu-custom] # 使用你的自定义镜像标签
steps:
- uses: actions/checkout@v4
- name: 看看 Node 版本
run: node --version # 验证预装成功
只要你的 Runner 容器在运行,这个 job 就会路由到你的机器上执行。
三、什么时候该用,什么时候该躲着走
3.1 最香的场景
- 大型编译任务:比如编译 C++ 工程、创建 Docker 镜像,需要大内存和磁盘。官方 Runner 的 7GB 存储经常不够用,自己搞一台服务器随便装。
- 需要特殊许可证或专有工具:某些商业编译器或者内部 SDK 不能放到公共镜像里,用自托管 Runner 就可以偷偷装在容器里,GitHub 端看不到。
- 重复安装消耗时间:如果你的工作流每次都要安装 20 个 npm 包或者 NuGet 包,把这些缓存直接做进镜像里,一次安装,多次使用,速度提升明显。
3.2 也得认清楚缺点
- 维护成本高:你得自己更新 Runner 版本、操作系统安全补丁、预装软件版本。官方 Runner 啥都不用管。
- 安全风险:别人提交的 PR 也能在你的机器上跑代码,如果恶意脚本窃取环境变量里的 Token,你的仓库就危险了。所以必须用
actions/checkout之后的沙箱隔离,甚至考虑用 Docker in Docker 里面再套一层。 - 硬件开销:服务器 24 小时待命,电费、机时费都要算。如果项目不多,可能还不如买 GitHub 的企业版划算。
3.3 注意事项:别踩这些坑
- Token 别写死:镜像里绝对不能硬编码 PAT(Personal Access Token)。应该通过环境变量传入,或者使用 GitHub 的
GITHUB_TOKEN配合actions/runner的自动注册机制。 - Runner 版本要同步:GitHub 经常更新 Runner 协议,镜像里的 Runner 版本如果太老,可能无法进行任务分配。建议写个定期重建镜像的 CI/CD 流水线。
- 容器退出策略:
./run.sh必须前台运行,如果进程退出,容器也会停止。可以考虑用supervisord或者systemd来保活,但更推荐用 Docker 的restart=always策略半自动恢复。 - 磁盘空间:容器里的日志和临时文件会堆积,定期清理
/actions-runner/_diag和/tmp。
四、把套路升级一下:结合 Docker Compose 管理多个 Runner
如果你有多个仓库或者多个标签需求,每个镜像都启动单独的容器会麻烦。用 Docker Compose 可以一次性拉起一群 Runner,每个 Runner 根据环境变量注册到不同仓库。
# ============================================
# 文件名: docker-compose.yml
# 启动方法: docker-compose up -d
# 说明: 这里会启动三个 Runner 容器,
# 分别注册到同一个仓库,但名字不同
# ============================================
version: '3.8'
services:
runner-1:
image: my-custom-runner:latest
container_name: gh-runner-1
environment:
RUNNER_NAME: "builder-node-18"
REPO_URL: "https://github.com/your-org/your-repo"
TOKEN: "${GH_PAT}" # 从宿主机的 .env 文件读取
restart: unless-stopped
runner-2:
image: my-custom-runner:latest
container_name: gh-runner-2
environment:
RUNNER_NAME: "builder-node-18-second"
REPO_URL: "https://github.com/your-org/your-repo"
TOKEN: "${GH_PAT}"
restart: unless-stopped
runner-3:
image: my-custom-runner:latest
container_name: gh-runner-3
environment:
RUNNER_NAME: "builder-node-18-third"
REPO_URL: "https://github.com/your-org/another-repo"
TOKEN: "${GH_PAT}"
restart: unless-stopped
然后在 .env 文件里写你的 PAT:
# .env 文件,不要提交到版本库
GH_PAT=ghp_你的长令牌
最后运行 docker-compose up -d 就可以同时开三个 Runner。如果某个 Runner 挂了,Docker 会自动重启。
五、总结
自己动手做 Runner 自定义镜像,本质上就是把“环境配置”变成代码。一旦镜像做出来,任何一台机器只要安装 Docker,启动容器就能变成 GitHub Actions 的专用构建基座。初期搭建需要花几个小时搞定 Dockerfile 和启动脚本,但后面部署新 Runner 只需要一行 docker run。相比维护一堆物理机或者虚拟机,镜像加容器的方式更灵活,也更容易回滚。
不过,镜像管理也有自己的成本,比如安全更新、版本同步、Token 保护等。如果你的团队人数不多,项目更新也不频繁,直接使用官方托管 Runner 更省心。当遇到性能瓶颈或者特殊软件依赖时,再掏出自定义镜像这个大招,效果立竿见影。
Comments