
在Ubuntu 20.04上折腾Docker部署最让人血压升高的一个环节就是docker pull死活拉不下来。镜像名敲了无数遍网络也反复确认过可报错该出还是出超时、DNS解析失败、manifest not found、权限拒绝甚至明明昨天还拉得好好的镜像今天突然就toomanyrequests。这篇文章把我这些年遇过的 pull 失败原因、从环境、网络、认证到磁盘的一次完整排查流程以及最后能落地的部署验证整理成文。如果你正在被docker pull折磨照着下面的思路走一遍大概率能把你从“玄学排错”里捞出来。1. 先别急着改配置把失败场景分清楚遇到 pull 失败第一反应应该是判断“当前到底在哪一步失败”而不是立刻去改daemon.json。很多人在这一步就开始瞎试结果是配置文件越改越乱问题却没解决。我习惯把 pull 失败分成三类场景每一类的排查路径完全不同。1.1 全新环境首次部署的失败这类场景最常见尤其是刚装完 Ubuntu 20.04、docker 也是新装的人。表现是docker pull一执行就报Cannot connect to the Docker daemon at unix:///var/run/docker.sock或者permission denied while trying to connect to the Docker daemon socket。这类问题跟网络一点关系都没有纯粹是 docker 服务没起、没设置开机自启、或者当前用户不在 docker 组里。判断方法也简单先跑systemctl status docker --no-pager -l看服务状态再跑docker version看 client 和 server 是否都正常返回。如果 server 段报错就是服务端问题如果docker ps能跑但docker pull失败才轮到后面说的网络和配置问题。1.2 pull 过程中的网络与仓库问题这是“重灾区”。表现五花八门常见的有Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting for connectiondial tcp: lookup registry-1.docker.io on 127.0.0.53:53: server misbehavingi/o timeoutdenied: requested access to the resource is deniedmanifest unknown这一类的根因通常集中在网络连通性、DNS解析、镜像源配置、Docker Hub 限流和认证这几个点上。下面第2、3部分会详细拆。1.3 镜像pull成功但运行失败有一部分人被“pull失败”这几个字带偏了实际上镜像已经拉下来了只是docker run起不来于是误认为是 pull 阶段的问题。比如拉 MySQL 镜像后容器启动就退出或者端口冲突。遇到这种情况第一步应该是docker images确认镜像是否存在再用docker logs container看日志而不是反复去 pull 同一个镜像。这三类场景定下来之后再去看报错思路就清晰了。我下面按照“从环境到网络再到认证/存储”的顺序把原理讲透再给完整实操。2. 深入拆解为什么 docker pull 会失败要解决一个问题先搞清楚它的工作链路。docker pull并不是客户端直接去镜像仓库下载而是客户端把请求发给本机的 docker daemon由 daemon 去完成所有下载工作。也就是说docker pull 真正走的网络路径是 daemon 的网络不是你终端 shell 的网络。这个区别极其重要很多人在浏览器里能打开 Docker Hub就直接断定网络没问题结果 daemon 那边根本连不通这中间就存在认知偏差。2.1 pull 的完整链路执行docker pull nginx:latest时大体经历这几步客户端将请求通过 unix socket 发给/var/run/docker.sock上的 docker daemon。daemon 解析镜像名nginx补齐默认 registry 地址为registry-1.docker.io即 Docker Hub以及默认 taglatest。daemon 向 registry 发起 HTTPS 请求拿到镜像的 manifest 清单。daemon 根据 manifest 中的层信息逐个下载镜像层 blob 到本地/var/lib/docker。下载完成后解压、校验、生成最终镜像。这五步任何一步出问题表现都是docker pull失败但报错信息完全不同。所以“看报错关键词”比“百度整句报错然后照抄命令”更靠谱。2.2 网络与 Docket Hub 可达性问题Docker Hub 的默认仓库域名是registry-1.docker.io国内直连时经常会出现连接超时主要原因就是链路上有大量中间节点时不时就给你一个 timeout。这种问题纯属网络质量和配置本身没关系。解法就是给 docker daemon 配置registry-mirrors也就是常用的镜像加速地址。它的原理是让 daemon 先访问一个网络质量更好的 registry 镜像服务由那个服务再从 Docker Hub 转发数据相当于在 pull 链路上加了一个“中转站”。需要注意镜像加速是 docker 官方支持的配置项不是旁门左道但网上随便抄来的加速地址很多已经失效配了反而会让 pull 更慢因为 daemon 会逐个尝试、每次都等到超时才切换下一个。我的建议是优先使用云厂商提供的镜像加速地址比如阿里云容器镜像服务控制台里会为每个账号生成专属的加速地址格式一般是https://你的ID.mirror.aliyuncs.com。这类地址需要登录控制台查看不在控制台里、没有绑定到你账号的加速地址很可能不可靠。另外像中科大的 docker 镜像站这类历史上有名的公共源目前可用性也经常变化使用前先确认。2.3 DNS 解析失败lookup registry-1.docker.io on 127.0.0.53:53: server misbehaving这类报错问题在 DNS。Ubuntu 20.04 默认用 systemd-resolved/etc/resolv.conf的 nameserver 往往是127.0.0.53。如果这个上游 DNS 配置有问题或者 DNS 被网络环境影响daemon 就解析不了registry-1.docker.iopull 自然失败。排查方式很简单nslookup registry-1.docker.io看能不能正常返回 IP。如果解析超时或返回错误就需要换 DNS。可以先在/etc/docker/daemon.json里给 daemon 指定dns字段也可以直接改系统 DNS。但要注意改了系统 DNS 后 systemd-resolved 可能又给你改回去所以最省事的是直接在 daemon 配置里写死 DNS。2.4 认证与限流很多人不知道Docker Hub 对匿名拉取有限流策略具体是每个 IP 每 6 小时只能拉有限次数的镜像拉多了就会报toomanyrequests: You have reached your pull rate limit。如果你在一个共享出口 IP 的网络环境里比如公司或学校机房几个人一起拉镜像很容易触发限流。解决方法是执行docker login登录 Docker Hub 账号后再 pull登录用户的额度比匿名高不少。如果是拉私有仓库镜像那本来就必须要先docker login否则报错是denied: requested access to the resource is denied或unauthorized。这里也顺便说一句如果遇到pull model manifest: file does not exist这类报错那其实是 ollama 等 AI 模型工具的拉取错误和 docker 的 pull 链路不一样别用 docker 的排查套路去处理。2.5 磁盘空间pull 镜像需要把镜像层下载到本地存储默认路径是/var/lib/docker根分区不够就会出现write /var/lib/docker/tmp: no space left on device或failed to register layer: no space left on device。这类错误经常在部署 MySQL、Redis 等数据型容器时出现因为镜像本身不大但容器数据文件、日志文件增长很快把磁盘占满了。排查用df -h和docker system df。3. 实操一条龙把 pull 修好下面这套流程是我在 Ubuntu 20.04 上反复验证过的跟着做就好每步我会解释为什么这么做。3.1 环境检查三件套先确认 docker 本体是好的别一上来就改配置。docker version重点看输出里Server部分有没有内容。如果只有 Client没有 Server说明 daemon 没起来。再看服务状态sudo systemctl status docker --no-pager -l如果服务不在 running 状态先启动并设置开机自启sudo systemctl enable --now docker这里有个经验Ubuntu 20.04 上如果 docker 服务一直启动失败先看journalctl -u docker --no-pager -n 50很多情况是/etc/docker/daemon.json写错导致 daemon 起不来。这个问题我在 4.2 节会单独讲。最后看 daemon 的全局信息docker info重点看这几项Storage Driver是不是 overlay2、Docker Root Dir在哪个分区、Registry Mirrors有没有配置镜像源。如果docker info能看到 Registry Mirrors说明 daemon 配置已经生效看不到就说明配置没加载或没有配置。3.2 配置 daemon.json镜像源与 DNS如果环境正常但 pull 还是超时那就进入配置环节。先看当前有没有daemon.jsoncat /etc/docker/daemon.json这个文件可能不存在不存在就创建一个。配置文件是严格的 JSON 格式不能有注释、不能有多余逗号写错一个字符 docker 直接起不来。我用一个相对完整的配置示例{ registry-mirrors: [ https://你的专属ID.mirror.aliyuncs.com ], dns: [ 223.5.5.5, 114.114.114.114 ], log-driver: json-file, log-opts: { max-size: 10m, max-file: 3 } }逐个解释一下registry-mirrors镜像加速地址数组。建议只配 1-2 个确认可用的不要堆一堆失效地址。daemon 会按顺序尝试一个超时再试下一个坏地址越多越慢。dns给 daemon 用的 DNS223.5.5.5是阿里 DNS114.114.114.114是国内常用公共 DNS。如果你们内网有自己的 DNS可以写在前面但一定要保证它能解析公网域名。log-driver和log-opts限制容器日志文件大小防止容器日志把磁盘写满。单文件最大 10M保留 3 个文件。这个配置虽然和 pull 没有直接关系但能避免以后出现“因为磁盘被日志占满而 pull 失败”的连锁问题。改完配置最重要的一步是重启 daemon否则改动不生效sudo systemctl daemon-reload sudo systemctl restart docker然后验证镜像源是否生效docker info | grep -i -A2 registry mirrors能看到刚才配置的地址就说明 daemon 已经读取了配置。这一步做完后基本能解决大部分“连接超时”类的 pull 失败。3.3 验证网络与 DNS如果配置了镜像源还是失败再单独看 DNS 和网络。nslookup registry-1.docker.io如果nslookup没安装可以用getent hosts registry-1.docker.io代替。解析正常会返回几个 IP。如果解析超时或报错说明 DNS 确实有问题。此时检查一下/etc/resolv.confcat /etc/resolv.conf如果 nameserver 是127.0.0.53这是 systemd-resolved 的本地转发地址本身不是坏事关键是它上游的 DNS 配置。检查/etc/systemd/resolved.conf看DNS字段有没有设置。如果没设置系统会退回到自动获取的 DNS如果自动获取的 DNS 本身不可用解析就会失败。我的做法是直接在daemon.json里写死 DNS上面已经给了配置这样不依赖系统 DNSdocker 自己用指定的 DNS 请求。改完restart docker再 pull 测试。另外如果本机开了全局代理docker daemon 并不会自动走你 shell 里的代理环境变量反而可能出现“浏览器能访问但是 daemon 连不上”的怪现象。排查的时候可以在一个干净的网络环境下试 pull排除网络环境干扰但这里不展开代理配置多数个人机器用不到。3.4 最小化 pull 验证配置改完后用最小镜像验证docker pull hello-worldhello-world只有几 KB是验证 pull 链路的最好工具。如果它拉下来了说明 daemon 能正常访问 registry基础网络没问题。如果这一步也失败把报错原文记下来对照第 4 节的速查表找原因。再测一个稍大一点的常用镜像docker pull nginx:alpine注意我特意写了alpine标签不写latest。因为latest可能指向一个体积大、且 tag 变化频繁的镜像测试时不稳定。nginx:alpine体积小、常见、tag 明确适合做连通性测试。如果nginx:alpine能拉下来说明 pull 的基础链路完全正常剩下的就是具体镜像的问题比如 tag 不存在、架构不对、限流、认证等。3.5 用一个真实部署场景串起来光验证 hello-world 还不够我拿一个高频场景——部署 MySQL 8.0——把整个流程串一遍这样你能看到 pull 成功之后还要注意什么。docker pull mysql:8.0如果这一步超时回到 3.2 检查镜像源如果报manifest unknown去 Docker Hub 的 tags 页面确认8.0这个 tag 是否存在。拉下来之后启动容器docker run -d \ --name mysql8 \ --restartalways \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD你的密码 \ -v /opt/mysql8/data:/var/lib/mysql \ mysql:8.0参数说明-d后台运行--restartalways保证重启后自动拉起-p 3306:3306映射端口-e MYSQL_ROOT_PASSWORD设置密码-v /opt/mysql8/data:/var/lib/mysql把数据目录挂载到宿主机防止删除容器后数据丢失。启动后重点不是立刻docker ps而是看日志docker logs -f mysql8MySQL 首次初始化通常要几十秒日志里出现ready for connections才代表真正起来。如果日志停在某个地方不动或出现[ERROR]再用docker logs mysql8 | tail -20看最后几行定位问题。同样的思路部署 Redisdocker pull redis:7.0 docker run -d \ --name redis7 \ --restartalways \ -p 6379:6379 \ -v /opt/redis7/data:/data \ redis:7.0 \ redis-server --appendonly yes这个案例说明一件事pull 成功只是第一步容器能不能正常工作取决于很多运行期配置。但 pull 反复失败的时候先把 pull 链路打通再谈运行期的事。4. 高频报错速查表与避坑经验把常见报错整理成一张表方便你对照排查。这算是我排障时最喜欢用的方式看到错误关键词直接找对应方向不用每次从头猜。4.1 错误关键词对照表报错关键词主要原因解决方向Cannot connect to the Docker daemon at unix:///var/run/docker.sockdocker 服务没启动sudo systemctl start docker再查journalctl -u dockerpermission denied while trying to connect to the Docker daemon socket当前用户不在 docker 组sudo usermod -aG docker $USER重新登录net/http: request canceled while waiting for connection网络超时测试连通性配置registry-mirrorsdial tcp: lookup registry-1.docker.io ... server misbehavingDNS 解析失败在daemon.json里配置dnsmanifest unknown/no matching manifest for linux/amd64tag 不存在或架构不对去 Docker Hub tags 页面确认 tag 和架构denied: requested access to the resource is denied私有镜像未登录或无权限docker login确认镜像是否公开unauthorized: incorrect username or password登录凭证错误检查~/.docker/config.json重新 logintoomanyrequests: You have reached your pull rate limitDocker Hub 限流登录 Docker Hub 账号或换镜像源write ...: no space left on device磁盘满docker system prune扩容磁盘failed to register layer: no space left on device磁盘满清理/var/lib/docker检查日志大小i/o timeout网络或防火墙拦截检查防火墙规则换干净网络测试4.2 我的几个独家避坑经验daemon.json 写错导致 docker 直接起不来这个坑我踩过不止一次。JSON 里多了个逗号、漏了个引号daemon 启动直接失败systemctl status docker看到的是 red 状态docker version只剩 Client 没有 Server。改完配置文件后先做一次 JSON 校验再重启python3 -m json.tool /etc/docker/daemon.json如果输出正常说明语法没问题再重启 docker。千万别改完就systemctl restart docker一旦语法错误很可能连docker ps都跑不了排查起来更被动。镜像加速地址不是越多越好网上搜到的加速地址很多都已失效堆在一起只会让 pull 更慢因为 daemon 会逐个尝试每个都要等超时。我见过有人配了六七个地址导致一个本来秒拉的镜像等了整整一分钟。只保留 1-2 个确认可用的地址就够了。验证地址可用性的方法是curl -I https://你的加速地址/v2/返回 HTTP 200 基本说明能用返回 403、404、超时的直接删掉。docker pull 用的是 daemon 的网络这个在前面讲过但值得再强调一次daemon 不读你 shell 里的代理变量。所以出现“我浏览器能访问 Docker Hub但 docker pull 超时”的情况不用惊讶。这不是 docker 坏了而是 daemon 的网络路径和浏览器不一样。不要把 latest 当成“最新且不变的标签”latest这个名字本身是一个会变的 tag同一个latest镜像今天拉和三个月前拉可能是完全不同的东西。部署到生产环境前用 docker pull 确认 tag 后再用docker inspect 镜像名:tag看一下镜像摘要或者直接用 digest 部署。否则以后排查问题容易多一个“镜像版本变了”的干扰项。限流不是玄学真会触发在共享 IP 的环境里匿名拉取限流触发概率很高尤其是多个人一起做部署练习的时候。报错toomanyrequests别怀疑网络先docker login登录再试。登录之后不仅额度更高拉私有仓库也方便。5. 让后续部署更顺手的几个习惯pull 问题解决之后再给你几个我自己长期在用的习惯。这些不是解决一次故障的一锤子买卖而是让整个 docker 部署过程少踩坑。5.1 锁定镜像 tag不要裸写latest建议写成这样docker pull mysql:8.0 docker pull redis:7.0.15一些热门镜像的 tags 页面会列出具体版本号选择一个你验证过的稳定版本。如果之前部署用的是某个版本后续重建容器也要用同一个 tag避免版本漂移导致行为不一致。5.2 用 docker compose 固化部署参数命令行的docker run参数一多就容易漏尤其是端口映射、数据卷、环境变量混在一起复盘时根本看不清。我习惯把部署参数写进docker-compose.yml比如上面 MySQL 和 Redis 的部署可以直接改成services: mysql8: image: mysql:8.0 container_name: mysql8 restart: always ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: 你的密码 volumes: - /opt/mysql8/data:/var/lib/mysql redis7: image: redis:7.0 container_name: redis7 restart: always ports: - 6379:6379 volumes: - /opt/redis7/data:/data command: [redis-server, --appendonly, yes]然后一句sudo docker compose up -d就能拉起所有服务。如果拉镜像失败docker compose logs和docker compose ps能直接看到哪一步出错不需要翻命令历史。注意要用docker compose带空格而不是老的docker-composeUbuntu 20.04 上安装 docker compose 插件的方法很成熟这里不赘述但提醒一句用插件版本别用已经被官方放下的旧版 Python 脚本。5.3 关注容器启动状态而非“看似跑着”镜像拉取成功、容器也启动后不要只看docker ps里状态是 Up。Up 不代表服务可用很多容器进程起来后又崩了但 restart policy 不停重启docker ps 仍然显示 Up。用docker ps -a看状态用docker logs看真实日志。比如 MySQL 第一次初始化如果因为数据目录权限问题失败日志里会直接报错但docker ps里容器可能还在重启循环中。我见过太多人看到 Up 就觉得万事大吉结果服务根本没就绪。判断一个容器是否真正可用习惯看“日志里的就绪标记”而不是“进程是否活着”这个习惯能帮你省下大量排障时间。我自己在这台 Ubuntu 20.04 上前后折腾过不下十次 pull 失败最大的体会是先把报错关键词读懂再动配置改 daemon.json 之前一定先校验 JSON配了镜像源也别急着放弃 Docker Hub 本身因为有些镜像在第三方源上根本没有。最后再分享一个小习惯把验证过能用的镜像 tag 和对应加速地址记到笔记里下次部署直接照抄比临时翻历史命令省心得多。希望这套排查流程也能让你在下次遇到 docker pull 失败时少走几步弯路。