本文主要记录一次 gitlab-runner 自动化部署踩坑经历,涉及 GitLab Runner 的配置、Job 容器网络连通性问题,以及 Docker 宿主机接入方式的调整。通过这次经历,分享在实际部署中遇到的问题和解决方案。
最近为了更新和测试方便,需要给一个前端项目补一套 GitLab 自动化部署流程。目标也不复杂,推送正式发布标签后触发流水线,构建 latest 镜像,然后在服务器上替换旧容器,对外映射宿主机 8092 端口到容器 80 端口。
看上去是个很常规的动作,真正开始跑之后才发现,问题甚至不在流水线脚本里:任务还没执行到 docker build,就在准备执行环境和拉取源码时先后失败了。这篇文章记录整个判断过程,以及两处真正需要改动的 Runner 配置,如果你也在做类似的自动化部署,或许能少踩几个坑。
项目本身是一个基于 React + Vite 的静态前端,构建产物输出到 dist 目录。部署方案采用了常见的组合:Node 负责构建,Nginx 负责提供静态资源服务。GitLab 和 GitLab Runner 都通过 Docker Compose 启动,Runner 使用 Docker executor 创建 Job 容器。
为了让发布动作尽量收敛,我给它定了几个明确规则:
latest。对应的 .gitlab-ci.yml 并不复杂,核心逻辑就是构建镜像、删除旧容器、启动新容器。问题在于,流水线还没执行到这几步,就已经先倒下了。
workflow:
rules:
-if:'$CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+$/'
-when:never
deploy_latest:
stage:deploy
image:docker:27-cli
rules:
-if:'$CI_COMMIT_TAG =~ /^v?\d+\.\d+\.\d+$/'
script:
-dockerbuild--pull-t"$IMAGE_NAME".
-dockerrm-f"$CONTAINER_NAME"||true
-dockerrun-d--name"$CONTAINER_NAME"--restartunless-stopped-p"${HOST_PORT}:80""$IMAGE_NAME"

无数次失败
要想首次跑通,Runner 的执行环境必须准备好。GitLab Runner 的 Docker executor 会在每次 Job 执行时新建一个容器,默认使用 docker:27-cli 镜像。这个镜像本身是轻量的,里面只有 Docker CLI 工具,没有守护进程。
但 Runner 在准备 Docker executor 时,拉取官方 helper 镜像经常会因为网络问题卡住。
Running with gitlab-runner 18.11.0~pre.918.g2aaa2400 (2aaa2400)
Preparing the "docker" executor
Using Docker executor with image docker:27-cli ...
Using helper image: registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-v18.11.0
Pulling docker image registry.gitlab.com/gitlab-org/gitlab-runner/gitlab-runner-helper:x86_64-v18.11.0 ...
国内网络环境下,拉取该镜像可能不稳定。我们可以把 helper_image 指向国内镜像仓库中对应架构和 Runner 版本的镜像:
[runners.docker]
helper_image = "swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/gitlab/gitlab-runner-helper:x86_64-v18.9.0"
这里我使用的是
docker-compose部署的整个 DevOps 环境,要修改上面的配置需要将 gitlab-runner 容器的/etc/gitlab-runner挂载到宿主机,然后在宿主机上修改config.toml,再重启 gitlab-runner 容器。
需要注意,helper 镜像应尽量与 Runner 版本保持一致。这样会避免后面出现未知的兼容性问题。
解决了上面的问题后,流水线测试再次失败,日志停在了 Getting source from Git repository 阶段,报错大致是这样:
fatal: unable to access 'https://git.xxx.com/xxx.git/':
Failed to connect to git.xxx.com port 443: Could not connect to server

Job 容器无法访问 GitLab
日志很明显了,最开始我以为是 GitLab 返回的仓库地址不通,于是把 Runner 的 clone_url 改成了容器名形式,毕竟是在同一个 Docker Compose 网络里:
clone_url = "http://gitlab"
结果新的报错很快出现了:
fatal: unable to access 'http://gitlab/xxx.git/': Could not resolve host: gitlab
好吧,好吧,确实疏忽了。gitlab 是 Docker Compose 网络内的服务名,Runner 容器可以解析它;但 Docker executor 新建的 Job 容器并不自动加入这个 Compose 网络。因此,对 Job 容器来说,gitlab 就是一个不存在的主机名。
到这里,问题已经比较清楚了:
url 是 Runner 访问 GitLab 的地址。git clone 时使用的是 clone_url,未配置时则使用 GitLab 返回的仓库地址。而真正的根因也随之浮出来:当前可达的不是 Compose 服务名,而是 Docker 宿主机暴露 GitLab 服务的 IP 和端口。
最终可用的配置并不花哨:
clone_url = "http://172.17.0.1:8083"

网络信息
在 Linux 的 Docker 默认 bridge 网络中,172.17.0.1 常常是容器访问宿主机的网关地址,可以通过宿主机上的 ip a 或在 Job 容器内实际连通性测试确认。它不是应当硬编码到所有环境的固定值;重点在于,这个地址必须是“从 Job 容器内部看过去可达的 GitLab 地址”。
如果网络结构允许,让 Runner 的 Job 容器加入 GitLab 所在的自定义 Docker 网络,并使用服务名访问 GitLab,通常会比依赖默认 bridge 网关更稳定。但在当前环境中,宿主机地址是最直接可行的方案。
这一步解决的是源码拉取问题,但还有另一个隐患很快会跟上来:即便代码能拉下来了,流水线里的 docker build 和 docker run 也不一定能执行。
原因也很直接:Job 容器默认不能操作宿主机 Docker 守护进程。要让它具备这个能力,需要把宿主机的 Docker 套接字挂进来:
volumes = ["/cache", "/var/run/docker.sock:/var/run/docker.sock"]
这行配置看起来普通,实际意义却很大。没有它,容器里的 docker 命令只是一个没有可用守护进程的客户端;有了它,命令会连接到宿主机 Docker,后面的镜像构建和容器重启才有落点。
同时也要明确它的权限边界:能访问
/var/run/docker.sock的 Job 基本等同于拥有宿主机 Docker 的高权限。这个方案适合受信任的项目和 Runner,不应给不受信任的合并请求或公共 Runner 使用。
这次自动化部署的收尾,表面看是改了两行配置:一个 clone_url,一个 docker.sock 挂载。更值得记住的不是配置本身,而是背后那条朴素的原则:让任务运行在它真正能访问、真正能操作的环境里。
技术问题有时就这样,最后的修复动作不一定复杂,难的是先把问题看对。先区分失败阶段,再从实际执行环境验证网络和权限,很多看似无关的报错就会变得可解释、可复现,也更容易解决。