白板上画着两个框:左边是 GitLab,右边箭头指向 Runner。代码中枢有了,但没人干活。

一、GitLab 是大脑,Runner 是手脚
「Runner 是什么?」
「执行器。你推代码,GitLab 说『有人推代码了』,Runner 就过来领任务、跑流水线,再把结果告诉 GitLab。」
「那不就是 CI 的工人?」
「对。GitLab 是大脑,Runner 是手脚。」
执行器主要有三种:
- Shell executor:直接在宿主机跑,简单,但污染环境
- Docker executor:每个任务起一个容器,干净隔离
- Kubernetes executor:在 K8s 里跑,适合大规模
「我们用 Docker executor。每个流水线任务在独立容器里跑,不会因为上次任务留了垃圾文件就失败。」

二、启动 Runner
先创建配置目录:
sudo mkdir -p /srv/gitlab-runner/config
再启动 Runner 容器:
docker run -d --name gitlab-runner \
--restart always \
-v /srv/gitlab-runner/config:/etc/gitlab-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
gitlab/gitlab-runner:latest
「为什么挂 docker.sock?」
「因为 Docker executor 要起容器。Runner 本身在容器里,它要调用外面的 Docker,就得挂这个 socket。」
「安全吗?」
「有一定风险。生产环境 Runner 要单独隔离。」
这个风险值得说得更具体一点,因为它经常被当成「配置细节」而忽略。挂上 /var/run/docker.sock 之后,Runner 容器内的进程可以:
- 启动一个
-v /:/host 的容器,直接读写宿主机整个文件系统;
- 启动一个
--privileged 的容器,进一步触及内核能力;
- 读取宿主机上所有容器的环境变量,包括其他服务的密码。
换句话说:谁能改 .gitlab-ci.yml 并推到仓库,谁就等于拿到了宿主机 root。
加固手段按成本从低到高:
| 手段 |
效果 |
| 保护分支 + 限制谁能推流水线文件 |
最有效 |
| Runner 单独一台 VM |
缩小影响面 |
| rootless Docker / Podman |
不需要 root 的 socket |
| K8s executor |
让集群 RBAC 和 Pod 安全策略兜底 |
「教学环境先跑通」不等于「生产可以这么干」。
三、注册 Runner(★ 这一步的规则变了)
在 Admin Area → CI/CD → Runners,点击 New instance runner:
- Description:docker-runner-01
- Tags:docker, linux, x86_64
- 勾选 Run untagged jobs
创建后,GitLab 显示一个令牌:
glrt-xxxxxxxxxxxxxxxxxxxx
注意这个 token 的格式:glrt- 开头。这不是随便举的例子——它意味着 GitLab 已经把你引到了「新的注册工作流」上。
| 维度 |
旧:注册令牌 |
新:认证令牌 |
| 长什么样 |
一串十六进制,无前缀 |
glrt- 开头 |
| 在哪拿 |
Admin Area → Runners(全局令牌) |
先点 New instance runner 创建 runner,创建完才给令牌 |
| 配置填在哪 |
register 命令一路问你 |
description、tags 在 UI 创建时就填好了 |
| 注册参数 |
--registration-token |
--token |
为什么必须用新流程?GitLab 17.0 起,传统注册流程默认被禁用。如果还用旧方式,会拿到:
410 Gone - runner registration disallowed
这行报错看起来像「权限不够」,其实是你用了被废弃的流程。
所以,先看 token 前缀:glrt- 开头 = 新流程,用 --token;其他格式 = 旧流程,用 --registration-token。这类「工具还在、但使用方式已经变了」的改动,是自建平台最容易卡住的地方。遇到莫名其妙的失败,先搜一下这个版本的 Release Notes,比反复重试有效得多。

建议:能一行搞定,就别走交互式。
docker exec -it gitlab-runner gitlab-runner register \
--non-interactive \
--url "http://10.0.0.101" \
--token "glrt-xxxxxxxxxxxxxxxxxxxx" \
--executor "docker" \
--docker-image "alpine:latest"
两个好处:① 可复制、可脚本化;② 不会因为手滑选错——交互式流程里多按一次回车,可能就把 executor 选成了 shell。
交互式流程则是:
docker exec -it gitlab-runner gitlab-runner register
依次填:URL → token → description → tags → executor(docker)→ 默认镜像(alpine:latest)。
看到下面这行,说明注册成功:
Runner registered successfully.
刷新 GitLab 页面,能看到:
docker-runner-01 online docker,linux,x86_64
「在线了。绿色就对了。如果灰色,说明连不上——检查 URL 和 token。」
四、config.toml
注册完成后,Runner 的配置会落在 /srv/gitlab-runner/config/config.toml 里,大概长这样:
concurrent = 1
check_interval = 0
[[runners]]
name = "docker-runner-01"
url = "http://10.0.0.101"
token = "glrt-xxxxxxxxxxxxxxxxxxxx"
executor = "docker"
[runners.docker]
image = "alpine:latest"
privileged = false # 不开特权,安全
volumes = ["/cache"]
如果想提高并发,可以这样改:
sudo vim /srv/gitlab-runner/config/config.toml # concurrent = 2
docker restart gitlab-runner
「改了配置都要重启。Runner 启动时读配置。」
五、验证:第一条流水线
在项目里加一个最简的 .gitlab-ci.yml:
stages:
- test
hello:
stage: test
tags:
- docker
script:
- echo "Hello from GitLab Runner"
- uname -m
- cat /etc/os-release | head -2
提交后,进 CI/CD → Pipelines,会看到任务从 pending 走到 running,再到 passed。
输出大概是:
Hello from GitLab Runner
x86_64
PRETTY_NAME="Alpine Linux v3.x"
「注意,这是 x86_64。因为 Runner 跑在 vm-gitlab 上,VM 是 x86_64。」
「所以 CI 构建的镜像是 x86_64,正好给私有云用。」
「对。这就是为什么 CI 要用私有云的 Runner,而不是你本地的 Mac。架构一致,构建出来的镜像直接能部署。」

这不是偶然,是刻意的设计。看整条链路:
| 环节 |
架构 |
说明 |
| 你写代码的 Mac |
ARM64 |
只是编辑,不构建镜像 |
| 跑流水线的 Runner |
x86_64 |
构建在这里发生 |
| 私有 Registry |
x86_64 |
存的就是 Runner 构建出来的 |
| 部署目标 |
x86_64 |
和镜像架构一致 |
整条链路里,只有「写代码」是 ARM64,而它不产出镜像。所以后面所有环节都不会撞上架构不匹配。
六、常见问题
Q1:注册时提示 410 Gone - runner registration disallowed,怎么办?
这是「用了被废弃的旧流程」的典型症状,不是权限问题。
- (推荐)走新流程:New instance runner → 拿
glrt- 令牌 → 用 --token 注册;
- 临时启用旧流程:Admin Area → Settings → CI/CD,勾上 Allow runner registration tokens——权宜之计;
- 检查组/实例层级:token 从哪一级拿,决定 Runner 的可见范围。
判据:看 token 前缀。
Q2:Runner 显示 offline(灰色),怎么排查?
- 容器还在吗:
docker ps | grep runner;
- 能不能访问到 GitLab:
docker exec -it gitlab-runner curl -I http://10.0.0.101;
- token 对不对:看
config.toml;
- 看 Runner 日志:
docker logs gitlab-runner。
最常见的原因:url 写成了 localhost——Runner 在容器里,localhost 是它自己。
Q3:改了 config.toml 为什么不生效?
因为 Runner 只在启动时读配置,改完要 docker restart gitlab-runner。
顺便提醒:concurrent 调大之前,先想清楚内存够不够——每个并发任务都会起一个构建容器。
Q4:可以让 Runner 只在特定分支跑吗?
那是 .gitlab-ci.yml 里控制的,不是 Runner 配置:
build:
script: ...
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
Runner 层控制「在哪台机器上跑」(tags),流水线层控制「什么时候跑」(rules)——两个维度别搞混。
Q5:为什么默认镜像选 alpine?
因为小。alpine 只有几 MB,ubuntu 有几十 MB。CI 里每次都要拉镜像,差距会累积成可观的时间。
但要注意:alpine 用 musl libc 和 BusyBox,某些依赖 glibc 的二进制跑不起来。这也是 docker exec 时要用 sh 而不是 bash 的原因。
建议:默认用 alpine 做轻任务,需要完整环境的任务单独指定 ubuntu 或 python:3.x 镜像。
七、下话
第 13 话:第一条流水线——lint + test + build。
老周把前面几话的 shellcheck、bats、docker build 全摆在桌上。
「你前面几话练的那些,今天要串成一条线。」
「一条线?」
「对。一条你不参与、也会自动跑的线。」
下话见。