1. 私有镜像库到底解决什么问题团队里只要超过三个人协作迟早会撞上同一个尴尬本地构建的镜像只能在自己机器上跑想让同事拉取就得推到公共仓库可公司内部业务镜像又不可能公开。早期我们用过最土的办法——docker save打包成 tar 文件通过内部文件服务器传来传去再docker load导入。这套流程在镜像只有几百兆、更新频率一周一次的时候勉强能用一旦镜像涨到两三个 G、每天要迭代七八次光传输和导入就够喝一壶。私有镜像库要解决的核心问题就三个字存、管、取。存是给镜像一个稳定的落脚点不再依赖某台开发机的本地存储管是能控制谁能推、谁能拉、哪些镜像保留多久取是让集群里的每一台机器都能用一条docker pull命令拿到指定版本的镜像。这三点听起来简单但真正落地时会牵扯出认证、存储后端、网络暴露、垃圾回收等一连串细节。这篇文章适合谁看如果你正在给一个小团队搭建内部镜像仓库或者你所在的环境不允许访问外部镜像服务又或者你只是想在自己的一台测试机上跑一个轻量级的镜像库来练手那接下来的内容基本能覆盖你 90% 的需求。我会从整体设计思路讲起把每个关键决策背后的理由说清楚再给出可以直接复现的操作步骤最后把我在实际搭建过程中踩过的坑整理成排查清单。需要提前说明的是镜像库的搭建方式有很多种有官方提供的 Registry 镜像也有带图形界面的第三方方案。本文聚焦在最基础、最通用、依赖最少的那条路线上——用官方 Registry 镜像起一个容器配合合理的存储和认证配置把它变成一个能长期稳定运行的内部服务。这条路线的优势是组件少、可控性强出问题容易定位代价是图形界面、多用户权限这些高级功能需要自己额外补但对于大多数中小团队来说够用了。2. 整体架构与方案选型思路2.1 为什么选官方 Registry 而不是带界面的方案市面上能搭私有镜像库的方案大致分两类。一类是官方 Registry就是一个纯粹的镜像存储服务只提供推拉接口没有网页界面配置全靠环境变量和配置文件。另一类是带管理界面的方案通常会在 Registry 前面套一层 Web 服务提供用户管理、镜像浏览、权限分配等功能。我选官方 Registry 的理由很直接依赖最少故障面最小。带界面的方案往往需要额外的数据库、缓存、Web 容器组件一多出问题的概率就成倍上升。有一次我帮朋友排查一个镜像库拉取失败的问题折腾了半天才发现是他那套带界面的方案里某个中间件容器内存溢出被系统杀掉了而 Registry 本身其实一直好好的。如果一开始就用最简架构这种问题根本不会出现。当然官方 Registry 也不是没有短板。它默认没有用户体系认证要靠外挂没有垃圾回收的自动触发得手动跑命令镜像的浏览只能靠命令行或者自己写脚本查 API。但这些短板都可以通过合理的运维手段补上而且补的过程是可控的、透明的比黑盒子里出问题强得多。2.2 存储后端的选择本地盘还是对象存储Registry 支持多种存储后端常见的有本地文件系统、对象存储、以及一些分布式文件系统。选哪种取决于你的镜像量和可用性要求。本地文件系统是最简单的直接把容器内的一个目录挂载到宿主机上就行。它的优点是零依赖、读写快、排查方便镜像文件就实实在在躺在磁盘上出问题了直接进去看。缺点是单点宿主机磁盘坏了数据就没了而且没法横向扩展。对象存储的优点是容量几乎无限、天然支持多副本、不怕单机故障。缺点是需要额外的对象存储服务配置起来多一层而且如果对象存储本身是自建的那又引入了新的运维负担。我的建议是如果团队规模在几十人以内镜像总量在几百 G 以内直接用本地盘加定期备份就够了。别为了追求所谓的“高可用”把架构搞得过于复杂小团队最怕的就是维护一堆自己都不完全理解的服务。等镜像量真的涨到本地盘扛不住再迁移到对象存储也不迟Registry 的存储后端是可以切换的迁移成本没有想象中那么高。2.3 网络暴露方式直接暴露还是走反向代理Registry 默认监听 5000 端口走的是 HTTP。但 Docker 客户端默认要求用 HTTPS 访问镜像库除非你把某个地址显式配置成“不安全仓库”。这就带来一个选择是直接暴露 5000 端口并配置客户端信任还是在前端加一个反向代理来处理 TLS。直接暴露加客户端配置的方式适合内网测试环境配置简单几分钟就能跑起来。但它的缺点是每个需要访问的客户端都要改配置机器一多就很烦而且 HTTP 传输镜像内容在安全上也不够稳妥。走反向代理的方式是在 Registry 前面放一个 Nginx 之类的服务由它来终结 TLS然后把请求转发给后端的 Registry。这样客户端只需要正常用 HTTPS 访问域名就行不用改任何配置。缺点是多了反向代理这一层需要额外维护证书和代理配置。我的实际做法是测试阶段直接暴露端口快速验证正式环境一律走反向代理加 TLS。证书可以用内部 CA 签发也可以申请一个内部域名对应的证书看团队的基础设施情况。反向代理的配置不复杂后面我会给出具体的配置片段。3. 核心组件与关键参数详解3.1 Registry 容器的关键环境变量启动 Registry 容器时有几个环境变量是必须关注的它们直接决定了服务的行为。REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY指定镜像数据在容器内的存储路径默认是/var/lib/registry。我们通常会把这个路径挂载到宿主机的某个目录比如/data/registry这样容器重建时数据不会丢。REGISTRY_HTTP_ADDR指定监听地址和端口默认是:5000。如果前面有反向代理这个保持默认就行反向代理负责对外暴露。REGISTRY_STORAGE_DELETE_ENABLED这个变量很关键默认是false意味着你没法删除镜像。很多人搭完之后发现删不掉镜像就是因为忘了设这个。把它设成true才能通过 API 删除镜像标签和清单。REGISTRY_AUTH和相关的认证变量在需要开启认证时使用。如果暂时不开启认证可以不设。但要注意一旦服务暴露在可被非信任网络访问的位置没有认证就等于把镜像库完全敞开这是很危险的。3.2 存储目录的规划与权限存储目录的规划看起来是小事但规划不好后期很麻烦。我一般会在宿主机上单独划一个分区或者目录来放镜像数据比如/data/registry。这样做的好处是镜像数据和系统数据分离磁盘满了不会影响系统运行备份和迁移也方便。权限方面Registry 容器默认以 root 用户运行写入挂载目录时用的是容器内的 root。如果宿主机上的目录权限设置不当可能会出现写入失败。最简单的做法是把宿主机目录的属主设成 root权限设成 755让容器内的 root 能正常读写。如果你出于安全考虑想让容器以非 root 用户运行那就需要仔细调整目录属主和容器启动用户这部分后面在注意事项里会展开。3.3 认证机制的选型基础认证还是令牌认证Registry 支持两种认证方式基础认证和令牌认证。基础认证就是简单的用户名密码配置简单适合小规模使用。令牌认证需要一个额外的认证服务来签发令牌适合需要细粒度权限控制的场景。对于大多数中小团队基础认证完全够用。它的配置方式是在 Registry 前面放一个支持基础认证的反向代理或者用 Registry 自带的 htpasswd 认证。htpasswd 方式需要生成一个密码文件然后在 Registry 配置里指定这个文件。这种方式的好处是不依赖外部服务坏处是用户管理靠手动编辑文件用户多了会有点繁琐。令牌认证的典型场景是每个用户或每个服务有独立的凭证并且需要控制某个用户只能推拉特定命名空间下的镜像。这种需求在大型组织里常见小团队一般用不上。如果你确实需要可以考虑在 Registry 前面套一层支持细粒度权限的代理服务但那就超出本文的范围了。4. 从零搭建的完整实操流程4.1 环境准备与目录创建假设我们在一台内网服务器上操作操作系统是常见的 Linux 发行版已经装好了容器运行时。首先创建数据目录和配置目录mkdir -p /data/registry mkdir -p /etc/registry/data/registry用来存放镜像数据/etc/registry用来存放配置文件和认证文件。这两个目录分开是为了让配置和数据解耦迁移数据时不用动配置改配置时也不用碰数据。接下来生成基础认证的密码文件。需要用到htpasswd工具如果系统里没有可以通过包管理器安装。生成密码文件的命令如下htpasswd -Bbn myuser mypassword /etc/registry/htpasswd这里-B表示用 bcrypt 加密-b表示在命令行里直接传密码-n表示把结果输出到标准输出而不是写入文件。myuser和mypassword替换成你实际要用的用户名和密码。生成的文件里会有一行myuser:$2y$...的内容这就是认证凭据。注意密码文件里存的是哈希值但依然要控制好文件权限建议设成 600只有 root 能读。另外bcrypt 加密强度较高生成和校验会稍微慢一点但安全性更好值得。4.2 编写 Registry 配置文件虽然 Registry 可以通过环境变量配置但配置项一多用配置文件更清晰。在/etc/registry/config.yml里写入以下内容version: 0.1 log: level: info formatter: text storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true http: addr: :5000 secret: some-random-secret-string auth: htpasswd: realm: basic-realm path: /etc/registry/htpasswd逐项解释一下。log部分设置日志级别和格式info级别足够日常使用出问题时可以临时调到debug。storage部分指定用文件系统存储根目录是容器内的/var/lib/registry并且开启了删除功能。http部分的secret用于签名状态令牌随便填一个足够长的随机字符串就行但不要用默认值。auth部分指定用 htpasswd 认证realm是认证域的名称可以随便起path指向容器内的密码文件路径。提示http.secret这个值如果泄露攻击者可能伪造令牌。虽然在内网环境下风险相对可控但还是建议用一个随机生成的长字符串并且不要提交到代码仓库里。4.3 启动 Registry 容器配置文件准备好之后用以下命令启动容器docker run -d \ --name registry \ --restart always \ -p 5000:5000 \ -v /data/registry:/var/lib/registry \ -v /etc/registry/config.yml:/etc/registry/config.yml \ -v /etc/registry/htpasswd:/etc/registry/htpasswd \ registry:2这里把数据目录、配置文件和密码文件都挂载进容器。--restart always保证容器在宿主机重启后自动拉起。-p 5000:5000把端口暴露出来如果前面有反向代理可以改成只监听本地回环地址比如-p 127.0.0.1:5000:5000这样外部只能通过反向代理访问。启动之后用docker ps确认容器在运行再用curl测试一下接口curl -u myuser:mypassword http://localhost:5000/v2/如果返回{}说明服务正常且认证生效。如果返回 401说明认证配置有问题检查密码文件路径和内容。如果连接被拒绝检查容器是否真的起来了以及端口映射是否正确。4.4 配置客户端并推送第一个镜像Docker 客户端默认要求 HTTPS所以直接推送到 HTTP 的 Registry 会报错。有两种解决办法一是配置反向代理加 TLS二是把该地址标记为不安全仓库。测试阶段用第二种更快。在需要访问 Registry 的机器上编辑 Docker 的配置文件。对于使用 systemd 的系统通常是/etc/docker/daemon.json加入以下内容{ insecure-registries: [registry.internal:5000] }把registry.internal:5000替换成你实际的地址。改完之后重启 Docker 服务使配置生效。然后登录docker login registry.internal:5000输入用户名密码提示登录成功后就可以推送镜像了。先给本地镜像打个标签docker tag myapp:latest registry.internal:5000/team/myapp:latest然后推送docker push registry.internal:5000/team/myapp:latest推送完成后可以在宿主机上查看数据目录应该能看到对应的镜像文件已经落盘。拉取的时候用完整的镜像地址即可docker pull registry.internal:5000/team/myapp:latest4.5 加上反向代理和 TLS 的正式配置测试通过之后正式环境建议加上反向代理和 TLS。以 Nginx 为例配置片段大致如下server { listen 443 ssl; server_name registry.internal; ssl_certificate /etc/nginx/certs/registry.crt; ssl_certificate_key /etc/nginx/certs/registry.key; client_max_body_size 0; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $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; } }client_max_body_size 0这一行很关键它表示不限制请求体大小。镜像层动辄几百兆如果这里设了限制推送大镜像时会失败。proxy_set_header那几行是为了把客户端的真实信息传给后端方便日志记录和排查。配置好之后客户端就不需要insecure-registries了直接用域名访问即可。记得把 Registry 容器的端口映射改成只监听本地回环避免绕过反向代理直接访问。5. 日常运维与常见问题排查5.1 镜像清理与垃圾回收Registry 删除镜像标签之后底层的 blob 数据并不会立即释放需要手动触发垃圾回收。这是很多人容易忽略的一点导致磁盘空间只增不减。删除镜像可以通过 API 操作。先获取镜像的 digestcurl -u myuser:mypassword \ -H Accept: application/vnd.docker.distribution.manifest.v2json \ -I https://registry.internal/v2/team/myapp/manifests/latest响应头里的Docker-Content-Digest就是 digest。然后删除curl -u myuser:mypassword -X DELETE \ https://registry.internal/v2/team/myapp/manifests/digest删除之后进入 Registry 容器执行垃圾回收docker exec registry registry garbage-collect /etc/registry/config.yml这个命令会扫描所有 blob把没有被任何清单引用的删掉。注意垃圾回收期间最好停止写入否则可能出现竞态。我一般会选在低峰期执行执行前先确认没有正在进行的推送。注意垃圾回收命令默认是“只读”模式也就是只报告不删除。要真正删除需要加上-m参数。但加-m之前一定要确认配置和备份都到位因为删错了没法恢复。5.2 常见问题速查表问题现象可能原因排查方向推送时报 413反向代理限制了请求体大小检查client_max_body_size配置推送时报 401认证失败检查密码文件、用户名密码、认证配置拉取时报 manifest unknown镜像标签不存在或已被删除用 API 查询标签列表确认磁盘空间不释放未执行垃圾回收删除标签后手动跑垃圾回收容器启动后立即退出配置文件格式错误查看容器日志检查 YAML 缩进客户端报证书错误TLS 证书不被信任把内部 CA 证书加入客户端信任列表5.3 实操心得与避坑建议第一个坑是配置文件缩进。YAML 对缩进极其敏感多一个空格少一个空格都可能导致解析失败。我习惯用两个空格作为一级缩进写完之后用在线工具或者python -c import yaml; yaml.safe_load(open(config.yml))验证一下。第二个坑是密码文件路径。配置文件里写的是容器内的路径不是宿主机的路径。挂载的时候要确保宿主机路径和容器内路径对应正确否则容器里找不到文件认证会直接失败。第三个坑是垃圾回收的时机。如果在推送过程中跑垃圾回收可能会把正在上传的 blob 误删导致推送中断。一定要在确认没有写入操作的时候执行。第四个坑是磁盘空间监控。镜像库的磁盘占用增长往往是非线性的平时可能几天才涨一点某天突然推了个大镜像就涨几十 G。建议给数据目录加上磁盘使用率监控超过阈值就告警别等磁盘满了才发现。第五个坑是备份策略。本地盘存储虽然简单但单点风险是真实存在的。我一般会定期把数据目录同步到另一台机器或者备份存储上。同步的时候要注意Registry 运行期间数据文件可能正在被写入直接复制可能得到不一致的快照。稳妥的做法是先停容器再备份或者用支持快照的文件系统。6. 扩展方向与进阶玩法6.1 多实例与高可用当镜像库成为关键依赖之后单实例就成了风险点。要做高可用思路是把存储后端换成共享存储或者对象存储然后起多个 Registry 实例前面用负载均衡分发请求。这样任何一个实例挂掉其他实例还能继续服务。不过要提醒一句多实例方案的前提是存储后端支持并发访问。本地文件系统挂载到多个实例上是不行的会出现数据不一致。对象存储天然支持所以如果打算走高可用路线存储后端的选择要提前规划好。6.2 与持续集成流程的集成镜像库搭好之后下一步通常是把它接入持续集成流程。典型的做法是在构建阶段生成镜像并打上包含提交信息的标签然后推送到私有库部署阶段再从私有库拉取。这样每次构建的产物都有据可查回滚的时候也能快速定位到具体版本。标签命名建议包含日期和提交短哈希比如20240115-a1b2c3d这样既能看出时间顺序又能对应到具体代码版本。不要只用latest因为latest会被覆盖出问题时无法追溯。6.3 镜像体积优化私有库的磁盘占用很大程度上取决于镜像体积。优化镜像体积不仅能省磁盘还能加快推拉速度。常见的优化手段包括使用更小的基础镜像、合并构建层、清理构建缓存、使用多阶段构建。这些手段在构建阶段做效果会直接体现在私有库的存储上。我个人的经验是一个未经优化的应用镜像动辄上 G经过多阶段构建和基础镜像替换之后往往能压到几百兆甚至更小。这个优化带来的收益是长期的值得在搭建私有库的同时一并考虑。最后分享一个我在实际运维中的小习惯给每个镜像库的镜像都加上描述性的标签并且在团队内部维护一个简单的镜像清单文档记录每个镜像的用途、负责人和更新频率。这个习惯看起来不起眼但当镜像数量涨到几十上百个之后没有清单根本记不住哪个是哪个。文档不用很正式一个共享的表格就够了关键是持续维护。