一、为什么你需要自己动手做 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 更省心。当遇到性能瓶颈或者特殊软件依赖时,再掏出自定义镜像这个大招,效果立竿见影。