1. 为什么私有镜像仓库是容器化落地的关键一环
很多人学 Docker 的时候,习惯性地把docker pull当成理所当然的事——敲一条命令,镜像就从远端拉下来了。但真到了团队协作、生产部署的阶段,你会发现一个很现实的问题:你自己构建的业务镜像,往哪儿放?总不能每次都靠docker save导出 tar 包、再scp传到另一台机器上docker load吧。这种“人肉搬运”的方式在小规模试验时还能凑合,一旦涉及多环境、多节点、持续集成,立刻就会崩盘。
这就是Docker Registry要解决的核心问题。简单说,Registry 就是一个专门用来存储和分发 Docker 镜像的服务端。你熟悉的 Docker Hub 是官方运营的公共 Registry,而我们要聊的,是如何搭建和用好自己的私有 Registry。它解决的问题非常明确:让镜像有一个统一的、可版本化管理的存放地,让构建、推送、拉取形成闭环,让团队里每个人拿到的都是同一份可信的镜像。
这篇文章适合谁看?如果你已经会用 Docker 构建镜像、跑过容器,但对“镜像怎么管、怎么分发”还停留在手动拷贝阶段,那这篇内容正好补上这块拼图。如果你已经在用私有仓库,但只是照着教程敲了几条命令、没搞清楚背后的存储结构和鉴权逻辑,那这篇也能帮你把认知补齐。我会从 Registry 的整体设计思路讲起,拆解它的存储模型、API 交互、鉴权机制,再落到实操:怎么起一个带基本认证的私有仓库、怎么配置 HTTPS、怎么清理磁盘、怎么排查推送失败。全程按我实际踩过的路子来讲,不绕弯子。
需要先明确一个概念边界:Registry 本身是一个无状态的镜像存储服务,它不负责构建、不负责编排,只干“存”和“取”两件事。理解这一点很重要,因为很多新手会把 Registry 和 Harbor 这类带 Web 界面、带权限管理、带镜像扫描的上层平台混为一谈。Registry 是底座,Harbor 是在它之上包了一层管理和 UI。我们这篇聚焦底座本身,把底座的脾气摸透,上层平台用起来才不心虚。
2. Registry 的整体设计与存储模型拆解
2.1 为什么 Registry 要设计成“无状态 + 后端存储”的架构
Registry 最核心的设计哲学,是把“服务进程”和“数据存储”彻底分开。Registry 进程本身不保存任何镜像数据,它只负责处理 HTTP 请求、校验权限、然后把镜像的二进制块(blob)和元数据(manifest)写到后端存储里去。这个后端可以是本地文件系统,也可以是对象存储(比如 S3 兼容的存储服务)、也可以是其他实现了对应驱动的存储后端。
这么设计的好处非常直接。第一,水平扩展变得简单:你可以在多台机器上跑多个 Registry 实例,它们共享同一个后端存储,前面挂一个负载均衡,请求打到哪个实例都一样,因为它们本身不持有状态。第二,数据持久性和服务可用性解耦:Registry 进程挂了重启就行,数据在后端存储里安然无恙;后端存储扩容也不影响服务进程。第三,迁移成本低:换存储后端只需要改配置、迁移数据,服务层几乎不用动。
我用一个生活化的类比来解释:Registry 进程就像银行柜员,后端存储就像金库。柜员不把钱揣自己兜里,所有钱都进金库;柜员换班了,金库还在;金库换了个更大的地方,柜员照样按流程存取。理解了这个模型,你后面配置storage段、排查“为什么重启后镜像还在但配置丢了”这类问题,思路就清晰了。
2.2 镜像在 Registry 里到底是怎么存的
这是很多人没搞明白的地方。你在本地看到的镜像是一个整体,但推到 Registry 之后,它被拆成了两部分:manifest(清单)和blob(数据块)。manifest 是一份 JSON 描述,记录了“这个镜像由哪些层组成、每层的摘要是什么、配置文件是什么”;blob 则是实际的数据,包括每一层文件系统的 tar 包和镜像的 config JSON。
Registry 的存储目录结构大致是这样的(以文件系统后端为例):有一个docker/registry/v2/根目录,下面分blobs/和repositories/两大块。blobs/里按摘要的前两位做子目录,存放所有实际的数据块,文件名就是内容的 SHA256 摘要。repositories/里按“命名空间/镜像名”组织,每个镜像下面有_manifests/和_layers/等目录,里面存的是指向 blobs 的链接文件。
这个设计的关键点在于内容寻址(content-addressable):blob 的路径由它内容的哈希决定,同样的内容永远落在同一个位置。这就带来了天然的去重能力——如果两个镜像共享同一层基础镜像,那一层在 Registry 里只存一份。这也是为什么 Registry 的磁盘占用往往比你想象的小,因为大量镜像共享着相同的基础层。但反过来,这也意味着你不能简单粗暴地按目录删镜像,因为删掉一个 blob 可能影响多个镜像,必须通过 Registry 的 API 来做删除,让它自己维护引用关系。这一点后面讲垃圾回收时会重点说。
2.3 一次 push 和 pull 背后发生了什么
理解交互流程,排查问题时才不会抓瞎。当你执行docker push时,客户端大致做了这几件事:先向 Registry 请求开始一个上传会话,Registry 返回一个上传 URL;然后客户端把镜像的每一层分别上传(如果某层已存在,Registry 会告诉它“这层有了,跳过”);所有层传完后,客户端上传 manifest,Registry 校验 manifest 里引用的层是否都存在,都存在才接受。整个过程是分块、可断点续传的,这也是为什么大镜像推送中断后重试往往能从断点继续。
docker pull则相反:客户端先拉 manifest,解析出需要哪些层,然后逐层去请求 blob,本地已有的层直接跳过。这里有个实操中很常见的现象:你pull一个镜像感觉很慢,但看日志发现它其实在并行下载多个层。Registry 支持通过配置调整并发相关的行为,但更影响体验的往往是网络和存储后端的读取速度。
提示:Registry 的 API 是标准的 HTTP API,路径形如
/v2/<name>/manifests/<reference>和/v2/<name>/blobs/<digest>。你完全可以用curl直接调这些接口来检查镜像是否存在、查看 manifest 内容,这在排查“镜像明明推上去了却拉不下来”时特别有用。
3. 从零搭一个能用的私有 Registry:实操与参数详解
3.1 最简启动与端口、存储的取舍
先把最简单的跑法摆出来,再逐层加码。启动一个基础 Registry,一条命令就够:
docker run -d \ --name registry \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ registry:2这里有几个决策点值得说清楚。端口映射用5000:5000,5000 是 Registry 默认监听端口,映射到宿主机同端口方便本地测试。但要注意,Docker 默认把localhost:5000之外的非 HTTPS 仓库视为不安全,如果你用 IP 或域名访问,客户端会报http: server gave HTTP response to HTTPS client。解决办法要么配 HTTPS(推荐,后面讲),要么在客户端的daemon.json里把该地址加入insecure-registries列表(仅限内网测试,生产别这么干)。
存储挂载用-v /data/registry:/var/lib/registry,把容器内的存储目录挂到宿主机。这一步千万别省——不挂载的话,容器一删镜像全没。我见过有人图省事不挂载,结果容器重启后镜像“消失”,排查半天以为是 Registry 的 bug,其实就是数据在容器可写层里跟着容器生命周期走了。宿主机目录选一个磁盘空间充足的位置,因为镜像会越堆越多,后面会讲怎么监控和清理。
镜像版本用registry:2,这是官方维护的 Registry 2.x 系列。选它而不是自己从源码编译,是因为官方镜像已经把配置、启动脚本、健康检查都封装好了,稳定省心。版本号建议锁定到具体的小版本(比如registry:2.8),避免latest某天悄悄升级带来行为变化。
3.2 用配置文件把 Registry 的行为管起来
命令行启动适合快速验证,但真正要用,得靠配置文件。Registry 支持通过-v挂载一个config.yml进去,覆盖默认行为。一个够用的配置骨架长这样:
version: 0.1 log: level: info formatter: text storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true http: addr: :5000 headers: X-Content-Type-Options: [nosniff]逐段解释。log段控制日志级别和格式,排查问题时把level调到debug能看到每个请求的细节,但平时用info就行,debug日志量很大。storage段指定后端为文件系统,rootdirectory是容器内路径,配合挂载使用。delete.enabled: true这一项很关键:Registry 默认不允许通过 API 删除镜像,必须显式打开这个开关,否则你调删除接口会收到UNSUPPORTED的响应。很多人卡在这里,以为删除接口坏了,其实是没开开关。
http段的headers是安全加固的小细节,加上X-Content-Type-Options: nosniff防止浏览器对响应内容做 MIME 类型嗅探,属于顺手就做的防护。启动时把配置挂进去:
docker run -d \ --name registry \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ -v /data/registry-config/config.yml:/etc/docker/registry/config.yml \ registry:2注意:配置文件里的
rootdirectory是容器内路径,挂载时宿主机目录对应到容器内这个路径。两者别搞混,否则会出现“配置写的是 A 路径,数据却落在 B 路径”的错位。
3.3 加上基本认证,别让仓库裸奔
一个没有任何认证的 Registry,任何能访问到端口的人都能推拉镜像,这在团队环境里是不可接受的。最轻量的做法是加 HTTP Basic Auth。思路是:用htpasswd生成一个用户密码文件,然后在配置里启用htpasswd认证方式。
先生成密码文件(需要宿主机有htpasswd工具,没有的话用一次性容器跑):
mkdir -p /data/registry-auth docker run --rm --entrypoint htpasswd httpd:2 -Bbn myuser mypassword > /data/registry-auth/htpasswd-B表示用 bcrypt 加密,-b表示从命令行直接给密码,-n表示输出到标准输出。生成的每一行是用户名:加密后的密码。然后在config.yml里加认证段:
auth: htpasswd: realm: basic-realm path: /etc/docker/registry/htpasswd挂载密码文件并重启容器后,客户端推拉镜像前需要先docker login:
docker login localhost:5000 -u myuser -p mypassword这里有个实操心得:docker login成功后凭据会存在~/.docker/config.json里,后续操作自动带上。但如果你在多台机器、多个用户之间切换,容易遇到“明明登录了却还是 401”的情况,多半是凭据文件里的条目和当前访问的地址对不上(比如登录的是localhost:5000,访问的却是127.0.0.1:5000,在 Docker 看来这是两个不同的仓库地址)。统一用同一个地址访问,能省掉很多莫名其妙的认证问题。
3.4 配 HTTPS:自签证书的完整走法
生产环境必须上 HTTPS,否则密码是明文传输,镜像内容也可能被篡改。内网环境没有公网证书,用自签证书是常见做法。步骤分三步:生成证书、配置 Registry 使用证书、让客户端信任证书。
生成自签证书(假设访问地址是registry.internal):
mkdir -p /data/registry-certs openssl req -newkey rsa:4096 -nodes -sha256 \ -keyout /data/registry-certs/domain.key \ -x509 -days 365 \ -out /data/registry-certs/domain.crt \ -subj "/CN=registry.internal" \ -addext "subjectAltName=DNS:registry.internal"-addext subjectAltName这一项别漏,现代客户端校验证书时看的是 SAN 字段而不是 CN,只写 CN 会报证书不匹配。然后在config.yml的http段加 TLS 配置:
http: addr: :5000 tls: certificate: /etc/docker/registry/domain.crt key: /etc/docker/registry/domain.key挂载证书目录、重启容器。客户端这边,把domain.crt放到 Docker 信任的证书目录下(不同系统路径不同,常见的是/etc/docker/certs.d/registry.internal:5000/ca.crt),然后重启 Docker 服务让它重新加载。这里的关键是目录名必须和访问地址完全一致,包括端口号,否则 Docker 找不到对应的证书。
提示:自签证书的
domain.crt在客户端要命名为ca.crt放进对应目录,这是 Docker 的约定。放错名字或放错目录,都会导致x509: certificate signed by unknown authority的报错。
4. 运维实战:清理、监控与高频故障排查
4.1 镜像删了磁盘没降?垃圾回收的正确姿势
这是 Registry 运维里被问得最多的问题。你通过 API 删除了某个镜像的 manifest,curl返回 202,看起来成功了,但du -sh一看磁盘占用纹丝不动。原因在于 Registry 的删除是软删除:它只是把 manifest 的引用标记删掉,底层的 blob 数据还在,因为可能有其他镜像还在引用这些层。真正释放空间需要跑垃圾回收(garbage collection)。
回收命令通过 Registry 容器的入口执行:
docker exec registry \ /bin/registry garbage-collect \ /etc/docker/registry/config.yml这条命令会扫描所有 manifest,标记出仍被引用的 blob,然后删除没有被任何 manifest 引用的 blob。听起来很美好,但有两个坑必须提前知道。第一,回收期间最好停止写入,因为回收过程是“先扫描再删除”,如果扫描之后又有新镜像推入引用了某个 blob,而回收逻辑没感知到,就可能误删。稳妥做法是回收前暂停推送,或者干脆停掉 Registry 服务再以只读方式跑回收。第二,回收不会删除空的目录结构,repositories/下会残留一些空目录,这不影响功能,但看着乱,可以手动清理(确认是空目录再删)。
我个人的习惯是:删除镜像和垃圾回收分开做,删除后先观察一两天,确认没有业务还在依赖被删的镜像,再挑低峰期跑回收。回收前先备份rootdirectory(至少备份repositories/目录),万一误删还有退路。
4.2 磁盘空间监控与容量规划
Registry 的磁盘增长主要来自两处:新镜像的层,以及 manifest 元数据。层是主要占用,manifest 很小。容量规划上,一个粗略的经验值是:按团队镜像总量估算,再留 2 到 3 倍余量。因为镜像迭代会产生大量历史版本,旧版本不清理就会一直占着。比如团队有 50 个服务,每个服务平均 10 个历史版本,每个镜像 500MB,那就是 250GB 打底,加上去重后实际可能少一些,但留足余量总没错。
监控上,最直接的是定期看rootdirectory的磁盘占用,配合df -h看分区水位。更细一点,可以统计blobs/目录的大小和文件数量,观察增长趋势。如果发现增长异常快,多半是某个 CI 流水线在疯狂推镜像却没清理旧版本,这时候要从流程上解决——比如在 CI 里配置“只保留最近 N 个版本”,而不是靠 Registry 端被动清理。
4.3 常见故障速查表
下面这张表是我在实际运维中整理的高频问题,按“现象—可能原因—排查动作”组织,遇到问题先对号入座。
| 现象 | 可能原因 | 排查动作 |
|---|---|---|
push 报http: server gave HTTP response to HTTPS client | 客户端用 HTTPS 访问但 Registry 只配了 HTTP | 检查访问地址协议;内网测试可临时加insecure-registries,生产应配 HTTPS |
push 报413 Request Entity Too Large | 前置代理(如 Nginx)的请求体大小限制 | 调大代理的client_max_body_size,或绕过代理直连 Registry |
pull 报manifest unknown | 镜像名或 tag 拼错;镜像确实不存在 | 用curl调/v2/<name>/tags/list确认 tag 是否存在 |
删除镜像返回UNSUPPORTED | 配置里没开storage.delete.enabled | 在config.yml中开启并重启 |
| 删除后磁盘不降 | 只做了软删除,未跑垃圾回收 | 执行garbage-collect,注意回收期间控制写入 |
| 登录成功但操作仍 401 | 凭据地址与实际访问地址不一致 | 统一用同一地址(含端口)登录和访问 |
证书报unknown authority | 客户端未信任自签证书,或证书目录/命名不对 | 检查/etc/docker/certs.d/<地址>/ca.crt是否存在且命名正确 |
| 大镜像推送中断后重试很慢 | 部分层已上传但会话失效,重新协商 | 属正常现象,Registry 会跳过已存在的层,耐心等待或检查网络稳定性 |
这张表建议收藏,遇到问题先扫一遍,能省下大量翻日志的时间。表里没覆盖的,再去看 Registry 容器的日志(docker logs registry),把log.level临时调到debug,请求路径、状态码、错误信息都会打出来,定位起来快很多。
4.4 几个容易被忽略的实操细节
第一个细节是镜像命名规范。推送到私有仓库的镜像,名字必须带上仓库地址前缀,比如registry.internal:5000/myteam/myapp:v1.2.3。很多人本地构建时用的是myapp:v1.2.3,推的时候才docker tag一下,这没问题,但要注意 tag 之后原镜像还在本地占着空间,定期docker image prune清理无标签镜像能省不少本地磁盘。
第二个细节是manifest 的媒体类型。较新的 Docker 客户端默认使用 OCI 或 Docker V2 schema 2 的 manifest 格式,老版本 Registry 或某些代理可能不兼容。如果你遇到推送成功但拉取时报 manifest 解析错误,先确认 Registry 版本是否足够新,再看是不是中间代理改写了响应内容。
第三个细节是并发推送的锁竞争。多个 CI 任务同时推同一个镜像的不同 tag 时,如果它们共享相同的层,Registry 内部对 blob 的上传会有并发处理。正常情况下没问题,但在存储后端较慢时可能出现上传超时。缓解办法是错开推送时间,或者给 Registry 配置更宽松的超时参数。
5. 关于 Registry 选型与扩展的一点个人看法
聊到这儿,基础 Registry 的能力边界其实已经很清楚了:它能存、能取、能鉴权、能清理,足够支撑一个中小团队的镜像管理需求。但它也确实“素”——没有 Web 界面、没有细粒度的项目级权限、没有镜像漏洞扫描、没有复制策略。这些能力需要在上层平台里补,比如用 Harbor 这类项目在 Registry 之上做封装。我的建议是:先把 Registry 本身用明白,再考虑上平台。因为上层平台出问题时,最终还是要回到 Registry 的存储结构、API 行为、日志里去定位,底座不熟,上层就是黑盒。
至于扩展方向,如果你的团队规模继续增长,可能会遇到跨地域分发、多集群共享镜像的需求,这时候可以考虑 Registry 的存储后端换成对象存储以获得更好的扩展性,或者引入镜像同步机制在多个 Registry 之间复制。但这些都属于“规模到了再考虑”的事,不必一开始就上复杂架构。我见过不少团队一上来就搭一套重型方案,结果日常用的功能不到两成,维护成本却翻了几倍。
最后分享一个我自己的习惯:给 Registry 的rootdirectory单独挂一块盘,并且给这块盘设一个磁盘水位告警。镜像仓库这种服务,平时没人关注,一旦磁盘满了就是全线推送失败,影响面很大。提前把告警配好,比事后救火省心得多。另外,定期(比如每季度)做一次镜像清单盘点,把长期没人拉取的镜像清掉,既省空间也让仓库保持整洁——这件事听起来琐碎,但坚持下来,仓库的维护成本会低很多。