云服务器自建私有Docker镜像仓库:Registry部署、认证与GC清理实战 私有 Docker 镜像仓库这件事我第一次动手是在一台 2 核 4G 的云服务器上前后折腾了大半天问题不在于步骤多而在于每一步都有个不起眼的前提没被讲清楚证书放哪个目录、htpasswd 为什么要重启才生效、删了 tag 为什么磁盘不降。这份记录就是把这些前提全部摊开讲一遍从为什么要在云服务器上自建私有 Docker 镜像仓库到选型、部署、客户端推送、存储清理、备份监控一条完整链路走下来。内容偏学习向适合刚接触容器生态、想搞明白镜像到底是怎么被存储和分发的朋友也适合手上有个小团队、准备把内部镜像统一收拢到一台机器上的同学。默认你有一台能连上的 Linux 云主机和基本的 Docker 使用经验其余的前置知识我会在用到的地方补上。1. 私有镜像仓库解决的到底是什么问题很多人第一次接触私有仓库是因为在公共仓库上拉镜像越来越慢或者某个基础镜像突然拉不到了。这只是表象。真正推动团队去自建仓库的通常是三类更具体的不适感。第一类是交付物没法安放公司内部业务镜像包含了自己的代码和配置天然不适合放到公开可查的地方。第二类是构建链路对外部网络形成了硬依赖CI 流水线每次跑都要从公网拉一堆基础镜像出口带宽一抖动整个流水线就开始排队。第三类是协作上的混乱同一个项目A 同学本地构建的镜像 tag 叫app:testB 同学叫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很容易变成照着文档敲命令跑起来了但不知道为什么。方案最低规格磁盘起步上手时间适合场景原生 Registry1 核 2G20G半小时学习原理、个人实验Harbor2 核 4G40G2 小时小团队协作、需要权限Nexus2 核 4G40G2 小时同时要管多种包格式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 \ --restartalways \ -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需要额外处理权限问题没必要。--restartalways不能不写。云主机重启、Docker 服务重启都会导致容器停止加上这个参数才能自动拉起来。启动完检查一下docker logs -f registry日志里出现监听信息就说明起来了。用 curl 验证一下 APIcurl -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 /CCN/STLocal/LLocal/ODemo/CNregistry.demo.internal \ -addext subjectAltNameDNS: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这个接口会返回所有仓库名。想看某个仓库有哪些 tagcurl -u youruser:yourpassword \ https://registry.demo.internal/v2/demo/myapp/tags/list在另一台机器上拉取记得先docker logout再docker login避免用到缓存的旧凭证。拉下来之后为了确认镜像内容没问题可以比对一下镜像 IDdocker 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-registriesx509: certificate signed by unknown authorityCA 没装或目录名不对检查/etc/docker/certs.d/地址/ca.crtx509: certificate is valid for X, not Y证书 SAN 里没有你用的域名重新签发补全 SANconnection refused端口没开或容器没起查安全组、系统防火墙、docker ps401 Unauthorized未登录、密码错、改密码没重启依次排查这三项413 Request Entity Too Large前面有转发层限了体积调大转发层的请求体上限manifest unknowntag 写错或没推成功用 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所以要先查 digestcurl -s -I \ -H Accept: application/vnd.docker.distribution.manifest.v2json \ -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。这些认知在你后面接触任何镜像仓库方案时都能复用包括那些托管服务——它们只是把这一整套东西包装起来让你不用自己管而已。