构建卡在拉基础镜像二十分钟: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 这类耗时,而不是看流水线颜色。
  • 预检的报错要写清"缺什么、放哪里、改成什么名字",否则下一个人还得重查一遍。
查看「工程效能」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。