☰
私有Docker Registry运维实战:存储、GC、证书与团队约定
2026/10/27 21:37:59 网站建设 项目流程

很多人第一次听说私有镜像仓库,第一反应是"一条docker run的事,十分钟搞定"。我自己也这么想过,直到把服务真正推到团队里用,才发现启动只是开头:镜像推不上去、客户端报证书错误、删了 tag 磁盘不降、流水线并发拉取把带宽打满、机器重装后配置文件和账号全丢。这些问题不会在部署当天出现,而是在"有人开始依赖它"之后集中爆发。私有 Docker Registry 说白了就是一套自己掌控的镜像存储与分发服务,它解决的是镜像资产的归属、内网可达、版本可控和分发效率四件事。这篇文章不打算只给你一条启动命令,而是把存储模型、配置细节、对象存储后端、垃圾回收、排错链路和团队约定一次讲透。适合已经会用 Docker、准备把镜像管理从"随手 pull"升级成"团队基础设施"的读者,也适合已经跑起来但被 GC 和证书问题折磨过的同行。

1. 自建镜像仓库前,先把账算明白

动手之前我最想劝的一件事,是先搞清楚"为什么"。私有 Registry 不是什么高深系统,但它一旦成为流水线的依赖,运维责任就落到你头上了。搞清楚驱动力在哪,后面选存储、定备份策略、划权限才有依据,否则很容易做成一个"能 push 但没人敢依赖"的半成品。

1.1 三个把团队逼向自建的真实场景

第一个场景是内网可达性。生产环境的机器往往在隔离网段里,根本连不到公共镜像源。每次上线都要人肉docker save成 tar 包、scp 过去、再docker load,一个 2GB 的基础镜像来回折腾半小时。有了内网 Registry,节点只需要一条docker pull,剩下的交给网络。

第二个场景是构建结果的分发。CI 里构建出来的镜像,如果只存在构建机的本地缓存里,换一台机器就得重来。把构建产物推到私有仓库,部署端按 digest 拉取,构建和部署就解耦了。这一步是流水线能真正跑通的前提。

第三个场景是分发效率与稳定性。公共源有速率限制,多人同时拉同一个大镜像时更容易触发。团队规模一上来,早会结束后十几个人同时拉镜像,撞上限流是常态。自建仓库把这份流量挪到内网,带宽是局域网的速度,也没有配额焦虑。

这三个场景的共同点是:它们都不是"技术洁癖"驱动的,而是被具体的阻塞逼出来的。如果你的团队规模还小、也没有隔离网段,托管方案可能更省心,这一点后面会给出对照。

1.2 容易被低估的四项长期成本

第一项是磁盘。镜像不是文本文件,它有层、有历史、有重复的 blob。一个团队如果每天构建二十次,每次产生 300MB 的新层,一个月就是接近 200GB。而且删 tag 并不会立刻释放空间,这个坑后面单独讲。

第二项是证书与信任链。Registry 默认走 HTTPS,你要么买证书,要么自签。自签就意味着每台客户端机器都要把 CA 装到/etc/docker/certs.d/下。机器越多,这件事越像体力活,必须提前想好怎么批量分发。

第三项是垃圾回收与备份。Registry 的 GC 是"停机或只读"式的操作,跑一次可能几分钟到几十分钟。备份也不是简单拷目录,得保证 manifest 和 blob 的一致性。

第四项是人的成本。谁负责加账号、谁负责清空间、谁在流水线挂掉时第一个被叫起来。这些不出现在技术方案里,但决定了这套东西能不能长期活下去。

1.3 托管与自建的取舍对照

维度自建 Registry托管镜像服务
初始投入一台机器加半天配置注册即用
内网可达天然满足取决于网络打通程度
数据归属完全自有存在第三方
容量扩展需要自己规划存储按量付费,弹性好
认证体系需要自己接(htpasswd、token、对接内部账号)通常自带
高可用需要自己做多副本与负载均衡通常内置
长期运维全责在你平台分担

表格里最关键的一行是"内网可达"。如果你的部署环境是隔离网段,托管方案再便宜也用不上,自建几乎是唯一解。反过来,如果团队只有三五个人、机器都在公网可达的环境里,自建的运维成本大概率超过收益。

另外还有一个中间形态值得考虑:单机 Registry 加对象存储后端。计算和存储在物理上分离,Registry 进程本身变成无状态的,宕了直接换一台机器重新拉起,数据还在对象存储里。这个形态在中小团队里性价比很高,第 4 章会详细讲怎么配。

2. Registry的存储模型:先看懂blob和manifest再动手

我见过太多人把 Registry 当成一个黑盒,出问题时只会删容器重来。其实它的数据模型非常干净,花二十分钟理解清楚,后面所有关于"为什么删了不释放空间""为什么 digest 变了"的疑问都会自动消解。

2.1 它本质上是一个无状态的HTTP服务

Registry 本体(也就是 distribution 这个项目)做的事情可以概括成三件:接收 HTTP 请求、校验权限、把数据读写到后端存储。它自己不维护数据库,不维护索引,所有状态都存在存储后端里。这意味着:进程可以随便重启,甚至可以起多个副本挂在同一个存储上(前提是存储支持并发写)。

它的 API 遵循一套固定的路径规则,核心是这几条:

  • GET /v2/— 健康探测,返回 200 表示服务可用
  • PUT /v2/<name>/blobs/uploads/— 开启一次 blob 上传
  • PUT /v2/<name>/manifests/<reference>— 推送或更新 manifest
  • GET /v2/<name>/manifests/<reference>— 拉取 manifest
  • DELETE /v2/<name>/manifests/<digest>— 删除 manifest(需要显式开启删除)
  • GET /v2/_catalog— 列出仓库名(默认关闭,量大时性能很差)

docker push和docker pull本质上就是客户端按这套规则发起的一串 HTTP 请求。理解了这一点,抓包和看日志就有方向了。

2.2 一次push在磁盘上留下什么

以 filesystem 驱动为例,一次完整的 push 之后,rootdirectory下面会形成这样的结构:

/var/lib/registry/ └── docker/ └── registry/ └── v2/ ├── blobs/ │ └── sha256/ │ ├── ab/abcdef.../data # 镜像层数据 │ └── cd/cdefab.../data # 配置 JSON └── repositories/ └── myapp/ ├── _layers/sha256/ab.../link ├── _manifests/ │ ├── revisions/sha256/ef.../link │ └── tags/ │ └── v1.2.0/ │ ├── current/link │ └── index/sha256/ef.../link

关键点在于:blob 是全局共享的。不同的仓库、不同的 tag,只要某一层的 digest 相同,就只存一份。这也是为什么改一个 Dockerfile 里靠后的指令,只需要新增一层,前面的层全部复用,push 速度快很多。

而repositories/<name>/_manifests/tags/<tag>/current/link这个文件里,存的其实就是一个 digest 字符串。这解释了一个很重要的行为:tag 是可变的,manifest 才是不变的。

2.3 tag只是可变指针,digest才是身份证

假设你给同一个应用连续打了v1和latest两个 tag,它们指向同一个 manifest,磁盘上只存一份。后来你重新构建,又给latest打了新内容。这时候latest的current/link被覆盖成新的 digest,旧的那个 manifest 仍然在_manifests/revisions/里躺着,对应的 blob 也还在。

这就是"删了 tag 磁盘没变少"的根本原因:删 tag 只删了那一个指针文件,manifest revision 和 blob 都还在,等着 GC 去回收。

提示:真正想彻底删掉一个镜像,正确顺序是先按 digest 删除 manifest,再跑一次 garbage-collect。只删 tag 是无效操作。

2.4 存储驱动与描述符缓存

Registry 支持多种存储后端,常见的有 filesystem、S3 兼容对象存储、Azure Blob、Swift 等。选择逻辑很简单:单机小规模用 filesystem,需要容量弹性和多副本用对象存储。

有一个配置项几乎所有人都该加但经常忘记:

storage: filesystem: rootdirectory: /var/lib/registry cache: blobdescriptor: inmemory

blobdescriptor: inmemory会在内存里缓存 blob 的描述信息,拉取镜像时能省掉大量对后端的 HEAD 请求。用对象存储后端时这个缓存的价值更大,因为每次 HEAD 都是一次网络往返。代价是进程重启后缓存清空,属于可接受的开销。

另外还有一点容易忽略:如果存储后端是对象存储,Registry 在判断某个 blob 是否已存在时,依赖的是后端自身的对象元数据。不同实现之间对Content-Type和自定义 header 的处理会有细微差异,遇到奇怪的上传失败时,先去后端看对象是不是已经写进去了,往往能快速定位问题出在 Registry 侧还是存储侧。

3. 单机部署实操:证书、认证、配置逐段落地

这一章是完整可复现的部署流程。我按"目录规划 → 证书 → 配置 → 认证 → 启动 → 验证 → 托管"的顺序组织,每一步都说明为什么这么做,而不是只堆命令。

3.1 目录规划与证书签发

先定目录。我的习惯是把配置、数据、证书、认证文件分成四个独立目录,好处是备份和迁移时边界清晰,也方便用不同的卷挂载:

sudo mkdir -p /opt/registry/{config,data,certs,auth}

证书这一步是新手最容易省、最后最常出问题的地方。虽然可以通过insecure-registries跳过,但那意味着镜像内容在网络上明文传输,多机器环境下还要逐台改 daemon 配置,长期看反而是负担。自签证书的生成命令如下:

openssl req -x509 -nodes -days 3650 -newkey rsa:4096 \ -keyout /opt/registry/certs/registry.key \ -out /opt/registry/certs/registry.crt \ -subj "/CN=registry.internal.example" \ -addext "subjectAltName=DNS:registry.internal.example,IP=10.0.0.20"

几个细节值得展开:

-days 3650定十年,是因为证书轮换本身就是个容易被遗忘的运维任务。十年当然不是最优实践,但对内网自签场景来说,比"每三个月手动换一次然后某个凌晨全线拉取失败"要实际得多。

subjectAltName必须包含客户端实际访问用的地址。Docker 客户端校验证书时看的是 SAN,不看 CN。如果客户端用 IP 访问,SAN 里就必须有那个 IP;用域名访问,就必须有域名。这一项漏了,报错信息会长这样:x509: certificate is valid for ..., not ...。

私钥文件的权限也要收紧:

sudo chmod 600 /opt/registry/certs/registry.key sudo chmod 644 /opt/registry/certs/registry.crt

3.2 config.yml逐段拆解

把配置文件写到/opt/registry/config/config.yml:

version: 0.1 log: level: info formatter: text fields: service: registry environment: prod storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true cache: blobdescriptor: inmemory maintenance: uploadpurging: enabled: true age: 168h interval: 24h dryrun: false http: addr: :5000 headers: X-Content-Type-Options: [nosniff] tls: certificate: /certs/registry.crt key: /certs/registry.key auth: htpasswd: realm: basic-realm path: /auth/htpasswd

逐段说几个关键点。

storage.delete.enabled: true是必须显式打开的。默认情况下 Registry 拒绝 DELETE 请求,这是为了防止误删。很多人抱怨"删不掉镜像",就是漏了这一行。

maintenance.uploadpurging处理的是中断的上传。客户端推到一半断网,会留下一堆没完成的 upload 目录,长期不清理会吃掉不少空间。age: 168h表示超过七天的未完成上传会被清掉,interval: 24h是检查频率。这个配置项很多部署文档里没有,但它能省掉一次半夜排查磁盘的麻烦。

http.headers里的X-Content-Type-Options: nosniff是安全加固,防止浏览器把响应内容当成别的类型解析。虽然 Registry 通常不直接在浏览器里访问,但加上没有坏处。

http.tls的路径写的是容器内路径(/certs/...),因为下面启动时会做卷挂载。这是新手常见的错位点:配置文件里写宿主路径,容器内找不到文件,启动直接失败。

3.3 htpasswd认证与账号管理

基础认证用 htpasswd 最省事。生成密码文件的推荐方式是借一个带 htpasswd 工具的镜像来跑,避免在宿主机上装依赖:

docker run --rm --entrypoint htpasswd httpd:2 -Bbn deploy 'StrongPass_2024' \ > /opt/registry/auth/htpasswd

-B表示使用 bcrypt,比默认的 MD5 版本安全得多。-b是命令行传密码,-n是输出到标准输出。追加新账号时用>>,但要注意:Registry 只在启动时读取一次 htpasswd 文件,新增账号后必须重启服务或发 SIGHUP 才生效。

docker run --rm --entrypoint htpasswd httpd:2 -Bbn ci-bot 'AnotherStrongPass' \ >> /opt/registry/auth/htpasswd docker restart registry

账号规划上我的建议是按用途分而不是按人分:一个给人手工操作用的账号,一个给流水线用的账号,必要时再给审计留一个只读账号。按人分配看似清晰,实际人员流动时销号很麻烦,而且流水线共用人工账号会让审计日志失去意义。

3.4 启动、验证与客户端信任

启动命令:

docker run -d \ --name registry \ --restart=always \ -p 5000:5000 \ -v /opt/registry/config:/etc/docker/registry \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/certs:/certs \ -v /opt/registry/auth:/auth \ registry:2

启动后先自检:

curl -k -u deploy:'StrongPass_2024' https://registry.internal.example:5000/v2/ # 期望返回 {}

返回{}说明服务活着且认证通了。返回 401 说明账号或密码文件有问题,返回 404 说明路径不对(有些代理会改写路径)。

接下来是客户端信任。这一步在内网自签场景里无法回避,每台要访问 Registry 的机器都要做:

sudo mkdir -p /etc/docker/certs.d/registry.internal.example:5000 sudo cp registry.crt /etc/docker/certs.d/registry.internal.example:5000/ca.crt sudo systemctl reload docker

目录名必须是域名:端口的形式,端口不能省。文件名必须是ca.crt(或者.cert)。这两条错了,Docker 会静默地不去读这个证书,然后报一个看起来毫不相关的 TLS 错误。

做完之后验证一次完整往返:

docker pull alpine:3.19 docker tag alpine:3.19 registry.internal.example:5000/base/alpine:3.19 docker push registry.internal.example:5000/base/alpine:3.19 docker rmi registry.internal.example:5000/base/alpine:3.19 docker pull registry.internal.example:5000/base/alpine:3.19

push 时观察输出里的 digest,pull 回来之后对比一下,一致就说明整条链路没问题。

3.5 systemd托管与日志落盘

用docker run --restart=always可以应付重启,但日志默认走 json-file 驱动,时间一长会把磁盘吃掉。更稳妥的做法是让容器日志交给 journald,或者直接用 systemd 管理一个非容器化的 Registry 进程。

容器方案的日志配置:

docker run -d \ --name registry \ --restart=always \ --log-driver=journald \ --log-opt tag="registry" \ -p 5000:5000 \ -v /opt/registry/config:/etc/docker/registry \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/certs:/certs \ -v /opt/registry/auth:/auth \ registry:2

如果倾向 systemd 直管二进制,单元文件大致这样写:

[Unit] Description=Private Docker Registry After=network-online.target Wants=network-online.target [Service] Type=simple ExecStart=/usr/local/bin/registry serve /etc/registry/config.yml Restart=always RestartSec=5 LimitNOFILE=65536 [Install] WantedBy=multi-user.target

LimitNOFILE=65536这条别省。镜像层多、并发拉取多的时候,文件描述符耗尽会导致连接被莫名拒绝,报错信息还特别模糊。

4. 后端换成对象存储:容量与迁移

单机 filesystem 跑一段时间后,最先撞上的墙通常是磁盘。这时候有两条路:加盘,或者把后端换成对象存储。前者简单但迟早还要再面对一次,后者一次性把"计算"和"存储"解耦,长期更省心。

4.1 换后端的时机判断

我给自己定的判断标准有三条,命中任意一条就该考虑换了:

  • 数据目录超过单块盘容量的 60%,且增长趋势明显;
  • 出现过多副本需求(想做负载均衡,把 Registry 挂两台上);
  • 备份窗口内无法完成整目录拷贝,或者拷贝时的一致性难保证。

第三条尤其值得注意。filesystem 后端做增量备份并不容易,因为 blob 目录的文件名是内容哈希,增量工具能识别新增,但配合 GC 的删除操作会出现"备份里还有、源端已删"的漂移。对象存储后端天生支持版本控制和跨区域复制,备份策略简单很多。

4.2 S3兼容配置逐项说明

以 S3 兼容后端为例,配置段长这样:

storage: s3: accesskey: AKIAEXAMPLEKEY0001 secretkey: ExampleSecretKeyValue0001 region: us-east-1 regionendpoint: http://storage.internal:9000 bucket: registry-prod rootdirectory: /docker storageclass: STANDARD forcepathstyle: true chunksize: 10485760 delete: enabled: true cache: blobdescriptor: inmemory

regionendpoint是自建对象存储时最容易漏的一项。默认 SDK 会去连公有云的端点,不走这个字段就会一直超时。填的是对象存储服务的地址,带协议不带路径。

forcepathstyle: true对自建服务几乎必开。有些实现不支持虚拟主机风格的bucket.endpoint访问方式,只认endpoint/bucket这种路径风格。这一项配错了,表现是上传时返回 404 或者签名错误。

chunksize控制分片上传的块大小。默认值不大,大层镜像上传时会产生大量分片请求。调到 10MB 左右能明显减少请求数,但也要看后端对分片数的限制。

region填一个占位值即可,因为实际请求都走regionendpoint。但字段不能空着,SDK 做签名时会用到。

配置改完启动,第一次验证建议只推一个小镜像,然后用对象存储的客户端工具确认bucket/docker/v2/blobs/下面确实有对象生成。这一步能快速把"配置错误"和"客户端错误"分开。

4.3 存量镜像迁移的两种路径

已经有了 filesystem 数据要迁到对象存储,方案有两种。

路径一:直拷目录结构。对象存储的 key 路径和 filesystem 的目录结构是对应的,用rclone或对象存储自带的同步工具把/opt/registry/data整个同步到bucket/docker即可。同步完成后把配置切过去,重启服务,然后用/v2/_catalog或者直接docker pull几个已知的 tag 验证。这个方案快,但要注意同步期间 Registry 最好处于只读状态,否则会有写冲突。

路径二:客户端重推。在一台机器上把关键镜像按 tag 全量pull下来再push到新仓库。这个方案慢,但最干净,因为过程中会重新计算 digest,也能顺手淘汰一批没人用的历史版本。团队镜像数量在几百以内时,我倾向于这个方案,反正迁移一次也就一两个小时。

迁移完成后别急着删老数据,留一周再清理。做基础设施迁移,我吃过最大的亏就是"验证看起来通过就清理",结果两天后发现某个冷门 tag 的某一层丢了,回溯时原始数据已经没了。

4.4 缓存与直读的取舍

对象存储后端下,blobdescriptor: inmemory这个缓存的价值会被放大。没有它,每次 pull 都要对每个层发一次 HEAD 请求确认存在性,一个 15 层的镜像就是 15 次往返。启用后这些信息缓存在内存里,只有缓存未命中才回源。

代价是:如果多个 Registry 副本共享同一个 bucket,一个副本删了镜像,另一个副本的缓存不会自动失效。表现出来就是客户端偶尔能拉到已经删掉的镜像,或者拿到 404 之后重试又成功了。这种情况下的处理办法是给缓存设置合理的失效时间(部分版本支持),或者干脆接受这种最终一致性,反正镜像删除本身也不是高频操作。

另一个取舍是是否开启 CDN 之类的中间缓存。镜像层是不可变内容,按 digest 寻址,天然适合缓存。但引入了额外一层,排错复杂度上升。小团队不建议一开始就上。

5. 跑起来之后才暴露的问题:清理、限流与排错

服务上线只是起点。真正消耗时间的是"为什么磁盘不降""为什么这个客户端拉不动""为什么昨天的流水线今天挂了"。这一章把最常见的问题按排查链路整理出来。

5.1 删了tag空间没变少:GC的完整链路

这是被问得最多的问题。完整的清理流程分三步,少一步都不行。

第一步:删除 manifest。注意删的是 digest,不是 tag。先查 digest:

curl -k -u deploy:pass -I \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ https://registry.internal.example:5000/v2/myapp/manifests/v1.2.0

响应头里的Docker-Content-Digest就是目标值。然后删除:

curl -k -u deploy:pass -X DELETE \ https://registry.internal.example:5000/v2/myapp/manifests/sha256:abcdef1234... # 期望返回 202 Accepted

第二步:跑垃圾回收。GC 会遍历所有 manifest,标记出仍在引用的 blob,然后删掉无引用的:

docker exec registry /bin/registry garbage-collect \ --delete-untagged \ /etc/docker/registry/config.yml

--delete-untagged会顺带清理没有 tag 指向的 manifest。不加这个参数,只删了 tag 的镜像仍然会被保留。这个参数在早期版本里不存在,跑之前先确认一下版本。

第三步:确认效果。

docker exec registry du -sh /var/lib/registry

GC 本身的注意点:它会遍历全部数据,期间 Registry 最好处于只读或者停止状态。在生产上直接跑,可能导致并发拉取时读到已经被删的 blob,报blob unknown to registry。我的做法是挂一个只读副本出来跑 GC,或者安排在低峰期短暂停服。

还有一个坑:GC 有一种情况会误删——如果某个 manifest 的引用关系在 GC 遍历期间发生变化(比如同时有人 push),可能出现竞态。所以GC 期间禁止写入这条不是建议,是必须。

5.2 高频报错对照表

报错信息常见原因处理方向
http: server gave HTTP response to HTTPS client客户端走了 HTTPS,服务端是 HTTP,或证书没被信任检查certs.d目录名与文件名
x509: certificate signed by unknown authorityCA 未安装到客户端把 crt 拷到/etc/docker/certs.d/<host:port>/ca.crt
x509: certificate is valid for X, not YSAN 不包含实际访问地址重新签发,补齐 SAN
401 Unauthorized账号密码错误,或 htpasswd 未重载重启服务,用 curl 单独验证
manifest unknowntag 不存在,或引用的是已删除的 digest用_catalog或 tag 列表接口确认
blob unknown to registryGC 与并发拉取冲突,或迁移时数据不全检查 GC 时间窗口,重推镜像
413 Request Entity Too Large反向代理的 body 大小限制调大client_max_body_size
no such host客户端 DNS 解析不到 Registry 域名检查 hosts 与内网 DNS

表里最值得多说一句的是 413。很多人在 Registry 前面挂一层 Nginx 做统一入口和限流,然后发现大镜像推不上去,报的是 413 而不是更明确的错误。Nginx 默认允许 1MB 的请求体,镜像层动辄上百 MB,必须显式放开:

server { listen 443 ssl; server_name registry.internal.example; ssl_certificate /etc/nginx/certs/registry.crt; ssl_certificate_key /etc/nginx/certs/registry.key; client_max_body_size 0; chunked_transfer_encoding on; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 900; } }

client_max_body_size 0表示不限制,proxy_read_timeout 900是因为大层上传可能持续十几分钟,默认 60 秒会被掐断。这两个参数在"偶尔推大镜像失败"的场景里几乎必中。

5.3 反向代理、限流与监控

把 Registry 藏到反向代理后面有几个实际好处:统一 TLS 终止、统一访问日志、可以做限流、可以按路径做访问控制。

限流方面,Nginx 的limit_req就够用:

limit_req_zone $binary_remote_addr zone=registry_req:10m rate=20r/s; location /v2/ { limit_req zone=registry_req burst=40 nodelay; proxy_pass http://127.0.0.1:5000; }

注意别把 rate 定得太低。docker pull一个镜像会发很多次请求(每个层一次 HEAD、一次 GET),rate 卡太死会把正常拉取也拦掉。

监控方面,Registry 提供一个健康检查端点/debug/health,可以直接给负载均衡用。较新版本还支持暴露 Prometheus 指标:

http: debug: addr: :5001 prometheus: enabled: true path: /metrics

这个配置项的字段名在不同版本之间有过调整,配完先 curl 一下/metrics看有没有输出,没有就是版本不支持,别硬调。

真正需要关注的指标我认为只有三个:磁盘或存储用量增长率、拉取请求的 P95 延迟、上传失败的次数。前两个决定容量规划,第三个决定用户体验。其他指标都属于锦上添花。

5.4 备份策略的最小可行方案

备份要覆盖四样东西:存储数据、配置文件、证书私钥、htpasswd 文件。后三样很小,但丢了就是灾难——证书要重签意味着所有客户端都要重新分发 CA。

filesystem 后端的最小方案:

#!/usr/bin/env bash set -euo pipefail STAMP=$(date +%Y%m%d) tar -czf /backup/registry-meta-$STAMP.tar.gz \ /opt/registry/config \ /opt/registry/certs \ /opt/registry/auth rsync -a --delete /opt/registry/data/ /backup/registry-data/

配置类文件每天备份没问题,数据目录用 rsync 增量同步即可。但要注意:rsync 正在跑的时候如果 GC 同时在删 blob,备份出来的可能是不一致的状态。稳妥的做法是 GC 期间暂停备份任务,或者备份在 GC 之后紧接着跑。

对象存储后端的备份简单很多:开版本控制,删掉的对象不会真的消失,误删可以恢复;再配一个跨桶同步作为异地副本。这种情况下 Registry 侧几乎不需要额外做什么。

6. 团队层面的几条使用约定

技术方案能跑通只是及格线,能不能被团队长期用下去,取决于约定。这几条是我在多个团队里反复验证过、最值得写进规范的。

6.1 命名规范与tag策略

仓库名建议按业务域/服务名两级划分,比如platform/gateway、data/etl-runner。层级不要超过三层,太深了记不住,列表接口也不好用。

tag 策略上,我的建议是:

  • 环境类 tag(prod、staging)只作为当前指针,不用于回溯;
  • 版本类 tag 用语义化版本,构建时打;
  • 生产部署一律使用 digest,不用 tag。

第三条是关键。tag 可变,意味着今天prod指向的内容和明天可能完全不同,出问题时你没法复现"当时跑的到底是哪个镜像"。用 digest 部署,回滚和审计都简单:digest 唯一标识一个不可变的 manifest。

提示:在 CI 里把构建产出的 digest 记录到发布记录里,比记录 tag 有用得多。

6.2 权限切分与账号生命周期

htpasswd 方案只能做"有权限/无权限"的二元判断,做不了细粒度。如果团队对权限隔离有要求,有两个方向:

一是引入带权限模型的完整方案(比如在 Registry 前面加一层支持 token 鉴权的网关,或者直接换用功能更全的镜像仓库软件);二是用仓库隔离代替权限隔离——给每个团队一个独立的 Registry 实例或独立域名,各自管理账号。后者运维成本高一些,但权限边界非常清晰,适合团队之间不太需要共享镜像的场景。

账号生命周期这块,最实用的做法是给每个自动化用途分配专属账号,密码存在流水线的密钥管理里,并且约定每季度轮换一次。轮换时先加新账号、改流水线、验证通过、再删旧账号,顺序不能反。

6.3 镜像体积控制

仓库磁盘涨得快,根因往往不是"镜像多",而是"镜像大"。几条实际有效的控制手段:

基础镜像统一。如果每个服务各自选基础镜像,光基础层就能存出好几个版本。统一到两三个基础镜像,层复用率会大幅提升。

构建时清理中间产物。在多阶段构建里把编译依赖留在 builder 阶段,最终镜像只拷产物,这一个动作经常能把镜像从 1.2GB 压到 200MB 以内。

定期清理。前面讲的 GC 流程最好固化成脚本,按季度跑一次,把长期无人拉取的 manifest 清掉。清理之前先通过访问日志统计一下哪些 digest 超过 90 天没有被拉取过,白名单里的不动,其余批量处理。

再补一个容易被忽略的点:不要把构建缓存推到生产仓库。有些构建工具支持把中间层缓存也推上去,短期看能加速构建,长期看会占掉大量空间,而且这些层的生命周期管理和业务镜像完全不同。真要缓存,单独建一个仓库、单独设保留策略。

我个人在实际操作中的体会是,私有 Registry 这类基础设施的价值不在于"功能多强",而在于"边界清楚"——数据在哪里、谁能访问、怎么备份、怎么清理,这四件事想清楚了,后面的运维就没什么意外。反倒是那些为了省事一路默认配置装上就用的,通常半年后都要推倒重做一次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询