云服务器自建私有Docker镜像仓库:Registry部署、认证与GC清理实战
私有 Docker 镜像仓库这件事,我第一次动手是在一台 2 核 4G 的云服务器上,前后折腾了大半天,问题不在于步骤多,而在于每一步都有个不起眼的前提没被讲清楚:证书放哪个目录、htpasswd 为什么要重启才生效、删了 tag 为什么磁盘不降。这份记录就是把这些前提全部摊开讲一遍,从为什么要在云服务器上自建私有 Docker 镜像仓库,到选型、部署、客户端推送、存储清理、备份监控,一条完整链路走下来。内容偏学习向,适合刚接触容器生态、想搞明白镜像到底是怎么被存储和分发的朋友,也适合手上有个小团队、准备把内部镜像统一收拢到一台机器上的同学。默认你有一台能连上的 Linux 云主机和基本的 Docker 使用经验,其余的前置知识我会在用到的地方补上。
1. 私有镜像仓库解决的到底是什么问题
很多人第一次接触私有仓库,是因为在公共仓库上拉镜像越来越慢,或者某个基础镜像突然拉不到了。这只是表象。真正推动团队去自建仓库的,通常是三类更具体的不适感。第一类是交付物没法安放:公司内部业务镜像包含了自己的代码和配置,天然不适合放到公开可查的地方。第二类是构建链路对外部网络形成了硬依赖:CI 流水线每次跑都要从公网拉一堆基础镜像,出口带宽一抖动,整个流水线就开始排队。第三类是协作上的混乱:同一个项目,A 同学本地构建的镜像 tag 叫app:test,B 同学叫app:20240101,最后谁的镜像部署上去了,没人说得清。
这三类问题的共同点是,它们都不是"镜像仓库软件"本身能解决的,而是"有了一处统一的、可控的镜像存放点"之后自然被解决的。理解了这一点,你就不会一上来就去纠结 Harbor 还是 Registry,而是先想清楚:我需要的到底是一个能存能取的存储点,还是一整套带权限、审计、复制的管理平台。这个判断会直接决定后面的选型,也会决定你要花多少精力在运维上。
1.1 三种典型场景,只有一种是真需要自建
我把接触过的需求粗略分成三档,对照着看会比较清晰。
第一种是纯学习。目标是把镜像分发的链路摸清楚:docker push那一刻到底发生了什么,镜像在服务端是以什么形式落盘的,manifest 和 blob 是什么关系,为什么删除操作要分两步。这种场景下,你要的不是功能,而是"透明"——越简单的实现越好,因为简单才能看清内部。原生 Registry 就是为这个场景量身定做的。
第二种是小团队内部协作。几个到十几个人,几十个到几百个镜像,需要区分谁能推谁能拉,需要知道某个镜像是什么时候被谁推的,偶尔需要清理磁盘。这种场景原生 Registry 就开始吃力了,因为它的权限模型只有"认证"没有"授权"——你能登录,你就能推任何仓库。这时候 Harbor 这类带项目、角色、审计日志的方案才有意义。
第三种是生产环境。多环境隔离、镜像签名、跨地域复制、高可用,这些需求叠加起来,自建仓库本身就是一个需要专门排期的工程项目,和"学习"已经完全不是一回事了。我这篇的内容主要覆盖前两种,尤其是第一种,因为标题里写的是学习。
注意:不要把学习环境的搭建方式直接搬到生产。单机 Registry 没有高可用,磁盘挂了镜像就没了,这一点在后面备份那节会细说。
1.2 自建仓库的隐性成本,先列清楚再动手
自建的好处很直观:内部分发快、数据可控、链路透明。但成本往往是隐性的,动手前最好心里有数。
| 成本项 | 具体表现 | 学习环境能否接受 |
|---|---|---|
| 证书维护 | 自签证书要分发到每台客户端,到期要换 | 能接受,用自签即可 |
| 存储增长 | 镜像只增不减,磁盘会缓慢写满 | 能接受,定期清理 |
| 备份 | 仓库目录就是全部数据,丢了就全丢 | 能接受,做快照即可 |
| 权限管理 | 原生方案只有认证没有授权 | 学习环境可忽略 |
| 升级维护 | 版本升级要考虑数据兼容 | 学习环境可忽略 |
这张表里我最想强调的是"存储增长"。镜像仓库有个很反直觉的特性:它几乎不会自己变小。你推了一百个版本的镜像,删掉了九十九个 tag,磁盘占用可能一点没降。原因在于镜像分层存储,blob 是被多个 manifest 共享的,删 tag 只是删了一个引用。这一点如果没提前知道,很容易在某个时刻突然发现磁盘满了,然后手忙脚乱。
2. 方案选型:Registry、Harbor、Nexus 三者差在哪
选型这件事,我的建议是不要看功能列表,而要看"你愿意为它付出多少运维注意力"。功能列表上 Harbor 一定比 Registry 长,但多出来的那些功能,如果你用不上,就只是增加了出故障的面积。下面按复杂度从低到高说三个常见选项。
2.1 原生 Registry:一个"哑存储",但正好适合看懂原理
Registry 是 Docker 官方维护的镜像仓库实现,它做的事情非常克制:接收 push 请求、把 blob 写到本地文件系统、把 manifest 存成 JSON、响应 pull 请求。没有数据库,没有 Web 界面,没有用户体系(基础的 htpasswd 认证是有的,但仅此而已)。
它的好处是极其轻量。一个容器跑起来,挂一个目录,五分钟就能开始推拉镜像。它的配置文件是一份 YAML,你能清楚看到存储驱动是什么、监听在哪个端口、认证走哪套。对于学习来说,这种"什么都在明面上"的感觉非常重要,因为你能对照着配置文件去理解每一条 API 的行为。
它的短板也很明确:没有 Web UI,想知道仓库里有什么镜像只能用 API 查;权限只有"全有或全无";垃圾回收要手动跑命令,而且跑的时候最好停写。这些短板在学习场景下都不算问题,在小团队场景下就开始难受了。
2.2 Harbor:能力齐全,代价是资源与复杂度
Harbor 是在 Registry 之上包了一整层企业级能力:项目维度的权限、基于角色的访问控制、镜像扫描、签名、审计日志、远程复制、Web 管理界面。它自己由一组容器组成,包括核心服务、数据库、缓存、任务队列等,用容器编排的方式统一拉起。
资源门槛是需要正视的。官方给的最低配置是 2 核 4G,但实际跑起来,如果你还要开镜像扫描,内存会比较紧。磁盘方面,Harbor 本身加上数据库,起步就要留出 40G 以上的空间,再算上镜像数据,一台小规格云主机很容易吃紧。我在一台 2 核 4G、40G 系统盘的机器上装过一次,装完只剩不到 10G 可用,后面推了几个稍大的镜像就告警了。
复杂度体现在排障上。原生 Registry 出问题,你看一个容器的日志就够了;Harbor 出问题,你得先判断是哪个组件挂了,是数据库连不上还是任务队列积压。对初学者来说,这个心智负担不小。
2.3 学习路线的选型建议与规格对照
我的建议很直接:第一遍一定用原生 Registry。等你把 push、pull、认证、GC 这几件事都摸过一遍,再上 Harbor,你会发现 Harbor 的很多设计你一看就懂,因为它就是把这些基础能力包装了一层。反过来,如果一上来就装 Harbor,很容易变成"照着文档敲命令,跑起来了但不知道为什么"。
| 方案 | 最低规格 | 磁盘起步 | 上手时间 | 适合场景 |
|---|---|---|---|---|
| 原生 Registry | 1 核 2G | 20G | 半小时 | 学习原理、个人实验 |
| Harbor | 2 核 4G | 40G+ | 2 小时 | 小团队协作、需要权限 |
| Nexus | 2 核 4G | 40G+ | 2 小时 | 同时要管多种包格式 |
Nexus 单独提一句:它的优势是"一个仓库管多种格式",除了 Docker 镜像还能管 Maven、npm、PyPI 等等。如果你的团队本来就有一套多语言的技术栈,希望统一入口,它有价值;但如果只是要管 Docker 镜像,它的配置复杂度相对于收益来说偏高。
还有一个现实因素:云主机的计费方式。学习用途的话,按量付费加用完关机是最省的,配置可以稍微给足一点,反正开机时间短。这一点在最后一节会展开。
3. 云主机上的落地:从空白系统到能推能拉
环境准备这部分,我见过太多人卡在和 Docker 完全无关的地方:安全组没开、系统防火墙没放行、磁盘挂载错了路径。所以这里按"先定资源、再跑服务、最后配证书和认证"的顺序走,每一步都说明为什么。
3.1 实例规格、磁盘和安全组,先把这三件事定下来
规格上,学习用途 1 核 2G 就够跑 Registry 本体,但如果你同时想在上面跑几个测试容器,2 核 4G 会更从容。操作系统选主流的 Linux 发行版即可,我习惯用较新的长期支持版本,Docker 的安装脚本对它的支持最稳。
磁盘是重点。系统盘默认一般 40G,镜像数据建议单独挂一块数据盘,哪怕只有 20G。原因不是为了容量,而是为了隔离:镜像数据增长是不可控的,一旦把系统盘写满,连日志都写不进去,排查会非常痛苦;单独挂数据盘,写满了最多是推不上去,系统还能正常登录。挂载后格式化成常见的文件系统,挂到固定路径,比如/data,并且写进/etc/fstab,避免重启后没挂上导致数据"消失"(其实是写到了系统盘的挂载点目录里)。
安全组要放行的端口,按你的方案来:
- 22 端口:SSH 登录,建议限制来源 IP 为自己的出口地址
- 443 端口:仓库的 HTTPS 入口
- 5000 端口:Registry 容器默认监听端口,如果你用 443 直接映射,这个可以不对外开
- 80 端口:只有做证书自动续期验证时才需要临时开放
提示:安全组和系统防火墙是两层,很多人只开了安全组,结果被系统里的防火墙挡住,白白排查半小时。建议先统一关掉系统防火墙做验证,跑通后再按需放开。
3.2 用容器跑 Registry:配置文件比命令行参数靠谱
大多数人第一次启动 Registry 用的是长串的-e环境变量。能跑,但不推荐长期这么干,因为参数一多就看不清全貌了。我的做法是写一份config.yml,用挂载的方式喂给容器,配置文件本身就是最好的文档。
先建目录结构:
mkdir -p /data/registry /data/certs /data/auth/data/registry存镜像数据,/data/certs放证书,/data/auth放认证文件。然后写配置:
version: 0.1 storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true http: addr: 0.0.0.0:5000 secret: change-this-to-a-random-string tls: certificate: /certs/server.crt key: /certs/server.key auth: htpasswd: realm: basic-realm path: /auth/htpasswd log: level: info逐项解释一下为什么这么配。storage.delete.enabled必须显式设为 true,否则后面删除镜像是会被拒绝的,这个默认值是 false,是个很常见的坑。http.secret是用于签发认证令牌的随机串,如果不配,容器每次重启都会重新生成,某些场景下会导致客户端持有的凭证失效,配一个固定值省心。log.level保持 info 就够,debug 会刷屏。
启动命令:
docker run -d \ --name registry \ --restart=always \ -p 443:5000 \ -v /data/registry:/var/lib/registry \ -v /data/certs:/certs:ro \ -v /data/auth:/auth:ro \ -v /data/config.yml:/etc/docker/registry/config.yml:ro \ registry:2这里有个细节值得说:容器内监听的还是 5000,对外映射到 443。这样做的好处是客户端访问https://域名时不用带端口号,体验和公共仓库一致。如果你把容器内也改成 443,需要额外处理权限问题,没必要。
--restart=always不能不写。云主机重启、Docker 服务重启,都会导致容器停止,加上这个参数才能自动拉起来。
启动完检查一下:
docker logs -f registry日志里出现监听信息就说明起来了。用 curl 验证一下 API:
curl -k https://127.0.0.1/v2/返回{}表示服务正常。
3.3 TLS:三条路径的取舍
Docker 客户端默认只信任受信任机构签发的证书,这是硬性要求,绕不开。你有三个选择,各有代价。
第一条路是用受信任机构签发的证书。好处是所有客户端开箱即用,不需要任何额外配置。代价是需要一个真实域名,并且要完成域名验证。学习场景下如果手上正好有域名,这是最省事的方式,很多证书签发工具能做到自动续期,配好之后基本不用管。
第二条路是自签证书。好处是完全离线、不依赖任何外部服务。代价是每台要访问仓库的机器,都得把 CA 证书放到指定目录,否则会报x509: certificate signed by unknown authority。生成一条自签证书的命令大致是这样:
cd /data/certs openssl req -newkey rsa:4096 -nodes -sha256 \ -keyout server.key -x509 -days 3650 \ -out server.crt \ -subj "/C=CN/ST=Local/L=Local/O=Demo/CN=registry.demo.internal" \ -addext "subjectAltName=DNS:registry.demo.internal"注意subjectAltName一定要加,现代客户端校验证书看的是 SAN 而不是 CN,只写 CN 会报域名不匹配。这里的域名registry.demo.internal是个示例,你需要换成自己的,并且保证客户端能解析到这台机器的 IP——最简单的方式是在客户端 hosts 文件里写一条。
第三条路是干脆用 HTTP。这需要客户端在daemon.json里配insecure-registries,仅建议在完全隔离的内网实验环境中使用,公网机器上绝对不要这么干,因为账密会明文传输。
3.4 基础认证:htpasswd 的生成与两个坑
认证文件的生成方式,官方文档给的是用 httpd 镜像里的 htpasswd 工具:
docker run --rm --entrypoint htpasswd httpd:2 \ -Bbn youruser yourpassword > /data/auth/htpasswd-B表示用 bcrypt 加密,-b表示从命令行读密码,-n表示输出到标准输出而不是写文件。这三个参数组合起来是一行一条记录。
这里有两个我踩过的坑。第一个坑是改完密码必须重启容器。Registry 在启动时会把 htpasswd 文件读进内存,之后不会再读,所以你改了文件但没重启,新密码是不生效的,会一直报401 Unauthorized,而且很难想到是这个原因。
第二个坑是文件权限。容器内运行 Registry 的用户需要有读权限,如果宿主机上文件权限是 600 且属主不对,容器里读不到,表现同样是 401,但日志里会有一行关于文件打开失败的记录。加一行chmod 644 /data/auth/htpasswd就能规避。
再补一个容易忽略的:如果同一次部署里你既配了 TLS 又配了认证,docker login的时候会先校验证书再校验密码,所以证书问题会掩盖认证问题。排查顺序上,先用 curl 不带凭证确认 TLS 通了,再测认证,会快很多。
4. 客户端侧:让 docker push 真正成功的那些细节
服务端跑起来只完成了一半,客户端这一侧的问题往往更琐碎,因为每台机器的 Docker 配置状态都不一样。这一节按"配置、验证、排错"的顺序说。
4.1 insecure-registries 与自签 CA,到底该选哪个
这两个配置项经常被混着用,其实它们解决的是完全不同的问题。
insecure-registries的意思是"允许用明文 HTTP 和这个地址通信"。配上之后,客户端根本不会尝试 TLS。它适用于服务端确实只有 HTTP 的情况。
自签 CA 的处理方式则完全不同:客户端仍然走 HTTPS,只是额外信任你指定的 CA 证书。做法是把 CA 证书放到/etc/docker/certs.d/<仓库地址>/ca.crt,注意目录名必须和你在docker push里写的地址完全一致,包括端口号。如果你推的是registry.demo.internal:443,目录就要叫registry.demo.internal:443;如果推的是不带端口的registry.demo.internal,目录就不能带端口。这个细节非常容易搞错,而且报错信息不会直接告诉你。
判断标准很简单:服务端配了 TLS 就装 CA,只有服务端确实无法配 TLS 才用 insecure。前者的安全性高一个数量级。
改完daemon.json之后要重载并重启:
systemctl daemon-reload systemctl restart docker这里有个副作用要知道:重启 Docker 会重启所有正在运行的容器。如果你机器上还跑着别的服务,建议在daemon.json里加上"live-restore": true,这样重启 Docker 守护进程时容器不会跟着停。这个参数在生产机器上几乎是必选项。
4.2 打标签、登录、推送、拉取的完整链路
镜像地址的格式是仓库地址/命名空间/镜像名:标签。命名空间这一层是可选的,但我强烈建议加上,哪怕只是library或者自己的用户名,因为后面查 API 和做清理的时候,有命名空间会清晰很多。
docker login registry.demo.internal -u youruser docker tag myapp:latest registry.demo.internal/demo/myapp:v1 docker push registry.demo.internal/demo/myapp:v1验证一下仓库里是不是真的有了:
curl -u youruser:yourpassword \ https://registry.demo.internal/v2/_catalog这个接口会返回所有仓库名。想看某个仓库有哪些 tag:
curl -u youruser:yourpassword \ https://registry.demo.internal/v2/demo/myapp/tags/list在另一台机器上拉取,记得先docker logout再docker login,避免用到缓存的旧凭证。拉下来之后,为了确认镜像内容没问题,可以比对一下镜像 ID:docker images --digests能看到 digest,两边一致就说明传的是同一份数据。
顺便提醒一个安全习惯:docker login之后密码会以 base64 形式存在~/.docker/config.json,这不是加密。学习机器上无所谓,但在共享机器上,用完记得docker logout。
4.3 报错逐条排查的顺序
我把遇到过的报错整理成一张表,按出现频率排序,遇到问题从上往下对照着查基本能定位。
| 报错信息 | 大概率原因 | 处理方式 |
|---|---|---|
http: server gave HTTP response to HTTPS client | 客户端走了 HTTPS,服务端只有 HTTP | 服务端配 TLS,或客户端加 insecure-registries |
x509: certificate signed by unknown authority | CA 没装或目录名不对 | 检查/etc/docker/certs.d/<地址>/ca.crt |
x509: certificate is valid for X, not Y | 证书 SAN 里没有你用的域名 | 重新签发,补全 SAN |
connection refused | 端口没开或容器没起 | 查安全组、系统防火墙、docker ps |
401 Unauthorized | 未登录、密码错、改密码没重启 | 依次排查这三项 |
413 Request Entity Too Large | 前面有转发层限了体积 | 调大转发层的请求体上限 |
manifest unknown | tag 写错或没推成功 | 用 tags/list 接口确认 |
no space left on device | 磁盘满 | 清理 + GC,见下一节 |
最后一条no space left on device特别值得单拎出来说。磁盘满的时候,Registry 的表现可能不是直接报磁盘满,而是推送卡住、连接超时,甚至容器直接退出。所以平时养成习惯,定期看一眼df -h和/data/registry目录的大小。监控那一节会再提。
5. 磁盘写满之前:存储结构与垃圾回收
这一节是整篇里最容易出问题的地方,也是原生 Registry 最反直觉的部分。搞明白了它,你对镜像分层的理解会上一个台阶。
5.1 仓库目录长什么样,为什么删了 tag 空间不降
进到/data/registry下面看,会看到两个主要目录:docker/registry/v2/blobs和docker/registry/v2/repositories。
blobs里存的是实际数据,按 sha256 摘要分目录,有两层:blobs/sha256/ab/abcdef.../data。这是所有镜像层、配置文件的真实内容。一个 layer 只存一份,不管被多少个镜像引用。
repositories里存的是"索引",按仓库名/_manifests/tags/<tag>/current/link这样的层级组织,里面是一个文本文件,内容就是某个 manifest 的摘要。
看到这里就明白了:docker push一个镜像,实际上是往blobs写了几份数据,然后往repositories写了一个指向 manifest 摘要的小文件。删除 tag 只是删掉那个小文件,blobs里的数据一个字节都没动。这就是为什么删了镜像空间不降——数据还在,只是没人引用了。
还有个更隐蔽的情况:你用同一个 tag 反复覆盖推送,比如每次都推myapp:latest。旧的 manifest 失去了 tag 引用,但它的数据还在blobs里躺着。这些"孤儿"数据只有靠垃圾回收才能清掉。
5.2 两步走:先删 manifest,再跑 GC
清理的正确姿势是两步,顺序不能反。
第一步,通过 API 删除 manifest。注意一个容易失败的点:删除接口要求你提供 manifest 的 digest 而不是 tag,所以要先查 digest:
curl -s -I \ -H "Accept: application/vnd.docker.distribution.manifest.v2+json" \ -u youruser:yourpassword \ https://registry.demo.internal/v2/demo/myapp/manifests/v1 \ | grep -i docker-content-digest拿到 digest 之后执行删除:
curl -X DELETE \ -u youruser:yourpassword \ https://registry.demo.internal/v2/demo/myapp/manifests/sha256:xxxxx返回 202 表示删除请求被接受。如果返回 405,说明config.yml里的storage.delete.enabled没打开。
第二步,跑垃圾回收:
docker exec registry \ bin/registry garbage-collect --delete-untagged \ /etc/docker/registry/config.yml这一步会遍历所有 manifest,标记出还被引用的 blob,把没被引用的删掉。--delete-untagged这个参数要慎用,它会删掉所有没有 tag 的 manifest,包括那些你可能还想保留的历史版本。第一次跑建议不带这个参数,先看看输出,确认要删的内容没问题再带上去。
跑 GC 期间最好停止写入。官方建议是让 Registry 进入只读模式,配置方式是:
storage: maintenance: readonly: enabled: true改完重启容器,跑完 GC 再改回来。听起来麻烦,但并发写入时跑 GC 有概率导致正在推送的镜像数据被误判为无引用而删掉,得不偿失。学习环境里最简单粗暴的做法是直接停掉容器,用一个临时容器挂载同一份数据去跑 GC,跑完再启动。
最后看一眼磁盘,du -sh /data/registry数字应该有明显下降。如果没降,检查一下是不是只删了 tag 没删 manifest,或者 GC 没带对参数。
6. 从"能跑通"到"敢长期放着":备份、监控与成本
学习环境也分两种:一种是搞完就删的,一种是打算长期留着的。如果你属于后者,下面三件事值得花半小时做掉,能省掉将来很多麻烦。
6.1 备份不只是拷一个目录
理论上把/data/registry整个目录打包就是完整备份,因为 Registry 是无状态服务,所有数据都在这个目录里。但实际上有几个细节要注意。
第一,备份时最好停写。运行中直接tar一个正在写入的目录,可能拿到不一致的快照——某个 blob 拷了一半,manifest 已经写完了。恢复的时候校验会失败。停容器再打包是最稳的。
第二,备份要包含config.yml、证书、htpasswd 文件。这三样东西丢了,恢复出来的数据是没法直接用的。我习惯把它们和镜像数据放在同一个压缩包里,恢复时一次性还原。
第三,备份要定期验证。做过一次完整恢复演练,你就知道这份备份到底靠不靠谱。具体做法是找一台干净的机器,把压缩包解开,按原配置起容器,curl /v2/_catalog看仓库列表对不对,再随便 pull 一个镜像试试。这个过程大概十分钟,值得做一次。
如果不想自己写脚本,云平台的对象存储一般有命令行工具,配上定时任务,把每天的打包文件传上去,成本很低。学习环境的话,简单点的方案是直接给数据盘打快照,恢复快,但要注意快照的数量和保留策略,别攒了几十个忘了删。
6.2 最低成本的监控方式
不需要上完整的监控体系,对学习环境来说,两个检查项就够:磁盘剩余空间和容器是否存活。
磁盘检查可以写一个十几行的脚本,用df取/data的使用率,超过 80% 就发一封邮件或者推一条消息到某个通知渠道。这个阈值不要设太高,因为 Registry 写入单个大镜像 layer 的时候,临时占用可能比你预期的大,等 95% 才告警往往已经晚了。
容器存活检查更简单,用docker inspect看状态,或者直接curl -k https://127.0.0.1/v2/看有没有返回。配一个每分钟跑一次的定时任务,连续三次失败才告警,避免误报。
还有一项容易被忘掉:证书到期时间。自签证书虽然设了十年有效期,但如果你后来换成受信任机构的证书,一般是三个月一续,到期前没有告警的话,某天早上所有推送突然失败,会很难受。检查方式:
openssl x509 -in /data/certs/server.crt -noout -enddate把这条命令塞进同一个检查脚本里,剩三十天开始提醒,就够用了。
6.3 学习环境的成本控制,用完真的可以停
最后说点实际的。云主机按量计费的话,一台 2 核 4G 的机器,跑一整天和跑一小时的成本差别是很明显的。学习场景的特点是你并不是全天都在用,大部分时间它在空转。
我的做法是:搭建和调试阶段一直开着,跑通之后如果只是偶尔用一下,用完就关机,需要的时候再开。关机状态下只收磁盘费用,很便宜。要注意的是关机再开机后 IP 可能会变,如果你在客户端配置里写的是 IP 而不是域名,每次开机都要重新改配置——这也是我一直建议用域名加 hosts 的原因,改一处就行。
数据盘要单独确认一下计费方式,有些云平台的按量数据盘在不挂载期间仍然计费。另外,如果做了快照,快照本身也占存储成本,定期清理不用的快照。
还有个小技巧:把整套部署过程写成一个脚本,从建目录、写配置、生成证书到起容器,全部串起来。这样每次换机器或者重装,一条命令就能恢复环境。写脚本的过程本身也是一次复盘,你会发现有些步骤其实可以省,有些参数每次都要手动改。我第一次写这个脚本的时候写了八十多行,后来精简到四十行不到,剩下的都是真正必要的配置。
踩过几次坑之后我的体会是,学习自建镜像仓库最值钱的收获不是"我会装 Registry 了",而是你从此看镜像、看 tag、看 digest 的眼光变了。你会知道为什么latest是个坏习惯(它每次都指向不同的 manifest,而 manifest 是不可变的),会知道为什么两个看起来一样大的镜像实际占用的磁盘可能差很多(共享层),也会知道为什么清理镜像不能靠docker rmi而要靠服务端的 GC。这些认知在你后面接触任何镜像仓库方案时都能复用,包括那些托管服务——它们只是把这一整套东西包装起来,让你不用自己管而已。