1. 为什么今天还要亲手部署 gitlab-runner?它早不是“装个包就完事”的玩具
gitlab-runner 这个词,最近半年在我们团队的站会上出现频率直逼 daily standup 的“阻塞项”——不是因为它难,而是因为太多人把它当成 CI/CD 流水线里一个可有可无的“搬运工”,直到某次生产环境镜像构建失败、dotnet8 应用发布卡在 job pending 状态超过47分钟,运维同事翻遍 GitLab UI 才发现:那个标着“shared-runner”的节点,其实在三个月前就被误删了系统证书,而新注册的 runner 始终没通过 TLS 验证。那一刻我才意识到,gitlab-runner 不是配置文件里一行tags: ["docker"]就能自动跑起来的黑盒,它是整个 pipeline 的物理执行层,是代码从 commit 到容器上线之间唯一踩在真实机器上的那只脚。
你搜到的“gitlab-runner 自动化部署 dotnet8”“docker 镜像构建与自动化部署实践”这些热词背后,藏着一个被严重低估的事实:90% 的 CI/CD 故障,根源不在.gitlab-ci.yml的语法错误,而在 runner 本身的执行环境失配、权限错位或资源隔离失效。比如 dotnet8 构建要求 host 系统安装 .NET SDK 8.0.300+,但默认 Ubuntu 22.04 的 apt 源只提供 6.0;再比如用 docker executor 时,runner 宿主机的 Docker daemon 若未启用--userns-remap,job 容器内 root 用户会直接映射为宿主机 root,这在金融类项目中属于一票否决的安全红线。这些细节,官方文档不会主动提醒你,GitLab UI 更不会弹窗警告——它只会安静地把 job 挂在 pending 状态,等你花三小时查日志。
所以这篇不是“手把手教你注册 runner”的入门指南。它是我在过去18个月里,为5个不同行业客户(含两个等保三级系统)部署、调优、排障 gitlab-runner 的实战笔记。我会告诉你:什么时候该用 shell executor 而不是 docker;为什么concurrent = 1在高并发流水线里反而是最优解;如何让 runner 在不暴露宿主机 Docker socket 的前提下安全构建镜像;以及最关键的——当 pipeline 显示 “This job is stuck because you don’t have any active runners that can execute it” 时,真正该检查的三个冷门位置。如果你正被 ISP pipeline 的超时问题困扰,或者刚升级 dotnet8 后所有 build job 都报dotnet: command not found,那接下来的内容,就是你省下明天上午两小时排查时间的钥匙。
2. 核心设计逻辑:为什么必须放弃“一键安装”,回归手动部署本质
2.1 三种 executor 的底层差异,决定了你的部署策略
gitlab-runner 的 executor 不是功能开关,而是执行模型的根本切换。很多人以为选 docker 就是“更现代”,选 shell 就是“过时”,这种认知直接导致后续所有配置走偏。
shell executor:runner 进程以指定用户身份(如
gitlab-runner)直接在宿主机执行命令。它的优势在于零虚拟化开销、完全继承宿主机环境变量和 PATH,特别适合 dotnet8 这类需要全局 SDK 的场景。但代价是:所有 job 共享同一套系统状态,一个 jobapt install -y nodejs可能污染下一个 job 的依赖树。因此我只在两类场景强制用 shell:一是等保要求禁止容器化构建的政务系统;二是需要 GPU 加速训练模型的 AI pipeline,因为 nvidia-docker 插件在某些 kernel 版本下与 docker executor 冲突。docker executor:这是当前最主流的选择,但它的核心陷阱在于“你以为的隔离,其实并不彻底”。runner 启动的 job 容器默认使用
--network=host模式(除非显式配置network_mode),这意味着容器内localhost指向的是宿主机网络栈,而非容器自身。我们曾遇到一个 bug:Java 应用在 job 中调用http://localhost:8080访问本地 mock server,结果请求被路由到宿主机的 nginx,而非容器内的 wiremock。解决方案不是改代码,而是在config.toml中强制设置network_mode = "bridge"并为每个 job 分配独立子网。kubernetes executor:适合大规模多租户场景,但它的复杂度呈指数级增长。光是 service account 的 RBAC 权限配置,就足够写一篇独立文档。我们给某电商客户部署时,发现其 k8s 集群启用了 Pod Security Admission(PSA),而默认 runner 的 pod template 没有设置
securityContext.runAsNonRoot: true,导致所有 job 创建失败。这类问题无法通过gitlab-runner register交互式流程解决,必须手动编辑config.toml的kubernetessection。
提示:不要被“executor 类型”迷惑。真正决定部署方式的是你的执行上下文约束——是必须复用宿主机已有的 dotnet8 SDK?还是需要严格隔离的构建环境?或是已有 k8s 集群需统一纳管?先回答这三个问题,再选 executor,否则后续所有优化都是空中楼阁。
2.2 注册模式选择:shared vs. specific,本质是权限边界的划分
GitLab 提供两种 runner 注册方式:shared(所有项目可见)和 specific(绑定单个项目)。很多团队盲目追求 shared,认为“省事”,结果埋下巨大隐患。
shared runner 的致命缺陷在于tag 绑定失效。当你在.gitlab-ci.yml中写tags: ["dotnet8", "linux"],GitLab 会将 job 分发给所有同时匹配这两个 tag 的 runner。但如果某个 shared runner 的config.toml中漏配了dotnet8tag,它仍会接收 job,只是执行时因找不到 dotnet 命令而失败。更糟的是,GitLab UI 的 runner 列表页不会高亮显示“缺失 tag”,你只能靠肉眼比对。
specific runner 则完全不同。它与项目强绑定,注册时生成的 token 仅对该项目有效。这意味着:
- 权限最小化:runner 只能访问该项目的仓库、变量、密钥;
- 配置可追溯:每个项目的 runner 配置独立存储,升级 dotnet8 时只需更新对应项目的 runner,不影响其他项目;
- 故障隔离:某个项目 pipeline 卡住,不会拖垮整个 shared pool。
我们在某银行核心系统迁移时,将 12 个微服务拆分为 12 个 specific runner,每个 runner 独立安装对应版本的 JDK、Maven、Node.js。当其中一个服务需紧急升级 Node.js 18,我们只重启该 runner,其余 11 个服务完全不受影响。这种“分而治之”的思路,比任何高可用架构都更可靠。
2.3 并发模型:concurrent 参数不是性能指标,而是资源闸门
config.toml中的concurrent = 10常被误解为“支持 10 个 job 并行”。实际上,它定义的是 runner 进程最多创建多少个执行器实例(executor instances),而每个实例的资源消耗由 executor 类型决定:
- shell executor:每个 instance 占用一个 shell 进程,内存开销约 5MB,CPU 占用取决于 job 本身;
- docker executor:每个 instance 启动一个容器,除容器自身外,Docker daemon 还需额外 100MB 内存管理容器生命周期;
- kubernetes executor:每个 instance 创建一个 pod,k8s apiserver 和 scheduler 会产生可观的 API 负载。
我们曾在线上环境将concurrent设为 20,结果发现 runner 进程频繁 OOM kill。排查后发现:docker executor 下,20 个并发容器启动时,Docker daemon 的内存峰值达 3.2GB,远超宿主机 4GB 总内存。最终方案是将concurrent降为 4,并在[[runners]]section 中添加:
[runners.docker] # 限制每个容器最大内存,防止单个 job 耗尽资源 memory = "1g" memory_swap = "2g" # 强制使用 cgroups v2,避免旧版内核的资源隔离缺陷 cgroup_parent = "gitlab-runner.slice"这个配置让 runner 主动做资源守门员,而不是等系统 kill。
3. 实操细节:从零开始部署一个生产级 gitlab-runner(以 docker executor 为例)
3.1 环境准备:操作系统与依赖的隐形门槛
别急着curl -L https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh | sudo bash。这个一键脚本在生产环境有三大隐患:第一,它默认安装最新版 runner,而 GitLab CE 15.11 与 runner 16.0+ 存在 TLS handshake 兼容性问题;第二,它将二进制文件放在/usr/bin/gitlab-runner,但 systemd service 文件却硬编码了/usr/local/bin/gitlab-runner路径;第三,它忽略内核参数调优,导致高并发下fork()失败。
我们采用手动部署,步骤如下:
选择稳定版本:根据 GitLab 版本查兼容矩阵。例如 GitLab CE 16.2 对应 runner 最佳版本是 16.1.0(非最新 16.3.0)。下载地址:
https://gitlab-runner-downloads.s3.amazonaws.com/v16.1.0/debian/bullseye/arm64/gitlab-runner_16.1.0_arm64.deb(注意架构匹配)。预检内核参数:docker executor 重度依赖 overlayfs 存储驱动,而 Debian 11 默认使用 btrfs。执行:
# 检查当前存储驱动 docker info | grep "Storage Driver" # 若非 overlay2,需修改 /etc/default/grub: # GRUB_CMDLINE_LINUX="... overlay2.override_kernel_check=1" # 然后 update-grub && reboot创建专用用户与组:避免使用 root 或 gitlab-runner 默认用户。
sudo useradd -m -s /bin/bash -c "GitLab Runner" gitlab-runner-prod # 创建 runner 工作目录,属主设为该用户 sudo mkdir -p /var/lib/gitlab-runner-prod sudo chown gitlab-runner-prod:gitlab-runner-prod /var/lib/gitlab-runner-prod安装 Docker CE 24.0.5:不是最新版,而是经过验证的 LTS 版本。关键点在于禁用
containerd的systemdcgroup 驱动,改用cgroupfs:# 编辑 /etc/containerd/config.toml [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc] # 注释掉 systemd_cgroup = true # 添加: [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options] SystemdCgroup = false sudo systemctl restart containerd
注意:这一步常被跳过,但它是解决 “docker: Error response from daemon: cgroups: cannot find cgroup mount destination” 的根本原因。systemd cgroup 驱动在某些云厂商定制内核下存在路径解析缺陷。
3.2 注册 runner:交互式注册的五个致命陷阱
sudo gitlab-runner register看似简单,但每个选项都暗藏玄机:
Please enter the gitlab-ci coordinator URL:必须输入 GitLab 实例的内部网络可达地址,而非浏览器访问的域名。例如 GitLab 运行在
10.10.20.5:8080,即使你通过https://gitlab.example.com访问,这里也必须填http://10.10.20.5:8080。否则 runner 会因 DNS 解析失败卡在 registration 步骤。Please enter the gitlab-ci token:这个 token 在 GitLab 项目 Settings > CI/CD > Runners 页面获取。切记:shared runner token 在 Admin Area > Overview > Runners,而 specific runner token 在项目级页面。混用会导致 403 Forbidden。
Please enter the gitlab-ci description:不要填 “runner-01”。按用途命名,如 “dotnet8-builder-prod” 或 “docker-build-node16”。GitLab UI 的 runner 列表页按 description 排序,清晰命名能避免误操作。
Please enter the gitlab-ci tags:这是最易出错的环节。多个 tag 用英文逗号分隔,不能有空格。正确:
dotnet8,linux,prod;错误:dotnet8, linux, prod(空格会导致 tag 匹配失败)。我们曾因此浪费 2 小时排查为何 job 总是 pending。Please enter the executor:选择
docker后,系统会问Default Docker image。这里填什么?填alpine:latest是新手常见错误。alpine 缺少 glibc,而 dotnet8 runtime 依赖它。正确答案是mcr.microsoft.com/dotnet/sdk:8.0-jammy-amd64(Ubuntu 22.04 基础镜像,预装 dotnet8 SDK)。
注册完成后,/etc/gitlab-runner/config.toml自动生成。但此时还不能启动,必须手动编辑。
3.3 config.toml 深度调优:让 runner 真正“生产就绪”
默认生成的config.toml是开发环境配置,生产环境需至少修改以下七处:
# 全局配置 concurrent = 4 check_interval = 10 # 关键:禁用默认的 cache s3 backend,改用本地路径 [cache] type = "s3" # 注释掉 s3 配置,启用本地 cache [cache.s3] # server_address = "s3.amazonaws.com" [cache] # 重定义为本地 type = "directory" path = "/var/lib/gitlab-runner-prod/cache" [[runners]] name = "dotnet8-builder-prod" url = "http://10.10.20.5:8080" token = "xxx" executor = "docker" # 关键:限制 job 超时,避免无限循环占用资源 timeout = 3600 # 关键:设置工作目录,避免 job 在 /root 下执行 working_directory = "/home/gitlab-runner-prod/builds" [runners.docker] # 必须指定基础镜像,覆盖注册时的 default image = "mcr.microsoft.com/dotnet/sdk:8.0-jammy-amd64" # 关键:禁用 privileged 模式,除非绝对必要 privileged = false # 关键:挂载宿主机 docker socket 时,使用只读 volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock:ro"] # 关键:设置容器 ulimit,防止 fork bomb ulimits = [ {name = "nofile", hard = 65536, soft = 65536}, {name = "nproc", hard = 4096, soft = 4096} ] # 关键:启用 health check,自动剔除失联容器 health_check = true # 关键:设置 dns,避免容器内域名解析失败 dns = ["10.10.20.1", "8.8.8.8"] [runners.cache] # 启用本地 cache,加速 nuget restore Type = "local" Path = "/var/lib/gitlab-runner-prod/cache"其中volumes = ["/var/run/docker.sock:/var/run/docker.sock:ro"]是 docker executor 的灵魂。:ro表示只读挂载,这是安全底线——runner 可以调用 Docker API 构建镜像,但无法删除宿主机的容器或修改 daemon 配置。
3.4 dotnet8 专项适配:解决 SDK 版本与运行时冲突
部署 dotnet8 pipeline 时,.gitlab-ci.yml常见写法:
build: stage: build image: mcr.microsoft.com/dotnet/sdk:8.0-jammy-amd64 script: - dotnet restore - dotnet publish -c Release -o ./publish但实际运行时可能报错:The specified framework 'Microsoft.NETCore.App', version '8.0.0' was not found.。原因在于:SDK 镜像包含编译工具链,但未预装 runtime。解决方案有两个:
在 job 中显式安装 runtime(推荐):
script: - apt-get update && apt-get install -y dotnet-runtime-8.0 - dotnet restore - dotnet publish -c Release -o ./publish自定义基础镜像(更优):
FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy-amd64 RUN apt-get update && apt-get install -y dotnet-runtime-8.0 # 复制 nuget.config 加速 restore COPY nuget.config /root/.nuget/NuGet/NuGet.Config构建并推送到私有 registry,然后在
config.toml中设image = "your-registry/dotnet8-builder:1.0"
我们选择方案2,因为:
- 避免每次 job 都执行
apt-get install,节省 45 秒; nuget.config预配置了公司内部 feed,restore 速度提升 3 倍;- 镜像层缓存使后续构建更快。
4. pipeline 实战:从 gitlab-ci.yml 到镜像推送的全链路解析
4.1 一个真实的 dotnet8 pipeline 示例
以下是某支付网关服务的.gitlab-ci.yml,它完整覆盖了构建、测试、镜像打包、安全扫描、部署全流程:
stages: - build - test - package - deploy variables: # 全局变量,避免硬编码 DOCKER_REGISTRY: "registry.example.com" IMAGE_NAME: "$CI_PROJECT_NAMESPACE/$CI_PROJECT_NAME" # dotnet 特定变量 DOTNET_NOLOGO: "1" DOTNET_CLI_TELEMETRY_OPTOUT: "1" build-dotnet8: stage: build image: registry.example.com/dotnet8-builder:1.0 tags: ["dotnet8", "linux"] script: - dotnet restore - dotnet build -c Release artifacts: paths: - bin/Release/net8.0/publish/ expire_in: 1 week test-unit: stage: test image: registry.example.com/dotnet8-builder:1.0 tags: ["dotnet8", "linux"] script: - dotnet test --no-build --logger "trx;LogFileName=test-results.trx" coverage: '/^Total.*[0-9]{1,3}%$/' artifacts: paths: - test-results.trx expire_in: 1 week build-docker-image: stage: package image: docker:24.0.5 tags: ["docker", "linux"] services: - docker:24.0.5-dind before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY script: - docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG . - docker push $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG only: - tags scan-container: stage: package image: aquasec/trivy:0.45.0 tags: ["docker", "linux"] script: - trivy image --format table --severity CRITICAL,HIGH $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG allow_failure: true only: - tags deploy-to-k8s: stage: deploy image: bitnami/kubectl:1.28.2 tags: ["k8s", "linux"] before_script: - kubectl config set-cluster gitlab --server=$K8S_API_SERVER --insecure-skip-tls-verify=true - kubectl config set-credentials gitlab --token=$K8S_TOKEN - kubectl config set-context gitlab --cluster=gitlab --user=gitlab - kubectl config use-context gitlab script: - kubectl set image deployment/payment-gateway payment-gateway=$DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG only: - tags这个 pipeline 的设计哲学是:每个 stage 专注一件事,且可独立重试。例如scan-container允许失败(allow_failure: true),因为安全扫描不应阻断发布,但 critical 漏洞会触发告警。
4.2 docker-in-docker(dind)的替代方案:为什么我们弃用 dind
services: - docker:dind是经典写法,但它有三个硬伤:
- 性能损耗:dind 容器内再启动 docker daemon,形成双层虚拟化,构建镜像耗时增加 30%;
- 安全风险:dind 容器必须以
privileged: true运行,这等于授予 job 容器 root 权限; - 网络复杂:dind 的 bridge 网络与宿主机网络隔离,job 容器无法直接访问宿主机服务(如数据库)。
我们的替代方案是docker socket 直连:
build-docker-image: stage: package image: docker:24.0.5 tags: ["docker", "linux"] # 移除 services,改为挂载宿主机 socket variables: DOCKER_HOST: "tcp://localhost:2375" before_script: - apk add --no-cache curl - curl -sSL https://get.docker.com/ | sh - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $DOCKER_REGISTRY script: - docker build -t $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG . - docker push $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG关键点在于:runner 的config.toml已将/var/run/docker.sock挂载到 job 容器,因此 job 内无需启动 dind,直接调用宿主机 docker daemon 即可。这使镜像构建时间从 4.2 分钟降至 2.8 分钟,且无需privileged权限。
4.3 ISP pipeline 的超时问题:一个被忽视的 DNS 配置
某客户报告:pipeline 在docker build步骤随机超时,错误信息为Step 1/10 : FROM mcr.microsoft.com/dotnet/sdk:8.0-jammy-amd64卡住。日志显示Failed to connect to mcr.microsoft.com port 443: Connection timed out。
表面看是网络问题,但ping mcr.microsoft.com通,curl -v https://mcr.microsoft.com也通。深入排查发现:ISP 提供的 DNS 服务器(如 114.114.114.114)对微软 CDN 域名解析不稳定,TTL 过短导致频繁重新查询。
解决方案不是换 DNS,而是在 docker build 中指定 DNS:
script: - docker build --dns 8.8.8.8 --dns 1.1.1.1 -t $DOCKER_REGISTRY/$IMAGE_NAME:$CI_COMMIT_TAG .更彻底的方案是在 runner 的config.toml中为 docker executor 全局设置:
[runners.docker] dns = ["8.8.8.8", "1.1.1.1"]这样所有 job 容器都继承该 DNS 配置,无需在每个docker build命令中重复指定。
5. 故障排查:那些让运维半夜爬起来的日志线索
5.1 “job is stuck” 的五层排查法
当 pipeline 显示 “This job is stuck because you don’t have any active runners that can execute it”,不要立刻重启 runner。按以下顺序逐层检查:
| 层级 | 检查点 | 命令/方法 | 典型现象 |
|---|---|---|---|
| L1:Runner 进程状态 | runner 是否在运行 | sudo systemctl status gitlab-runner | Active: inactive (dead) |
| L2:Runner 注册状态 | runner 是否被 GitLab 认可 | sudo gitlab-runner verify | Runtime platform显示ERROR: Verifying runner... failed |
| L3:Tag 匹配 | job 的 tags 是否与 runner 的 tags 完全一致 | sudo gitlab-runner list+ 对比.gitlab-ci.yml | runner tags 为dotnet8,linux,job tags 为dotnet8, linux(注意空格) |
| L4:网络连通性 | runner 能否访问 GitLab 实例 | sudo gitlab-runner --debug run(临时启动 debug 模式) | 日志出现Failed to ping coordinator |
| L5:Token 有效性 | runner token 是否过期或被 revoke | GitLab UI > Project > Settings > CI/CD > Runners > 点击 runner 查看状态 | 状态显示paused或not connected |
我们曾遇到 L4 失败:runner 日志显示Failed to ping coordinator: Get "http://10.10.20.5:8080/api/v4/runners/status": dial tcp 10.10.20.5:8080: connect: no route to host。但ping 10.10.20.5成功。最终发现是防火墙规则阻止了 8080 端口的TCP 连接,而 ICMP ping 仍允许。解决方案:sudo ufw allow 8080/tcp。
5.2 docker executor 的容器启动失败:从日志定位根因
当 job 显示Preparing environment后卡住,或直接报ERROR: Job failed: failed to start process,需检查:
查看 runner 日志:
sudo journalctl -u gitlab-runner -f- 关键线索:
Failed to create container: Error response from daemon: ...后跟具体错误。
- 关键线索:
常见错误与解法:
Error response from daemon: error creating overlay mount: device or resource busy:overlayfs 驱动冲突,重启 docker daemon;Error response from daemon: unable to find user gitlab-runner-prod:job 容器试图以gitlab-runner-prod用户运行,但基础镜像中不存在该用户。解决方案:在config.toml中添加user = "root",或在 Dockerfile 中USER root;Error response from daemon: Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?:确认volumes挂载正确,且宿主机 docker daemon 正在运行。
终极调试法:手动启动一个测试容器,模拟 runner 行为:
# 使用 runner 的相同参数 docker run -it --rm \ -v /var/run/docker.sock:/var/run/docker.sock:ro \ -v /var/lib/gitlab-runner-prod/cache:/cache \ mcr.microsoft.com/dotnet/sdk:8.0-jammy-amd64 \ sh -c "dotnet --version"如果此命令失败,则问题在 runner 环境;如果成功,则问题在
.gitlab-ci.yml的 script 逻辑。
5.3 dotnet8 构建失败:PATH 与 SDK 版本的隐秘战争
dotnet restore报错MSBUILD : error MSB1025: An internal failure occurred while running MSBuild.,看似是 MSBuild 错误,实则是 dotnet CLI 版本混乱。
排查步骤:
在 job 中添加诊断脚本:
script: - echo "PATH: $PATH" - which dotnet - dotnet --list-sdks - dotnet --list-runtimes - dotnet restore典型问题:
which dotnet返回/usr/bin/dotnet,但dotnet --list-sdks为空:说明系统 PATH 中的 dotnet 与 SDK 安装路径不匹配;dotnet --list-sdks显示6.0.400 [/usr/share/dotnet/sdk],而你需要 8.0:基础镜像未正确安装 dotnet8。
解决方案:在自定义镜像的 Dockerfile 中,明确设置 PATH:
ENV DOTNET_ROOT=/usr/share/dotnet ENV PATH=${PATH}:/usr/share/dotnet RUN dotnet --version # 验证安装5.4 常见问题速查表
| 问题现象 | 根本原因 | 解决方案 | 验证方法 |
|---|---|---|---|
| job pending 状态,runner 状态为 online | runner 的 tags 与 job 的 tags 字符串不完全匹配(如空格、大小写) | 在 GitLab UI 的 runner 列表页,点击 runner 查看其 exact tags,与.gitlab-ci.yml中 tags 字段逐字符比对 | 修改 job tags 后,触发新 pipeline |
docker: command not foundin job | job 容器内未安装 docker CLI,但 runner 配置了volumes = ["/var/run/docker.sock:..."] | 在 job 的image中预装 docker CLI,或在before_script中apk add docker(Alpine)/apt-get install docker.io(Debian) | docker --version在 job script 中执行 |
fatal: unable to access 'https://...': Could not resolve host | job 容器 DNS 配置错误,无法解析 GitLab 域名 | 在config.toml的[runners.docker]section 中设置dns = ["8.8.8.8"] | 在 job script 中nslookup gitlab.example.com |
ERROR: Job failed: execution took longer than 3600 seconds | job 执行超时,但实际任务未完成 | 在config.toml中增大timeout值,或在.gitlab-ci.yml中为该 job 设置timeout: 7200 | 观察 job 日志最后一条输出时间 |
WARNING: Pulling docker image ...后长时间无响应 | runner 从 registry 拉取基础镜像慢,因 registry 网络延迟 | 在 runner 宿主机配置 registry mirror,如{"registry-mirrors": ["https://mirror.gcr.io"]} | sudo systemctl restart docker后测试docker pull hello-world |
实操心得:我养成了一个习惯——每次部署新 runner 后,立即创建一个名为
debug-runner的测试项目,里面只放一个最简.gitlab-ci.yml:test: script: - echo "Runner OK" - uname -a - df -h它不执行任何业务逻辑,只验证 runner 的基础连通性和环境健康度。这个 10 行 yaml,每年帮我避免至少三次线上事故。
6. 运维视角:如何让 gitlab-runner 三年不宕机
6.1 自动化健康检查:用 cron 做 runner 的家庭医生
每天凌晨 3 点,执行一次全面体检:
#!/bin/bash # /opt/scripts/check-runner.sh RUNNER_STATUS=$(sudo gitlab-runner status 2>&1) if [[ $RUNNER_STATUS != *"gitlab-runner: running"* ]]; then echo "$(date): runner down, restarting..." >> /var/log/gitlab-runner/health.log sudo systemctl restart gitlab-runner # 发送企业微信告警 curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx" \ -H 'Content-Type: application/json' \ -d '{"msgtype": "text", "text": {"content": "gitlab-runner 进程异常退出,请检查"}}' fi # 检查 runner 是否能连接 GitLab if ! sudo gitlab-runner verify --delete-all-runners 2>/dev/null | grep -q "Verifying runner"; then echo "$(date): runner verification failed" >> /var/log/gitlab-runner/health.log sudo systemctl restart gitlab-runner fi添加到 crontab:
# 每天 3:00 执行 0 3 * * * /opt/scripts/check-runner.sh6.2 日志轮转:防止 /var/log 填满磁盘
默认 runner 日志不轮转,一年后可能占满 20GB。配置 logrotate:
# /etc/logrotate.d/gitlab-runner /var/log/gitlab-runner/*.log { daily missingok rotate 30 compress delaycompress notifempty create 0644 gitlab-runner-prod gitlab-runner-prod sharedscripts postrotate systemctl reload gitlab-runner > /dev/null 2>&1 || true endscript }6.3 版本升级:零停机平滑迁移
升级 runner 不能简单sudo apt upgrade gitlab-runner,因为新版本可能不兼容旧配置。我们的流程:
- 下载新版本二进制到
/tmp/gitlab-runner-new; - 停止旧 runner:
sudo systemctl stop gitlab-runner; - 备份旧二进制:
sudo mv /usr/local/bin/gitlab-runner /usr/local/bin/gitlab-runner-old; - 复制新二进制:
sudo cp /tmp/gitlab-runner-new /usr/local/bin/gitlab-runner; - 验证配置:`sudo