docker pull 超时最容易被误判成「网络慢」。实际排查要先分清三件事:Docker daemon 能不能连到 registry,registry 是否允许这次 pull,目标镜像的 tag、平台和 digest 是否与你预期一致。
Docker 官方文档在 docker image pull 页面写得很清楚:未写 registry 时默认从 Docker Hub 拉,未写 tag 时默认用 latest;daemon 默认同时下载 3 个镜像层,慢链路下可以把并发降下来。这个默认行为决定了排查不能只盯最后一行 timeout。
用几条命令把问题分层
先拿最小镜像和目标镜像做对照,别一上来改代理。下面 5 条命令能把问题切成 daemon、registry、认证、平台和镜像内容几层。
# 1. 客户端和服务端版本,确认 CLI 连到哪一个 daemon
docker version
# 2. daemon 配置摘要,重点看 Registry Mirrors、HTTP Proxy、HTTPS Proxy、No Proxy
docker info
# 3. 最小镜像连通性:失败通常不是业务镜像问题
docker pull hello-world:latest
# 4. 目标镜像固定 tag,不要用 latest 做故障样本
docker pull node:20-bookworm-slim
# 5. 多平台对照:Apple Silicon、arm64 服务器、amd64 CI 都要写清楚
docker pull --platform linux/amd64 node:20-bookworm-slim
| 现象 | 第一判断 | 下一步 |
|---|---|---|
hello-world 也超时 | daemon 到 registry 的链路有问题 | 查 DNS、daemon proxy、Docker Status、公司网络 |
| 小镜像成功,大镜像卡住 | 层大小、并发下载或出口带宽问题 | 看具体卡在哪一层,考虑降低 max-concurrent-downloads |
| 公共镜像成功,私有镜像失败 | 认证、scope 或私有 registry 策略 | 查 docker login、401 challenge、仓库权限 |
| 本机成功,CI 失败 | runner 出口和缓存不同 | 查 CI 网络、registry mirror、BuildKit cache |
| amd64 成功,arm64 失败 | manifest/platform 问题 | 查 manifest list 和 --platform |
如果日志里出现 context deadline exceeded、Client.Timeout exceeded while awaiting headers、net/http: TLS handshake timeout,按网络链路排查;如果是 unauthorized、denied、manifest unknown、no matching manifest,不要把它当成 timeout 处理。
DNS 和 Docker Hub 状态
DNS 要查两个域名:registry-1.docker.io 负责 registry 请求,auth.docker.io 负责 token 认证。很多公司网络只放行前者,结果日志看起来像 pull 超时,实际是 token 换取失败。
# macOS / Linux 都可先用 nslookup;有 dig 时再看解析耗时和返回记录
nslookup registry-1.docker.io
nslookup auth.docker.io
dig registry-1.docker.io
dig auth.docker.io
# Docker registry v2 未带 token 时返回 401 是正常挑战,不是直接失败
curl -I https://registry-1.docker.io/v2/
curl -I https://registry-1.docker.io/v2/ 正常情况下可能返回 401 Unauthorized,并带 WWW-Authenticate。这不是密码错,而是 registry 要求客户端去 token 服务换 Bearer token。真正异常是 DNS 解析慢、TLS 握手失败、公司代理替换证书后 Docker daemon 不信任,或 auth.docker.io 完全不可达。
Docker Status 也要看,但不要只看一句绿色状态。2026-05-22 抓取时,Docker Status 可见为 All Systems Operational,同时页面列出了 2026-05-28 的计划维护窗口,可能涉及 registry 和 web services。线上故障排查要把 status 页截图或时间戳记进 incident,不要事后凭记忆判断。
daemon 代理、CLI 代理和 NO_PROXY
docker pull 是 Docker daemon 下载镜像,不是当前 shell 直接下载文件。Docker CLI 的代理配置、容器里的环境变量、BuildKit 的 build args 都不能自动替代 daemon 出口。
| 配置位置 | 主要影响 | 不影响什么 | 常见误判 |
|---|---|---|---|
| Docker daemon proxy | docker pull、docker push、daemon 访问 registry | 已运行容器里的业务请求 | 只在 shell 里 export 变量后期待 pull 变好 |
| Docker Desktop 代理设置 | Desktop 管理的 daemon 出口 | Linux 服务器 daemon | 把 /etc/docker/daemon.json 经验搬到 Desktop |
~/.docker/config.json 的 proxies | 新容器和 docker build 的代理变量 / build args | Docker Engine 本身 | 以为它能修复 pull timeout |
docker run -e HTTP_PROXY | 单个容器进程 | daemon 拉镜像 | 容器能访问外部服务,但 pull 仍失败 |
Dockerfile ENV HTTP_PROXY | 镜像运行环境 | 安全地配置构建代理 | 把内部代理地址写进镜像历史 |
Linux 服务器可以在 daemon 配置里写代理。Docker 文档说明 daemon.json 里的 proxies 优先级高于启动环境变量。
{
"proxies": {
"http-proxy": "http://proxy.example.com:3128",
"https-proxy": "http://proxy.example.com:3128",
"no-proxy": "localhost,127.0.0.1,.corp.example.com,registry.corp.example.com"
},
"max-concurrent-downloads": 2
}
sudo systemctl restart docker
sudo systemctl show --property=Environment docker
docker info | grep -i -E 'proxy|registry'
Rootless Docker 的 systemd drop-in 在 ~/.config/systemd/user/docker.service.d/,命令要用 systemctl --user。Docker Desktop 不读取 daemon.json 里的 proxy 值,代理要从 Desktop 设置页改。NO_PROXY 里必须包含内网 registry、localhost、公司域名后缀;写得过宽会让外部 registry 也直连,写得太窄会让内网 registry 被代理转发。
registry 认证和镜像名
Docker Hub 的官方用量文档给了两个容易混淆的信号:未登录用户按 IPv4 地址或 IPv6 /64 子网计算,限制为每 6 小时 100 pulls;Personal 登录用户是每 6 小时 200 pulls;高频请求还可能触发 abuse limiter,表现为 429 Too Many Requests。
429 不是 timeout。处理顺序是登录、减少重复 pull、使用 CI cache、复用基础镜像,再考虑 registry mirror。
# 查看当前认证文件里有哪些 registry,不要把 token 打到日志里
jq '.auths | keys' ~/.docker/config.json
# 对 Docker Hub 明确登录
docker login registry-1.docker.io
# 对 GHCR、私有 registry 写完整 registry 名
docker login ghcr.io
docker pull ghcr.io/owner/image:tag
Registry v2 的认证流程通常是:registry 返回 401 Unauthorized 和 WWW-Authenticate: Bearer realm=...,service=...,scope=repository:NAME:pull,客户端再去认证服务换 token,随后带 Authorization: Bearer ... 重试。排查时看 scope 是否指向正确仓库,比反复输密码更有效。
镜像名也要写全。alpine 实际会被解析成 docker.io/library/alpine:latest;私有 registry 应写成 registry.example.com/team/app:2026-05-22。团队 runbook 里建议记录完整镜像名、tag、registry、账号类型和失败时间。
tag、multi-arch manifest 和 digest
latest 适合本地临时试验,不适合 CI 故障样本。Docker 文档说明,不写 tag 就用 latest;而 digest pull 会固定到一个确切镜像版本,不会自动取 tag 后续更新。
# 看 tag 背后的多平台 manifest
docker buildx imagetools inspect node:20-bookworm-slim
# 固定平台拉取,排除 host 自动选择差异
docker pull --platform linux/amd64 node:20-bookworm-slim
# 拉取 digest,确认 CI 和本地拿到同一份内容
docker pull node@sha256:REPLACE_WITH_REAL_DIGEST
# 查看本地镜像 RepoDigests
docker image inspect node:20-bookworm-slim --format '{{json .RepoDigests}}'
多平台镜像背后是 manifest list:列表指向 linux/amd64、linux/arm64 等不同 manifest,每个 manifest 又有自己的 config 和 layers。Docker 会按主机平台自动选择变体,所以 Apple Silicon、本地 amd64 Linux、arm64 云服务器、CI runner 可能拉到不同内容。
| 错误或差异 | 更可能的原因 | 处理动作 |
|---|---|---|
manifest unknown | tag 不存在或 registry 路径写错 | 查官方 tag 列表,写完整镜像名 |
no matching manifest for linux/arm64 | 镜像没有该平台变体 | 指定 --platform linux/amd64 或换支持 arm64 的镜像 |
| 本地和 CI digest 不同 | tag 漂移或平台不同 | 固定 digest,并记录 TARGETPLATFORM |
| 只在 QEMU 构建时很慢 | emulation 成本高 | 用原生 builder node 或交叉编译 |
checksum/digest 校验的意义不是加速,而是证明内容一致。CI 中最好把基础镜像写成 name@sha256:...,并在失败日志里保留 Digest: sha256:...。如果 tag 被上游重推,digest 会让差异直接暴露出来。
BuildKit 和 docker pull 的差异
docker pull 只验证 daemon 能取镜像;BuildKit 还要处理 build context、Dockerfile stage、缓存导入、并行构建和 build args。Docker BuildKit 文档说明,它可以跳过未使用阶段、并行构建独立 stage,只传变化的 context,并用 LLB checksum 追踪缓存。
# 把 BuildKit 日志打平,避免只看到最后一行失败
docker buildx build --progress=plain --pull .
# 指定平台,观察是不是某个平台的基础镜像失败
docker buildx build --platform linux/amd64 --progress=plain --pull .
# 只做一次诊断对照,不要长期关闭 BuildKit
docker build --progress=plain --pull .
BuildKit 失败时要看卡在哪个阶段:load metadata for docker.io/library/node 失败多半是基础镜像或 registry;RUN npm install 失败是容器内包管理器网络;importing cache manifest 失败则是远端 cache registry 或权限。把这些都叫成 Docker pull timeout,会让团队去改错配置。
CLI proxy 的 ~/.docker/config.json 对 build 有用,因为它会把代理作为新容器环境变量和 build args 注入;但 Docker 文档也提醒,代理值可能以明文进入容器配置或被 docker commit 捕获,不要写进 Dockerfile ENV。
retry、timeout 和公司网络的 runbook 写法
Docker CLI 没有一个通用的 docker pull --timeout 300 能把所有网络问题盖过去。慢链路下更可靠的做法是降低 daemon 并发、固定 tag/digest、减少重复下载,并把 retry 做成有上限的外层脚本。
image="node:20-bookworm-slim"
for i in 1 2 3; do
docker pull "$image" && break
sleep $((i * 5))
done
公司网络要单独写一页 registry 规则,不要散在 Slack 里。最少包含这些字段:DNS 解析器、允许访问的 registry 域名、是否有 TLS inspection、Docker Desktop 代理入口、Linux daemon 配置入口、NO_PROXY 样例、私有 registry 证书安装方式、Docker Hub 账号类型、CI runner 出口网络。
如果团队有多台服务器或 runner 重复拉 Docker Hub 公共镜像,可以考虑 Docker Hub registry mirror。官方文档说明,pull-through cache 第一次仍要从 Docker Hub 回源,后续才从本地缓存返回;它只镜像 central Hub,不等于私有 registry、GHCR、ECR 都会被同一套 mirror 覆盖。
还没恢复时的排查
如果你在共享办公、出差网络或远程机器上同时维护 GitHub Actions、Cloudflare、Docker Hub 和云控制台,故障会混在浏览器会话、CLI 认证和 daemon 出口里。可以用海外服务跑 GitHub Actions / Cloudflare 的稳定线路承载控制台查看和部署协作,但镜像下载本身仍要回到 daemon proxy、registry cache、认证和 digest 固定来解决。
升级给同事或平台支持时,别只发一张 timeout 截图。把下面信息贴全,基本能省掉两轮来回。
时间:2026-05-22 14:35 UTC+8
环境:macOS 15 / Docker Desktop 4.x / Apple Silicon,或 Ubuntu 24.04 + Docker Engine
命令:docker pull --platform linux/amd64 node:20-bookworm-slim
镜像:docker.io/library/node:20-bookworm-slim
结果:卡在 layer sha256:... 或返回 429 / 401 / TLS handshake timeout
DNS:registry-1.docker.io、auth.docker.io 解析结果
代理:Docker Desktop 设置或 daemon.json proxies
认证:docker login 的 registry,不贴 token
状态:Docker Status 当时截图或文字
Digest:成功机器上的 RepoDigests
如果 hello-world、目标镜像和 registry v2 探测全失败,查 DNS、Docker Status、daemon 代理和公司网络。只有目标镜像失败,优先查私有仓库权限、tag、multi-arch manifest、digest 和镜像层大小。只有 build 失败,进入 BuildKit stage、cache registry 和 Dockerfile 中的包管理器网络。