
为什么团队会把 CI 构建任务迁回自己的服务器
如果你的代码托管在 GitLab 上,但每次流水线运行都要排队半小时,或者共享构建机的分钟数月底总是提前耗尽,这篇指南可以帮你解决:把构建任务搬到一台自托管 GitLab Runner 上。自托管 Runner(负责执行流水线任务的服务进程)部署在自己的 VPS(虚拟专用服务器)或独立服务器上后,构建队列、依赖缓存、并发上限都由你自己掌控。一台 4 核 8GB 的机器跑常规单元测试通常只需 2-3 分钟,而同样的任务在免费共享 Runner 上可能要等 10 分钟排队再加 5 分钟执行。
整套方案分四步:在一台服务器上安装 Runner 服务、向 GitLab 项目注册、选择并配置执行器(Executor,真正运行构建任务的环境)、最后调优并发与缓存。下面按真实部署顺序逐步展开,每一步都给出可复制的命令。
安装 Runner 服务并连接 GitLab
选一台带宽(网络传输容量)稳定的服务器是第一步。Runner 需要频繁拉取代码仓库和依赖包,建议使用配置充足的 VPS 主机,系统以 Ubuntu 22.04 为例。GitLab 官方提供了独立的 apt 仓库,安装只需三条命令:
curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash
sudo apt install -y gitlab-runner
sudo systemctl enable --now gitlab-runner
安装完成后确认服务状态:systemctl status gitlab-runner 显示 active (running) 即可。注册前需要先拿到项目的注册令牌:进入 GitLab 项目的 Settings → CI/CD → Runners,展开后可以看到一串以 GR 开头的 token。由于 GitLab 17.0 起旧的注册令牌方式逐步被废弃,推荐直接在 Runners 页面点 New project runner,按向导生成一次性注册命令:
sudo gitlab-runner register \
--non-interactive \
--url "https://gitlab.com/" \
--token "GR1348941xxxxxxxxxxxx" \
--executor "docker" \
--docker-image "docker:24" \
--description "build-runner-01"
注册成功后刷新 Runners 页面,你会看到一个绿色圆点的在线状态。这里常见的失败原因有两个:服务器出方向无法访问 GitLab 域名(用 curl -I https://gitlab.com 验证),或者 token 复制时带了多余空格。如果 Runner 显示在线但始终不接任务,检查它是否被分配了受保护标签——流水线里的 tags: 字段必须和 Runner 的标签完全一致,任务才会被派发。
执行器怎么选:shell、docker 与 docker-in-docker
执行器决定了构建命令在哪里跑。shell 执行器直接在宿主机上执行命令,速度最快、没有容器开销,但构建脚本里的 apt install、rm -rf 会真实作用于宿主机,只适合跑完全可信的项目,安全性最差。docker 执行器每次任务都启动一个干净容器,环境隔离、依赖版本可控,是绝大多数团队的默认选择。
如果你的流水线需要构建 Docker 镜像,就要处理”容器里跑 Docker”的问题。两种主流方案:挂载宿主机的 Docker daemon(volume 模式),或者在容器内启动独立的 daemon(dind 模式)。volume 模式性能好但所有任务共享宿主机 Docker 状态;dind 模式隔离彻底但需要 --privileged 特权模式,内存开销多 300-500MB。团队规模小、只构建一两个自有项目时,volume 模式更省资源。
在 /etc/gitlab-runner/config.toml 里配置 docker 执行器的核心参数:
concurrent = 4
[[runners]]
name = "build-runner-01"
url = "https://gitlab.com/"
token = "GR1348941xxxxxxxxxxxx"
executor = "docker"
[runners.docker]
image = "docker:24"
privileged = true
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
pull_policy = ["if-not-present"]
shm_size = 268435456
pull_policy = ["if-not-present"] 这一行值得单独解释:默认情况下每个任务都会重新拉取镜像,Node.js 基础镜像超过 1GB 时,仅拉镜像就可能花掉 2 分钟。改为 if-not-present 后,本地已有镜像就直接复用,Java 项目的构建提速非常明显。代价是镜像更新需要你手动清理:定期跑 docker image prune -f 防止旧镜像堆积占满磁盘。
缓存与并发:把构建时间再砍一半
镜像拉取之外,第二个时间大头是依赖安装。一个中型前端项目 npm install 冷跑要 90-180 秒,配上缓存后可以压到 20 秒以内。GitLab 的缓存机制依赖前面 config.toml 里的 /cache 卷,配合 .gitlab-ci.yml 中的声明生效:
build:
stage: build
cache:
key:
files:
- package-lock.json
paths:
- node_modules/
script:
- npm ci --prefer-offline
缓存键绑定 package-lock.json 意味着依赖清单不变就复用旧缓存,锁文件一变才重建。实测一个 800 个依赖的前端项目,冷构建 3 分 40 秒,命中缓存后降到 1 分 10 秒。要注意缓存在 docker 执行器下默认是每个 Runner 本地存储的——如果你有多台 Runner,同一任务的两次运行可能命中不同机器的缓存,这时可以接入 S3 兼容的分布式缓存(配置 runners.cache 段),或者干脆接受一定的缓存未命中率。
并发数 concurrent = 4 不是越大越好。每个 docker 任务除了容器本身,还要占一份 docker daemon 资源,4 个并发任务在 8GB 内存机器上就可能触发 OOM(内存耗尽杀进程)。一个实用的估算:并发数 × 单任务峰值内存 ≤ 服务器内存 × 0.7。构建任务波动大的团队建议再加一层 swap(交换分区)兜底,避免一个失控的构建把整个 Runner 拖死。

常见故障排查:三个高频报错的定位路径
第一个高频问题是 job failed: preparation failed。这几乎都是镜像拉取环节出错:要么是 Docker Hub 匿名限流(每 6 小时 100 次拉取上限),要么是服务器 DNS(域名解析系统)解析异常。验证方法是在服务器上手动执行 docker pull docker:24,看到具体报错就能定位。被限流时最省事的解法是换镜像源,比如把基础镜像改成国内的镜像代理地址,或者在 config.toml 里给 docker 执行器配置 environment = ["DOCKER_TLS_CERTDIR="] 绕开 dind 的 TLS 初始化失败。
第二个是任务卡在 pending 状态不动。按这个顺序查:Runner 是否在线(GitLab Runners 页面绿点)→ 任务 tags 与 Runner 标签是否匹配 → concurrent 是否已被其他任务占满 → Runner 是否勾选了”只接受带标签的任务”而你的流水线没写标签。gitlab-runner verify 命令可以快速确认 Runner 与 GitLab 的连接仍然有效。
第三个是构建突然变慢,从 2 分钟涨到 20 分钟。多数情况是磁盘被构建垃圾塞满导致 I/O 变慢,df -h 看到 /var/lib/docker 所在分区超过 90% 基本可以确诊。执行 docker system prune -af --volumes 前先确认 /cache 卷不在删除范围内,否则所有项目缓存会被一并清掉,第二天全团队的构建都会变回冷启动速度。

总结与下一步行动
核心结论是:自托管 GitLab Runner 的部署本身不到半小时——安装、注册、配好 docker 执行器三步就能接活;真正决定构建体验的是后面这几项调优,镜像拉取策略、缓存键设计和并发上限。一个 4 核 8GB 的入门配置就能覆盖 10-20 人团队的日常 CI 需求,成本远低于按分钟计费的托管构建服务,而且缓存命中后的构建速度通常快一倍以上。有关构建产物如何最终部署到线上,可以参考这篇 WordPress 提速与 PHP-FPM 调优 的思路,把 CI 与运行环境当成同一条链路来优化。
如果你准备动手部署,建议按这个顺序推进:先在一台独立服务器上跑通单项目流水线,观察一周的构建时长和资源占用,再决定是否加并发、是否接入分布式缓存。需要一台稳定低延迟的构建机的话,可以考虑 Hostease 的 独立服务器,物理核不受虚拟化争抢,构建耗时的方差明显更小。多台构建机之间的数据同步可以复用 rsync 增量同步 的方案,保证缓存和构建缓存在节点间一致。CI 环境值得和生产服务器一样认真对待——毕竟每次提交代码的背后,都是一次对交付速度的考验。