构建卡在拉基础镜像二十分钟:DinD 不认流水线的 pull 设置
前四步全绿,构建那步只报一句缺少基础镜像。pull 设置管的是步骤容器,FROM 的基础镜像由 DinD 里的 dockerd 负责,而它的三个下载槽被失败的拉取任务占满了。
前端仓库的流水线一直是五步:clone、装依赖、类型检查、构建镜像、部署。那天前四步都绿,第五步构建红了,日志翻到最底下只有一行:
[ERROR] CI 宿主机缺少基础镜像: node:24-alpine docker/dockerfile:1
不是网络超时,不是 npm 报错,是一句我们自己写的预检拦下来的。我去看了 .woodpecker.yml,构建步骤明明配了 pull: false,本意就是"别在流水线里拉镜像,直接用现成的"。既然配了,为什么还缺?
一、把"镜像"这个词拆成两层
这个词在流水线里指两个完全不同的东西:
一是步骤容器镜像。每个 step 本身跑在一个容器里,image: 字段写的是它,pull: false 管的是它——告诉 agent 不要每次去 registry 检查更新,用本地已有的。
二是构建用的基础镜像。docker build 执行时,FROM node:24-alpine 这一行要由宿主机的 dockerd 去解析、去拉取。# syntax=docker/dockerfile:1 更特殊,它是 BuildKit 的前端镜像,在解析 Dockerfile 之前就要先拉到。
我们的 CI 是 DinD 模式:一个 Pod 里跑 agent,旁边一个 sidecar 跑 dockerd,构建请求通过 DOCKER_HOST=tcp://127.0.0.1:2375 打到这个 dockerd。所以"宿主机"指的不是节点,是这个 sidecar 容器。它里面预置了十五个步骤镜像,一个基础镜像都没有。
# 直接问 DinD 里的 dockerd:这两个镜像到底在不在
kubectl -n ci exec deploy/woodpecker-agent -c dind -- \
sh -c 'DOCKER_HOST=tcp://127.0.0.1:2375 docker image ls --format "{{.Repository}}:{{.Tag}} {{.Size}}"' \
| grep -Ei 'node|dockerfile'
输出里只有 node:24(Debian 版,1.13 GB),没有 alpine 版,也没有 docker/dockerfile。
| 镜像类别 | 由谁拉取 | 对应配置 | 缺失时的表现 |
|---|---|---|---|
| 步骤容器镜像 | agent | 步骤的 image: 与 pull: |
步骤起不来,报镜像不存在 |
| Dockerfile 基础镜像 | DinD 的 dockerd | 无配置项,只能预置 | 构建第一步就失败 |
| BuildKit 前端镜像 | DinD 的 dockerd | 无配置项,只能预置 | 解析 Dockerfile 前就失败 |
这就是为什么 pull: false 没能救它:它管的是第一行,问题出在第二、三行。
二、公共镜像源在这套网络里基本不可用
最直接的想法是让 dockerd 自己去拉。试了,两个镜像源都是同样的结果:十分钟只打印一行 Pulling fs layer,没有任何进展。本机测过到其中一个源的速度,大约 1 MB/s,而且用默认 UA 或带 Range 的请求会被前置的 CDN 直接 403(error code 1010),换浏览器 UA 才能过。
这条路不通,剩下两条:把镜像搬到我们自己的 registry,或者干脆不用 alpine 基础镜像。后者会牵动 Dockerfile 和一堆原生依赖,不是这一轮该做的事。
于是走搬运:在本机用 registry 到 registry 的方式,把镜像从公共源搬到自己的 ACR,只搬 linux/amd64 这一个子 manifest,OCI manifest 转成 docker v2 格式,层 blob 原样推送,不做解压重打包(不改 digest,也不占本地磁盘)。脚本做成幂等:推之前先 HEAD 一次,命中就跳过。
# 搬运后的结果(脚本输出摘要)
mirror/node:24-alpine manifest PUT 201 4 layers 62 MB
mirror/dockerfile:1 manifest PUT 201 1 layer 14 MB
三、把镜像塞进 DinD,卡在了并发下载槽
第二步是在节点上把这两个镜像拉进 DinD 并改名。写完脚本跑了一次,十分钟没有任何输出,既没报错也没进度。
查下来是两个原因叠在一起。第一个:dockerd 默认 max-concurrent-downloads=3,也就是同时只允许三个拉取任务。第二个:之前那几次失败的尝试并没有真正结束,三个卡在公共源上的 docker pull 客户端一直挂着,把三个槽全占了。于是从我们自己的 registry 拉取也只能排队。
# 找出还挂着的 pull 客户端(不要用 pkill -f,见下)
ps -eo pid,etime,args | grep '[d]ocker pull'
# 2314 00:19:42 docker pull node:24-alpine
# 2318 00:19:40 docker pull docker/dockerfile:1
# 2321 00:18:55 docker pull node:24-alpine
for p in 2314 2318 2321; do kill "$p"; done
杀掉之后重跑,从 ACR 拉 62 MB 用了五秒。
这里有个我自己踩过的坑,值得单独写一句:不要用 pkill -f 'docker pull'。当时脚本是通过 sh -c 下发的,sh 自己的命令行里就包含这个匹配串,pkill -f 把承载脚本的 shell 一起杀了,脚本后半段根本没执行,看起来像"命令跑了但什么都没发生"。按 PID 精确杀,或者用 pgrep -f 先列出来核对。
四、重跑一次构建,看的是 DONE 的时间
DinD 里的准备动作是拉取加改名,改名这一步不能省:Dockerfile 里写的是 node:24-alpine,dockerd 按这个名字查本地镜像,名字对不上就等于没有。
export DOCKER_HOST=tcp://127.0.0.1:2375
docker pull "$REGISTRY/mirror/node:24-alpine"
docker tag "$REGISTRY/mirror/node:24-alpine" node:24-alpine
docker pull "$REGISTRY/mirror/dockerfile:1"
docker tag "$REGISTRY/mirror/dockerfile:1" docker/dockerfile:1
docker image inspect node:24-alpine --format '{{.Id}} {{.Size}}'
docker image inspect docker/dockerfile:1 --format '{{.Id}} {{.Size}}'
然后触发了一次构建。这一步要看的不是"流水线绿了",而是构建日志里那几行的时间和来源:
#2 docker-image://docker.io/docker/dockerfile:1 DONE 0.0s
#4 load metadata node:24-alpine DONE 0.0s
#6 FROM node:24-alpine DONE 0.1s
0.0s、0.1s 说明全部命中本地缓存,没有任何网络拉取。如果这里出现 DONE 120.3s,那就是又去远端拉了,只是这次刚好成功——这种"绿得侥幸"的流水线,过几天网络一抖又会红。
构建产物是 web:latest 和一个带短提交号的 tag,写镜像那行日志里有 writing image sha256:...,这条也要看一眼,确认推的是这次构建的产物。
五、真正该改的是流水线的失败姿势
镜像补上之后构建是绿的,但整条流水线仍然是红的,卡在后面的部署步骤:[ERROR] 找不到 /deploy/.env。这个和镜像无关,是另一件事——构建环境里的目录挂载到了 DinD 里是空目录。但两件事放在一起看,暴露的是同一个问题:流水线的预检做得很好,报错信息却只说了一半。
预检那句 CI 宿主机缺少基础镜像 是准确的,但它没告诉看日志的人"该往哪儿放、放成什么名字"。补上这句之后,下一次遇到同样问题的人就不需要像我这样查两个小时。
# 预检改成带处置提示的版本
- name: build-image
image: docker:27-cli
commands:
- |
for img in node:24-alpine docker/dockerfile:1; do
if ! docker image inspect "$img" >/dev/null 2>&1; then
echo "[ERROR] DinD 缺少基础镜像: $img"
echo " HINT: 在 dind 容器内执行"
echo " docker pull \$REGISTRY/mirror/$img"
echo " docker tag ... $img"
exit 1
fi
done
小结
pull: false只管步骤容器镜像,FROM的基础镜像和 BuildKit 前端镜像由宿主 dockerd 负责,两者不是一回事。- 内网环境里让 dockerd 自己拉公共源基本不可行,可行路径是把镜像搬进私有 registry 再预置进 DinD。
- registry 到 registry 搬运只搬子 manifest 和原始 blob,不改 digest、不解压重打包,脚本要幂等。
- dockerd 的并发下载槽只有 3 个,失败的 pull 客户端会一直占着,新任务只能排队;按 PID 清理,别用
pkill -f。 - 验证构建是否真的用上本地镜像,看
DONE 0.0s这类耗时,而不是看流水线颜色。 - 预检的报错要写清"缺什么、放哪里、改成什么名字",否则下一个人还得重查一遍。
正在加载评论…