团队里老有人抱怨:docker pull 拉一个大点的镜像,一卡就是十分钟,偶尔还直接超时,整个构建流水线跟着翻车。后来我在服务器上部署了一个私有 Docker 镜像加速服务,代号就叫 KSpeeder,专门用来缓存从上游仓库拉下来的镜像层。配置好之后,同一台机器或者同网段的其他机器再拉同一个镜像,基本是秒开。这篇文章就记录一下我从零部署这个服务的全过程,包括原理、详细步骤、踩坑记录和常见问题排查,内容偏实战,按顺序照做就能把服务跑起来。
1. 为什么需要私有 Docker 镜像加速服务
1.1 拉镜像的那些烦心事
在没有镜像加速服务之前,我们的开发机和测试机都是直接连 Docker Hub 拉镜像。小镜像还好,几个大镜像比如带完整 Node 和 Python 环境的开发镜像,动辄几百 MB,经常出现下载到一半就断掉的尴尬情况。最麻烦的是多台机器重复拉同样的镜像,每台都要重新下载,遇到节假日一次批量更新,办公室出口带宽直接被打满,其他人上网都受影响。
我们还遇到过上游服务偶发不稳定,构建任务里只要多了一步 docker pull,整个流水线就可能失败。为了应对这个问题,大家常用的办法是手动在 daemon.json 里配一个公共的镜像加速地址,但是公共加速地址有配额、有频率限制,而且不同网络环境下效果差异很大,不适合长时间依赖。所以与其把稳定性寄托在别人身上,不如自己搭一个私有镜像加速服务,把流量收敛到内网,缓存由我们自己掌控。
1.2 镜像加速服务的工作原理
Docker 拉镜像的流程其实不复杂。一个完整的镜像由 manifest 和若干层(blob)组成。Docker 客户端在 docker pull 的时候,首先访问配置的 registry-mirrors 地址,请求 manifest;如果镜像加速服务的缓存里已经有对应数据,就直接从缓存返回。如果没有,镜像加速服务会向真正的上游仓库发起请求,把数据拉下来,一边返回给客户端,一边写入本地缓存。这样后续无论哪个客户端再拉同一个镜像,都能命中缓存。
用图书馆来类比:本地加速服务就是一个“分馆”,你借书时先查分馆有没有,有就直接拿走;没有的话,分馆的工作人员会去总馆把书调过来,你那边晚一点拿到书,之后这本书就存在分馆里了,下一个人再来借就快得多。理解这个流程之后,就能明白为什么部署镜像加速服务可以明显减少重复下载的带宽消耗。
1.3 KSpeeder 的定位与设计目标
KSpeeder 不是我从零写出来的一套源码,它其实是围绕 Docker Registry 官方镜像做的一套部署配置和自动化脚本。选择这个方案的原因很简单:Registry 的镜像仓库服务本身就内置了 mirror 模式,我们只需要给它一个上游地址和缓存目录,它就能当一个镜像加速器使用。我做的只是把常用的参数、目录规划、验证步骤整理成一套可以重复执行的部署流程。
它的定位是内网基础设施,不负责存储你自己构建的私有镜像,只负责加速从公共仓库拉取的镜像。私有镜像建议放到独立的私有仓库里,不要让加速服务和个人私有仓库混在一起,否则业务镜像和缓存镜像混着管理会非常混乱。我们当时就是单独规划了一个 /data/kspeeder 目录,专专用用,后来排查问题也省了很多事。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 部署前的环境准备与方案选型
2.1 服务器和 Docker 环境要求
先准备一台 Linux 服务器或者虚拟机,配置不用太高,2 核 CPU 加 2GB 内存足够跑起来。磁盘空间倒是建议预留大一些,至少 100GB。镜像缓存很占磁盘,尤其是团队经常更新镜像时,一个镜像的每一层都会在本地保留一份,日积月累容量增长比想象中快。
Docker 版本建议 20.10 以上,部署前先执行下面的命令确认版本:
bash复制docker version
如果 Docker 版本太老,后面做 registry-mirrors 配置或者处理 TLS 证书时,可能遇到兼容性问题。另外,加速服务默认监听 5000 端口,部署前要确认防火墙已经放行该端口,否则客户端访问不通。
2.2 存储与目录规划
我习惯把所有数据放在 /data/kspeeder 这个目录下,它对应容器内部的 /var/lib/registry。这样容器重启、升级都不会丢失缓存数据。目录结构如下:
bash复制/data/kspeeder
├── docker
│ └── registry
│ └── v2
│ ├── blobs
│ └── repositories
└── ...
磁盘类型方面,第一次拉取镜像时速度取决于上游网络,这时候本地磁盘不是瓶颈;但缓存命中以后,速度就完全看本地磁盘和网络了。所以如果预算允许,尽量用 SSD,尤其是多台机器同时命中缓存时,IO 很容易成为瓶颈。
2.3 为什么用 registry 镜像而不是写一套服务
市面上有各种各样的镜像加速实现,但很多是个人项目,更新维护不稳定。Registry 是 Docker 官方的开源组件,功能稳定,资料多,遇到问题容易排查。用它的 mirror 模式,只需要设置几个环境变量就能跑起来,部署成本极低,对小规模团队来说是最稳妥的选择。
另外 Registry 还提供了指标接口,可以很方便地和监控系统对接。我们团队后来给加速服务加了存活检查和磁盘告警,都是基于这些现成接口做的。自己造轮子的话,光是 API 兼容性就够折腾,完全没有必要在基础设施上炫技。
3. 手把手部署 KSpeeder
3.1 创建目录与网络
部署之前先创建数据目录,并准备一个 Docker 网络。单容器运行虽然不必须,但后续如果加了缓存监控容器或者其他辅助组件,同一个网络里会更好通信:
bash复制mkdir -p /data/kspeeder
docker network create kspeeder-net
如果命令提示网络已经存在,不用管,继续往下走就行。
3.2 启动核心容器
接下来直接用 docker run 启动 KSpeeder 核心容器。这里没有用 Docker Compose,因为配置项不算多,一个命令就能表达清楚:
bash复制docker run -d \
--name kspeeder \
--network kspeeder-net \
-p 5000:5000 \
--restart=always \
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
-e REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory \
-v /data/kspeeder:/var/lib/registry \
registry:2
逐个解释关键参数:
REGISTRY_PROXY_REMOTEURL是回源地址,也就是加速服务真正去拉取镜像的上游仓库地址。REGISTRY_STORAGE_CACHE_BLOBDESCRIPTOR=inmemory表示 blob 描述符缓存走内存,响应速度会更快一点。-v /data/kspeeder:/var/lib/registry把镜像层的实际数据持久化到宿主机。--restart=always保证服务器重启后容器能自动拉起来。
需要注意的是,inmemory 缓存的是元数据,不是镜像层本身。镜像层仍然写在宿主机目录里,所以不要担心丢失缓存。
3.3 验证服务是否正常
启动后用 curl 检查服务是否可用:
bash复制curl -I http://127.0.0.1:5000/v2/
正常情况下会返回 HTTP 200。再查看一下容器日志:
bash复制docker logs kspeeder --tail 50
能看到类似 listening on [::]:5000 的日志,说明服务已经正常监听 5000 端口。如果返回 404 或 503,优先检查环境变量是否拼写错误,以及服务器是否能正常访问上游地址。
3.4 配置 Docker 客户端使用该加速地址
服务端跑起来之后,还要让 Docker 客户端知道去访问它。修改每台机器的 /etc/docker/daemon.json:
json复制{
"registry-mirrors": ["http://192.168.1.10:5000"],
"insecure-registries": ["192.168.1.10:5000"]
}
这里有一个容易踩的坑:我们的加速服务是 HTTP 协议,而 Docker 默认只信任 HTTPS 注册表,所以必须同时在 insecure-registries 里加同样的地址,否则 Docker 会拒绝连接。如果加速服务就在本机,可以把地址写成 http://127.0.0.1:5000。
修改完配置后重启 Docker:
bash复制systemctl restart docker
然后确认配置真的生效了:
bash复制docker info | grep -A 2 "Registry Mirrors"
如果输出里能看到 http://192.168.1.10:5000,说明配置已经生效。
3.5 第一次拉取与缓存命中的完整验证
光配好还不算完,我们要实际验证缓存效果。先找一个简单的镜像,比如 alpine,执行第一次拉取并计时:
bash复制time docker pull alpine:3.19
拉完后删掉本地镜像,模拟“再次需要该镜像”的场景:
bash复制docker rmi alpine:3.19
time docker pull alpine:3.19
第一次通常要几秒钟,第二次如果走了加速服务缓存,耗时会明显缩短,甚至接近瞬间完成。此时再看看宿主机目录:
bash复制du -sh /data/kspeeder
如果目录大小增加了,说明镜像层确实被缓存下来了。也可以再看一眼容器日志,能看到请求记录和缓存命中日志,这样心里更有底。
4. 进阶配置:HTTPS 访问与多机共享
4.1 给镜像加速服务加上 TLS
如果客户端机器很多,每次都依赖 insecure-registries 并不是长久之计。更好的是给加速服务配上 HTTPS,让客户端用标准方式访问。准备证书文件 kspeeder.crt 和 kspeeder.key,放到 /data/certs 目录,然后重新部署容器:
bash复制docker run -d \
--name kspeeder \
-p 5000:5000 \
--restart=always \
-e REGISTRY_PROXY_REMOTEURL=https://registry-1.docker.io \
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/kspeeder.crt \
-e REGISTRY_HTTP_TLS_KEY=/certs/kspeeder.key \
-v /data/certs:/certs:ro \
-v /data/kspeeder:/var/lib/registry \
registry:2
需要注意,证书里的域名或 IP 必须和客户端访问的地址一致,否则验证会失败。如果使用自签名证书,客户端还有一种选择:把自建 CA 加入系统信任列表。这样 insecure-registries 就不需要再配了。
4.2 多台宿主机统一配置
假设团队有 10 台开发机,手动一台台改配置效率太低。我当时的做法是先把一份标准 daemon.json 准备好,然后通过配置管理工具批量下发。如果是临时用,也可以用 scp 加 ssh 的方式跑一遍。
下发之后不要同时重启所有机器,最好分批操作,避免业务中断。重启后可以写一个小循环检查每台机器的 Docker 是否正常:
bash复制for host in dev01 dev02 dev03; do
ssh $host docker info | grep -A 2 "Registry Mirrors"
done
如果某台机器显示不出来,要么是配置文件没有下发成功,要么是 Docker 没有重启。
4.3 监控缓存命中率与磁盘空间
镜像加速服务跑起来之后,磁盘增长是个长期问题。Registry 自带 /metrics 接口,可以用来对接 Prometheus。先手动看一眼:
bash复制curl -s http://127.0.0.1:5000/metrics | grep registry
可以看到请求数、错误数等指标。我建议把磁盘使用率作为重点监控对象,当 /data/kspeeder 所在分区超过 80% 时触发告警,避免缓存撑满磁盘导致服务异常。
另外要注意,mirror 模式下通过 /v2/_catalog 查看缓存镜像列表,通常会返回空数组,这是正常的,不代表服务有问题。需要主动判断缓存内容时,直接看宿主机目录里 blob 的文件大小就行。
5. 常见问题与排查实录
5.1 拉取镜像时报 502 或超时
这个现象通常是加速服务回源失败。首先在部署加速服务的服务器上执行一下 curl,看看能不能正常访问上游仓库:
bash复制curl -I https://registry-1.docker.io/v2/
如果手动访问都超时,说明服务器出网有问题,需要检查防火墙、路由和 DNS。如果手动访问正常,但客户端拉镜像还是报 502,再看容器日志:
bash复制docker logs kspeeder --tail 100
日志里如果出现 connection refused 或者 timeout,基本都是上游网络抖动。可以在客户端适当加大拉取超时时间,或者在回源失败的场景下加一个重试策略。
5.2 配置了镜像加速但不生效
最大的可能性是 daemon.json 改了但 Docker 没有重启,或者是 JSON 格式写错了导致 Docker 启动时回退到默认配置。先用 docker info 确认:
bash复制docker info | grep -A 2 "Registry Mirrors"
如果看不到加速地址,就检查一下 JSON 里有没有缺少逗号、引号用了全角符号之类的问题。还有一种情况:加速地址写成了 HTTPS,但加速服务本身是 HTTP,Docker 客户端访问不到,也会导致各种超时。这种时候把 daemon.json 改成 HTTP 加 insecure-registries,再重启一次就能解决。
5.3 缓存目录疯狂增长怎么办
镜像加速服务跑得越久,缓存目录越大,这是正常现象。但不能直接删目录,因为会破坏 Registry 的索引结构。正确做法是用 Registry 自带的 garbage collect 命令清理未被引用的层:
bash复制docker run --rm \
-v /data/kspeeder:/var/lib/registry \
registry:2 garbage-collect /etc/docker/registry/config.yml
这个命令会删除没有被任何 manifest 引用的 blob,执行前建议先备份一下目录。我们当时是每个月手动执行一次,同时配合磁盘监控,基本能把容量控制在安全范围内。
5.4 HTTP 与 HTTPS 不匹配导致的奇怪问题
有一种比较隐蔽的情况:daemon.json 里写了 HTTP 的 mirror 地址,但忘了配 insecure-registries。Docker 不会直接拒绝,而是会认为该地址不可信,然后回源去官方仓库拉取,导致所有镜像都绕过加速服务,缓存里什么也没有。排查思路就是看客户端上 docker info 是否有镜像加速地址,同时观察加速服务日志里有没有收到请求。如果没有收到,多半就是协议不匹配。
处理方式也很简单:要么在 insecure-registries 里加入这个 HTTP 地址,要么给加速服务配上 HTTPS,二选一即可。
整个 KSpeeder 部署下来,给我最大的感觉是,当你把镜像加速服务做成内网基础设施以后,CI/CD 的稳定性瞬间提升了一个档次。我踩过最深的坑就是忘了 insecure-registries,导致客户端一直回源,缓存里什么都没有。后来凡是新加机器,第一件事就是把 daemon.json 推下去,然后用 docker info 确认。另外建议定期做缓存清理,别等到磁盘爆了再处理。这个服务后期还可以继续扩展,比如加上简单的登录认证、把监控指标接到告警平台,甚至可以多做一套自动化部署脚本,让新环境一键拉起,省心很多。
