我最近部署一套服务,环境就是 CentOS 7,Docker 装好之后顺手docker pull一个业务镜像,结果卡在拉取层数据那一步,半天过去进度条纹丝不动,最后直接超时。查了一圈日志,问题就出在默认走官方镜像仓库,网络链路太长,延迟和丢包根本扛不住。后来把镜像源换掉,几十 GB 的镜像几分钟就拉完了。这篇文章就是把我当时排查、配置、踩坑的过程完整梳理一遍,给同样在 CentOS 7 上用 Docker、被拉镜像折磨的兄弟们一个可以直接照抄的方案。
1. Docker 拉镜像为什么会卡在官方仓库
先说结论:默认配置下 Docker 所有镜像都从官方默认仓库拉取,而这个仓库的服务器在境外。从国内的 CentOS 7 服务器访问它,经常要跨海、跨运营商绕路,TCP 连接握手慢、传输不稳定,尤其是镜像层一多、单个层体积一大,丢包重传能把整个下载过程拖到天荒地老。
这不是个例。我遇到过三种典型表现:
docker pull执行后永远停在Waiting或者Pulling fs layer,日志没有任何报错,但就是不出进度。- 等了大半天,突然冒出一句
net/http: TLS handshake timeout,整个拉取直接失败。 - 偶尔能拉完,但速度只有几十 KB/s,一个 500MB 的镜像能下一晚上。
要理解换源为什么能解决这个问题,得先明白 Docker 拉镜像的完整链路。你执行docker pull nginx时,本地 Docker 守护进程会向配置的镜像仓库发起 HTTPS 请求,先拿 manifest 清单文件,确认镜像有哪些层,再逐层下载 blob 数据,解压后放到本地存储/var/lib/docker里。默认情况下,这个"镜像仓库"就是官方默认仓库,所有流量都压在这条链路上。
镜像源(加速器)干的事情,就是在你本地 Docker 和官方仓库之间加了一个中转缓存。你配置好加速地址后,Docker 会优先请求这个中转地址,如果它本地已经有缓存,直接从缓存返回,速度飞快;如果没有缓存,它再去官方仓库拉取一次,然后把数据缓存下来供后续请求复用。所以换了源不是让数据凭空变近,而是让链路从"你 → 境外官方仓库"变成"你 → 国内中转节点",网络开销完全不是一个量级。
搞清楚了这个原理,哪些机器适合换源也就清楚了:只要是国内服务器、国内网络环境,连官方仓库明显卡顿的,都应该换。即使你的业务镜像都在私有仓库里,日常拉基础镜像(构建环境、操作系统镜像、运行时镜像)也绕不开官方仓库,换了只会更快,不会有副作用。
2. 镜像源配置的底层逻辑,明白了才能不出错
很多教程直接扔给你一段配置让你粘贴,但粘贴完一重启 Docker 起不来,你就懵了。所以配置之前,先把底层机制捋清楚。
2.1 Docker 守护进程的配置文件从哪里读
Docker 的镜像源配置不是写在某个图形界面里的,它由 Docker 守护进程dockerd启动时读取。默认情况下,dockerd会读取/etc/docker/daemon.json,这个文件里可以放一堆守护进程级别的配置,其中跟镜像源相关的字段就是registry-mirrors。
daemon.json必须是合法的 JSON 格式,注意三点:
- 键和字符串值必须用双引号,不能用单引号。
- 最后一个字段后面不能有逗号。
- 文件整体最外层必须是一对大括号。
下面这个是最小可用的配置内容:
{ "registry-mirrors": [ "https://xxx.mirror.example.com", "https://yyy.mirror.example.com" ] }每个镜像源地址都是https://开头的完整 URL,结尾不要带斜杠。registry-mirrors是一个数组,可以写多个地址。
2.2 旧版 Docker 的另类配置方式
CentOS 7 上有个特殊坑:系统自带的 Docker 可能不是新版 Docker CE,而是老版本,有些老版本对daemon.json的支持不完整,改了配置根本没反应。这时候需要用 systemd 的覆盖配置方式,给 Docker 服务手动追加启动参数。
做法是新建目录和文件:
mkdir -p /etc/systemd/system/docker.service.d然后在/etc/systemd/system/docker.service.d/daemon.conf里写入:
[Service] ExecStart= ExecStart=/usr/bin/dockerd --registry-mirror=https://xxx.mirror.example.com这里有两个细节非常关键。第一,ExecStart=这一行必须先写一遍空的,再写新的配置。这是 systemd 的规则:覆盖配置里重新定义ExecStart之前,必须先清空原来的值,否则新值会把旧值追加在后面,导致同一进程启动两次。第二,如果你配置了多个镜像源,用--registry-mirror参数的话每个地址前面都要带一次参数,例如--registry-mirror=https://xxx.mirror.example.com --registry-mirror=https://yyy.mirror.example.com,不能指望它像daemon.json里数组那样自动识别。
2.3 两种方式的优先级关系
那daemon.json和 systemd 覆盖配置里的--registry-mirror同时存在时听谁的?
根据 Docker 的启动逻辑,命令行参数优先级高于配置文件。也就是说,如果你在 systemd 里写了--registry-mirror,即使/etc/docker/daemon.json里也写了registry-mirrors,启动时命令行参数会覆盖配置文件里的值。反过来,如果你没有任何 systemd 覆盖配置,只写了daemon.json,那配置文件就生效。
我在实际处理过程中,优先使用daemon.json方案,原因很简单:这个文件一次写好,后续所有节点都能复用同一份配置,而 systemd 覆盖配置需要手动拼参数,容易漏。只有当你确认daemon.json不生效、或者 Docker 版本老到不支持对应字段时,才需要切到 systemd 方案。
3. CentOS 7 更换镜像源的完整操作步骤
下面是我在一台 CentOS 7.9 机器上完整操作过一次的流程,每一步都给出可执行命令和验证方法。你可以直接照着做,也可以在每一步后检查输出,确保没有走偏。
3.1 先确认 Docker 版本和安装方式
开始前先把环境摸清楚。执行:
docker version重点关注Server部分里的Version。如果是17.06之前的版本,或者docker --version显示的是一长串带docker.io字样的输出,说明是系统仓库装的老版本,建议直接用 systemd 覆盖方案,或者干脆升级到新版 Docker CE 再用daemon.json。
看服务状态也很重要:
systemctl status docker确保 Docker 服务是运行状态。如果服务本来就没起来,后面改配置会分不清是配置问题还是原有问题。
我建议在改配置之前,先手动拉一个最小的镜像做一次"速度基准测试",后面好对比:
docker pull busybox把这个时间记下来,换完源再拉一次,直观对比差距。
3.2 编辑 daemon.json 写入镜像源地址
检查目录是否存在,不存在则创建:
mkdir -p /etc/docker然后编辑配置文件:
vim /etc/docker/daemon.json把下面内容粘贴进去,注意把地址替换成你自己拿到或者选定的镜像源地址:
{ "registry-mirrors": [ "https://xxx.mirror.example.com", "https://yyy.mirror.example.com" ] }如果/etc/docker/daemon.json已经存在(比如之前配置过其他参数),千万不要直接覆盖。先在原文件基础上追加registry-mirrors字段。例如原文件内容是关于日志配置的,那最终应该是:
{ "log-driver": "json-file", "log-opts": { "max-size": "50m" }, "registry-mirrors": [ "https://xxx.mirror.example.com" ] }JSON 语法对逗号和括号敏感,多了一个逗号少了一个大括号,Docker 直接拒绝启动。改完最好顺手验证一下语法:
python -m json.tool /etc/docker/daemon.json如果命令没有报错,说明格式没问题。这里不要用python -c 'json.loads(...)'这种写法,系统里可能没有 Python 3,python -m json.tool在 Python 2 和 3 下都能跑。
3.3 重启 Docker 并确认配置生效
配置写好后,重启守护进程:
systemctl restart docker重启后立刻看服务状态:
systemctl status docker如果状态是active (running),说明启动成功。如果启动失败,跳到下一章的排查方法。
确认服务正常后,执行:
docker info在输出里找Registry Mirrors这一行。如果配置生效,你会看到类似这样的输出:
Registry Mirrors: https://xxx.mirror.example.com/ https://yyy.mirror.example.com/看到自己配置的地址出现在这里,就说明配置被 Docker 守护进程读到了。
3.4 用实际拉取做最终验证
配置生效不等于拉取就一定成功,最终还是要实测。先拉最小的镜像:
docker pull busybox注意观察输出里的Pulling from行。如果显示的是你配置的镜像源地址,说明流量已经走了加速链路;如果显示的仍然是官方仓库地址,说明配置没彻底生效或者有多个仓库配置干扰。
拉完小镜像,再拉一个有点分量的镜像做压力测试:
docker pull nginx或者直接拉你业务需要的那个大镜像。以 nginx 为例,正常情况下几百 MB 的镜像几十秒内就能拉完,整体速度对比换源前必须有肉眼可见的提升。
如果一切正常,恭喜你,镜像源更换完成,后续所有docker pull、docker build拉取基础镜像都会自动走新链路。
4. 更换源之后遇到的高频问题排查手册
换源这事儿不难,但很多人栽在换完之后的意外状况上。我把自己实际遇到过的几个问题和排查思路整理出来,每一条都是真实场景,不是教科书理论。
4.1 daemon.json 写错导致 Docker 启动失败
这是更换镜像源之后最常见的事故,我一共遇到不下三次。表现形式非常粗暴:systemctl restart docker之后,服务直接起不来,systemctl status docker显示失败。
首次遇到时我第一反应是奇怪,明明只加了一行配置,怎么会崩?看日志才发现问题:
journalctl -u docker --no-pager -n 50日志里通常会有类似unable to configure the Docker daemon with file /etc/docker/daemon.json: invalid json的报错。这就是 JSON 格式写错了。
排查方法很简单,先独立验证文件:
python -m json.tool /etc/docker/daemon.json如果输出Expecting , delimiter或Extra data之类的提示,说明语法有问题,逐行检查引号、逗号、括号。
应急恢复方案:如果一时找不到问题,先把配置文件改名或备份移走,让 Docker 以默认配置启动:
mv /etc/docker/daemon.json /etc/docker/daemon.json.bak systemctl restart docker服务能起来之后,再慢慢修 JSON 内容。这里有个心得:改配置之前第一步永远是备份,有一个daemon.json.bak在手,出任何问题都有后悔药。千万别嫌麻烦,别省这一步。
4.2 拉镜像时报 timeout、EOF、TLS 握手失败
配置没问题,Registry Mirrors也显示了,但docker pull还是报错,常见的错误有这么几类:
net/http: TLS handshake timeoutdial tcp xx.xx.xx.xx:443: connect: connection timed outEOF或unexpected EOF
这些错误看起来吓人,其实根因大多是下面几个:
第一,镜像源地址本身不可用。有的加速地址需要实时从云厂商控制台获取专属域名,过期或者授权失效的连接肯定失败。验证方法:直接用curl测试镜像源的连通性,执行curl -I https://xxx.mirror.example.com,如果一直卡住或者返回非 200 状态码,基本可以判定这个源不可用,换一个。
第二,DNS 解析问题。Docker 守护进程使用宿主机的 DNS 配置,如果/etc/resolv.conf里的 DNS 服务器解析不到镜像源域名,就会表现为超时。排查:nslookup xxx.mirror.example.com,看是否能返回 IP。如果解析不出来,改/etc/resolv.conf换公共 DNS 或内网 DNS。
第三,系统时间偏差导致 TLS 握手失败。镜像源都是 HTTPS 协议,证书校验依赖系统时间,如果 CentOS 7 的系统时间跟实际时间差太多,TLS 握手必然失败。排查:执行date看时间,跟手机时间对一下,差太多就同步一下时间再试。时间同步命令常规方法即可(如ntpdate或配置 chrony),具体工具取决于你机器上装了哪个服务。
第四,防火墙和安全组拦截了 443 端口。云服务器一般有两层防护:安全组和宿主机防火墙。确认firewalld状态:
systemctl status firewalld如果是开启状态,检查是否放行 443 端口;如果云平台安全组没放行,也需要在控制台调整。
4.3 镜像源生效了但速度依然不理想
还有一种更气人的情况:配置生效、拉取也成功、TLS 正常,但速度就是上不去。遇到这个别着急,先冷静分析。
最核心的原因:加速器只对自己的缓存镜像有加速效果,如果是一个冷门镜像、刚发布的 tag、或者加速器上没人拉过的新镜像,它只能现场回源到官方仓库拉取,这个过程依然会慢。所以第一次拉某个镜像慢,不代表配置没用;拉第二次、第三次就快了,因为缓存已经生成。
第二个原因是并发下载限制。Docker 默认会并发拉取多个镜像层,但层数非常多、每层都很大的时候,磁盘写入可能成为瓶颈。你可以在拉取过程中用top或iostat看看 CPU 和磁盘 I/O,如果 I/O 打满,说明瓶颈在磁盘不在网络。
第三个原因是镜像源配置了多个,但 Docker 使用它们是"依次尝试"而不是"负载均衡"。如果第一个源网络质量差,Docker 会一直重试它而不会自动切到第二个,看起来就像整个配置都失效了。解决方法是把质量最好的源放在数组第一个位置,或者干脆只留一个最稳的。
4.4 多个镜像源的正确使用姿势
registry-mirrors里配置多个地址确实有意义,但期望值要放对。打个比方:一组加速器像一排备用轮胎,不是同时用的;第一个源正常时全部流量都走第一个,只有第一个源彻底不可用才会换下一个。
所以正确的优先级排序原则是:把最快、最稳、最大概率有缓存的源放在第一位,备用源放在后面。我自己习惯配置两个地址:主源一个,备用源一个。理论上可以配更多,但配太多意义不大,反而会让 Docker 在某些情况下尝试耗时更长。
另外提醒一下,镜像源不是越多越好,关键是质量。某个加速服务偶尔能连通但回源极慢,这种源排第一位反而拖累整体体验。选源之前,逐个用curl -I测延迟,选延迟最低、连接最稳定的那个当主源。
5. 换完源之后,还有一些能让拉镜像更舒服的经验
设置好镜像源只是第一步,日常使用中有些事情处理好了,能让整个 Docker 使用体验上一个台阶。这部分是我长期实践中沉淀下来的,算不上高深理论,但很实用。
5.1 利用镜像分层特性,避免重复下载
Docker 镜像是分层的,不同镜像之间可能有共享层。比如你频繁用 nginx 和它的衍生镜像,只要底层相同,后续拉取新镜像时共性层不会重新下载。前提是你别动不动把 tag 改成latest,因为latest每次更新都可能导致底层变化。建议生产环境固定使用具体版本号,例如nginx:1.25.3而不是nginx:latest,这样镜像层复用的概率最高,拉取速度也会更快。
如果你正在用docker build构建自定义镜像,记得把变化最不频繁的步骤放在 Dockerfile 前面,把代码 COPY 这类常变化的操作放在后面。这样基础依赖层能缓存,后续构建不会每次都从零拉取。
5.2 离线环境搬运镜像的正确姿势
换了源不代表所有机器都有外网。内网生产服务器、隔离环境里的机器,就必须用离线搬运方案。
标准流程是这样的。在能联网、已经配置好镜像源的机器上:
docker pull nginx:1.25.3 docker save nginx:1.25.3 | gzip > nginx.tar.gz然后把这个 tar.gz 拷贝到目标机器上,加载:
docker load < nginx.tar.gz这里要记住:docker save保存的是完整的镜像层历史,适合跨机器搬运;docker export只能从容器里导出文件系统,会丢失镜像历史和元数据,不适合作为镜像搬运方案。
我之前有一次图省事,用docker export导出一个配置好的容器,结果到另一台机器上docker import之后,镜像内原有环境变量、启动命令全没了,跑起来完全不是那么回事。之后我只用save/load组合,没有再翻过车。
5.3 定期清理无用镜像,防止磁盘写满
镜像源的缓存和加速属性会放大一个问题:磁盘占用。拉得越快,历史镜像越多,/var/lib/docker膨胀也越快。CentOS 7 默认分区如果给得不大,很快就会被填满。
我自己习惯每周做一次检查:
du -sh /var/lib/docker如果磁盘空间告警,先清理悬空镜像和中止的容器:
docker container prune -f docker image prune -f如果是彻底清理,不打算留任何没在用的镜像,可以用:
docker system prune -a -f这个命令会把未运行的容器、未使用的镜像、网络和构建缓存全部清掉。执行前确认你确实不需要这些数据,因为它不会提示确认。
5.4 镜像源配置也要纳入版本管理
最后分享一个很多人忽略的点:/etc/docker/daemon.json也应该纳入你的配置管理,就像管理服务配置一样对待它。我维护的多台机器上都有这个文件,换源时需要一致的内容,如果手工一台台敲,很容易敲错或者漏掉某台。
做法是:把这个文件放到你的配置管理仓库、脚本自动化部署、或者至少放一份模板在固定地方。新机器装好 Docker 后直接用模板覆盖,再重启服务,完全不需要手动逐个编辑。
另外,镜像源地址偶尔会发生变化。我的习惯是每季度检查一次配置里的地址是否仍然有效,顺便测一下各个地址的响应延迟。这个习惯帮我稳定运行很久了,从来没有因为镜像源失效而出现大规模拉取失败的情况。
写在最后的一点建议
镜像源换好之后,Docker 的整体体验确实会提升一个档次。但我也要说句实话:换源不是一劳永逸,镜像源本身也有可用性和稳定性波动,所以一方面要保留备选源,另一方面要定期验证拉取链路。
我个人的最终经验是:基础镜像用小而常见的,版本固定不带 latest,配合一个稳定的镜像源;遇到慢的镜像不要死等,先确认路径上到底卡在哪一环。另外给一个小技巧,配置好镜像源后第一次拉镜像,先用一个几十 MB 的小镜像探路,确认链路整体通了再去拉几百 MB、几个 GB 的大镜像,这样即使有问题也能快速分层定位,不会因为一个大镜像拉半天失败而心态爆炸。