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 exceededClient.Timeout exceeded while awaiting headersnet/http: TLS handshake timeout,按网络链路排查;如果是 unauthorizeddeniedmanifest unknownno 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 proxydocker pulldocker push、daemon 访问 registry已运行容器里的业务请求只在 shell 里 export 变量后期待 pull 变好
Docker Desktop 代理设置Desktop 管理的 daemon 出口Linux 服务器 daemon/etc/docker/daemon.json 经验搬到 Desktop
~/.docker/config.jsonproxies新容器和 docker build 的代理变量 / build argsDocker 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 UnauthorizedWWW-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/amd64linux/arm64 等不同 manifest,每个 manifest 又有自己的 config 和 layers。Docker 会按主机平台自动选择变体,所以 Apple Silicon、本地 amd64 Linux、arm64 云服务器、CI runner 可能拉到不同内容。

错误或差异更可能的原因处理动作
manifest unknowntag 不存在或 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 中的包管理器网络。

相关阅读