Docker镜像拉取失败怎么办?仓库地址、认证与网络故障分析

docker pull 卡在 Pulling fs layer、报 net/http: TLS handshake timeout、或者抛出 unauthorized: authentication required——镜像拉取失败是运维日常里出现频率最高的 Docker 故障之一。它可能来自网络、DNS、仓库地址、认证凭据、磁盘空间甚至时间同步,表面上都是「拉不下来」,处理思路却完全不同。

很多新手遇到拉取失败就习惯性换镜像源,结果有时能通有时不通,问题反复出现。其实只要按「网络层 → 传输层 → 仓库认证 → 本地存储」这个顺序逐层排除,绝大多数问题能在几分钟内定位。

本文会先讲清 Docker 拉取镜像的完整流程,再给出一套分层的排查命令,最后覆盖私有仓库认证、代理配置、磁盘与 inode 不足等常见坑位,并提供可复制的修复步骤和回滚方式。

一、先理解 docker pull 做了什么

一次 docker pull 大致分四步:
1. 解析镜像名,得到仓库地址(Registry)、命名空间、仓库名和标签;
2. 向 Registry 发起请求,获取镜像 Manifest(清单);
3. 按 Manifest 中的 digest 逐层下载 Layer(层),每层都做校验;
4. 解压写入本地存储目录(默认 /var/lib/docker)。

任何一步失败都会中断。把这四步和错误信息对应起来,定位就快:
- 第 1、2 步失败 → 多半是 DNS、网络、仓库地址问题;
- 认证相关报错 → 是凭据问题;
- 下载中途失败 → 传输、代理或 MTU 问题;
- 写盘失败 → 磁盘空间或 inode 不足。

常见报错与阶段对照:

报错关键字 所处阶段 大概率原因
no such host 解析仓库地址 DNS 失败
TLS handshake timeout 建立连接 网络受阻/代理未配
connection refused 建立连接 仓库端口不可达
unauthorized 认证 未登录或凭据过期
denied: requested access 权限 账号无该仓库权限
no space left on device 写盘 磁盘/inode 满
EOF / unexpected EOF 下载层 传输中断/MTU

二、第一步:确认网络与 DNS

先确保基础网络没问题:

# 解析仓库域名
getent hosts registry-1.docker.io

# 测试端口连通性(443)
curl -v --connect-timeout 5 https://registry-1.docker.io/v2/

# 若返回 401 且带 WWW-Authenticate 头,说明网络通、只是需要认证,属正常

如果 getent hosts 失败,是 DNS 问题,参考容器 DNS 排查思路处理宿主机解析。若解析正常但连不上 443,检查出口防火墙、代理和 NAT。

在宿主机层面也可以直接用容器验证网络:

docker run --rm curlimages/curl -sv --connect-timeout 5 https://registry-1.docker.io/v2/

三、配置代理(企业内网最常见)

如果宿主机走 HTTP 代理出网,Docker 守护进程本身并不会自动读取 shell 的 http_proxy,必须单独配置 systemd 环境变量。

针对 docker.service 创建 drop-in:

sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/http-proxy.conf >/dev/null <<'EOF'
[Service]
Environment="HTTP_PROXY=http://proxy.example.com:3128"
Environment="HTTPS_PROXY=http://proxy.example.com:3128"
Environment="NO_PROXY=localhost,127.0.0.1,10.0.0.0/8,192.168.0.0/16,.internal"
EOF

sudo systemctl daemon-reload
sudo systemctl restart docker

验证环境变量是否生效:

sudo systemctl show docker --property=Environment

风险提示:重启 Docker 会中断运行中的容器,请安排在维护窗口;NO_PROXY 一定要包含内网网段和私有仓库域名,否则内网拉取会被错误地走代理。回滚方式是删除该 drop-in 文件后 daemon-reload 并重启。

同时,容器内的程序若也要走代理,需在 ~/.docker/config.json 中配置:

{
  "proxies": {
    "default": {
      "httpProxy": "http://proxy.example.com:3128",
      "httpsProxy": "http://proxy.example.com:3128",
      "noProxy": "*.internal,10.0.0.0/8"
    }
  }
}

四、私有仓库认证

访问企业私有仓库(如 Harbor、云厂商的容器镜像服务)通常需要先登录:

docker login registry.internal.example.com -u <用户名>

# 查看已登录的凭据(凭据经过 base64,属于敏感信息,注意权限)
cat ~/.docker/config.json

认证类报错的处理:
- unauthorized: authentication required:没登录或 token 过期,重新 docker login 即可;
- denied: requested access to the resource is denied:账号没有该仓库的 pull 权限,找管理员授权或换账号;
- 登录成功但 pull 仍报 401:检查机器时间是否准确,token 有效期受时间影响。

时间偏移是很隐蔽的原因,用下面的命令确认:

timedatectl status
# 若时间偏差较大,开启 NTP 同步
sudo timedatectl set-ntp true

另外,配合 Kubernetes 时凭据放在 imagePullSecrets 里:

kubectl create secret docker-registry regcred \
  --docker-server=registry.internal.example.com \
  --docker-username=<用户名> \
  --docker-password='<密码或token>' \
  --docker-email=ops@example.com

在 Deployment 中引用:

spec:
  template:
    spec:
      imagePullSecrets:
        - name: regcred

五、磁盘与 inode:容易被忽视的写盘失败

镜像层解压会占用大量空间。一次拉取失败如果报 no space left on device,先看空间:

df -h /var/lib/docker
df -i /var/lib/docker

df -h 看容量,df -i 看 inode。有时候容量还有富余,但 inode 用尽,同样报这个错。清理时优先删无用镜像和停止的容器:

# 清理未使用的镜像、容器、网络(谨慎,会删掉未使用的资源)
docker system prune

# 只清理悬空镜像
docker image prune

# 查看占用
docker system df -v

风险提示:docker system prune -a 会删除所有未被容器引用的镜像,包括你手动拉取的常用镜像,生产环境执行前务必确认现有容器不依赖这些镜像。回滚手段有限——被删的镜像只能重新拉取,因此建议先 docker images 导出清单留档,必要时用 docker save 备份关键镜像。

如果确实磁盘紧张,可迁移数据目录到更大的盘。编辑 /etc/docker/daemon.json:

{
  "data-root": "/data/docker"
}

迁移步骤(需先停 Docker):

sudo systemctl stop docker
sudo rsync -aHAX --info=progress2 /var/lib/docker/ /data/docker/
sudo systemctl start docker
docker info | grep "Docker Root Dir"

风险提示:迁移期间不要有其他进程写 /var/lib/docker,rsync 完成后先保留原目录一段时间再删除,确认无误后再清理,避免数据丢失。

六、镜像加速与仓库地址

在国内拉取公共镜像慢或超时,可以配置镜像加速器(Registry Mirror)。编辑 /etc/docker/daemon.json:

{
  "registry-mirrors": [
    "https://<你的加速地址>"
  ]
}

配置后重启:

sudo systemctl daemon-reload
sudo systemctl restart docker
docker info | grep -A3 "Registry Mirrors"

需要说明的是,加速器地址由各服务商提供且可能随时间调整,请以官方最新说明为准,不要长期依赖来历不明的第三方地址。对于私有仓库,直接使用其完整域名拉取即可,如 docker pull registry.internal.example.com/team/app:1.0。

另外,insecure-registries 用于自签名证书的私有仓库,配置后允许走 HTTP:

{
  "insecure-registries": ["registry.internal.example.com:5000"]
}

风险提示:允许明文 HTTP 传输意味着凭据和镜像可能被窃听,仅在受信任的内网使用,长期应部署正规 TLS 证书。

七、修改仓库地址后的验证与回滚

改完 daemon.json 后统一做一遍验证:

# 校验 JSON 语法,避免写错导致 daemon 起不来
python3 -m json.tool /etc/docker/daemon.json

sudo systemctl restart docker
docker info | grep -E "Registry|Root Dir|Proxy"
docker pull alpine:3.19

如果 Docker 起不来,通常是 daemon.json JSON 语法错误。此时查看日志:

journalctl -u docker --no-pager -n 50

回滚就是把 daemon.json 恢复成修改前的内容(建议改动前先 cp /etc/docker/daemon.json{,.bak}),再重启。

八、为什么拉取失败常常「时好时坏」

很多人反馈:同一个镜像,早上拉不下来,下午重试就好了;或者一台机器能拉,另一台就不行。这种「时好时坏」最容易让人误判成 Docker 的 bug,其实背后往往是几个可控因素的叠加。

一是镜像层的并发下载。Docker 默认会同时开多个连接下载不同的层,层数越多、并发越高,对出口带宽和代理连接数的压力就越大。当代理的最大连接数被打满,或出口带宽在小文件突发时被打满,就会出现部分层下载超时,表现为整体拉取失败。可以通过守护进程参数收敛并发:

{
  "max-concurrent-downloads": 3,
  "max-download-attempts": 5
}

max-concurrent-downloads 控制同时下载的层数,网络条件一般的内网把它降到 3 到 5 往往更稳;max-download-attempts 控制单层重试次数,在网络抖动频繁的环境适当提高。

二是大层与小层的差异。镜像里动辄几百 MB 的层(比如把一堆依赖打包进一个层)对链路稳定性要求高,一个小丢包就可能触发重传甚至超时;而小的配置层几乎不受影响。这也是为什么「有的镜像能拉、有的不能」——不一定是仓库不一样,可能是镜像层构成不一样。构建镜像时把不常变的大依赖放到前面的层、让层尺寸更均匀,能间接提升拉取稳定性。

三是仓库侧的限流与 CDN 回源。公共镜像仓库对匿名拉取通常有限流策略,高峰时段会出现 429 之类的限流响应;而使用加速器时,如果加速器缓存未命中需要回源,回源链路慢也会让整体变慢甚至失败。这类问题靠重试确实能缓解,但长期看应该通过私有仓库或镜像预热来规避。

理解这些因素后,遇到「时好时坏」的拉取失败,先别急着改配置,可以按「降低并发 → 检查代理连接数 → 检查是否限流(看返回码)→ 考虑预热镜像」的顺序处理,通常能稳定下来。

常见问题

Q1:docker pull 报 TLS handshake timeout,重启 Docker 就好了,过一会又出现。
A:说明网络链路不稳定或代理连接数受限。检查代理最大连接数、出口带宽,考虑给 daemon.json 配置更合理的 max-concurrent-downloads,降低并发。

Q2:宿主机 curl 能访问仓库,但 docker pull 不行。
A:Docker 守护进程不继承 shell 代理,需按第三节给 docker.service 配置 systemd 环境变量。

Q3:unauthorized 和 denied 有什么区别?
A:unauthorized 是没通过认证(没登录/凭据过期),denied 是通过了认证但没有该资源的访问权限,前者重新登录,后者要找管理员授权。

Q4:Kubernetes 里 Pod 报 ImagePullBackOff,和 Docker pull 失败一样排查吗?
A:思路一致,但要先确认是节点级问题还是 Pod 级问题。用 kubectl describe pod 看 Events,常见原因是 imagePullSecrets 缺失或节点无法访问仓库。

Q5:no space left on device,但 df -h 显示还有空间?
A:检查 inode,用 df -i。也可能是被删除但未释放的文件占用,用 lsof | grep deleted 排查。

总结

  1. 按「网络 → 传输 → 认证 → 存储」四层顺序排查,把报错关键字对应到具体阶段,效率最高。
  2. Docker 守护进程不继承 shell 代理,企业内网务必给 docker.service 配 systemd 代理环境变量,并正确设置 NO_PROXY。
  3. 私有仓库认准三件事:登录状态、账号权限、机器时间同步。
  4. no space left on device 要先分容量还是 inode,清理前用 docker system df -v 看清占用,谨慎使用 system prune -a。
  5. 任何 daemon.json 改动前先备份,改完用 python3 -m json.tool 校验语法,出了问题看 journalctl -u docker 并快速回滚。