自建 Docker Registry 私有镜像仓库:从安装到生产级配置
搭建过团队内部持续集成流程的人多半都会有这个体会容器镜像一旦多起来全推到一个公共仓库要么网速让人抓狂要么隐私和合规上心里没底。自己动手装一个 Docker Registry其实是最自然的解法。这是容器镜像的私有仓库服务专门用来存储和分发 Docker 镜像可以理解成自己家里搭的镜像超市想上架就上架想下架就下架不求人。这篇文章我把自己从零开始安装、配置、运行 Docker Registry 的完整过程写下来适合正在做容器化改造、本地开发调试镜像或者想给内网 CI 系统搭建镜像中转的开发者参考。我实战下来的感受是Registry 本身远比想象中轻量——就是一个官方镜像跑到容器里就算安装成功。但真正决定它容不容易用的是后续的存储配置、访问认证、TLS 证书、镜像清理这些细节。这篇文章会把这些细节全部拆开讲清楚附带我踩过的坑和排查思路保证你照着做就能跑起来。1. 内容整体设计与思路拆解1.1 为什么需要自己部署 Registry很多新手会有疑问Docker Hub 明明可以免费拉镜像我 push 上去不就行了这个问题的答案要分场景看。如果你只是个人开发、网络畅通、不涉及敏感业务那 Docker Hub 确实够用。但一旦涉及团队协作、离线环境、自动化构建公共仓库的问题就开始暴露了。我举几个实际场景。第一个某公司内网环境完全和互联网隔离开发好的镜像要发到生产环境总不能拿 U 盘拷 tar 包吧。有了私有 Registry内网机器直接docker pull内网地址速度快、链路稳。第二个CI/CD 流水线每次构建完镜像如果都推到 Docker Hub构建次数一多不仅慢还可能触发限流。自建一个局域网内的 Registrypush 和 pull 都是毫秒级延迟。第三个镜像内容可能包含内部业务代码、模型文件、客户数据随便推到公共平台不符合安全规范私有仓库可以把访问权限收口到自己手里。也有人会问既然要自建为什么不用功能更全的 Harbor我对比过两者的定位Harbor 是重量级企业方案有 Web UI、权限管理、镜像同步、漏洞扫描这些组件部署起来需要 docker-compose 甚至 Kubernetes对资源占用不低而 Registry 是官方出品的轻量级组件核心功能就是存储和分发镜像部署就是一个单容器。如果你的需求就是“内部能 push、能 pull、链路稳定”Registry 的简洁反而是优势。如果后续要多人协作、分项目权限管理再迁到 Harbor 也来得及毕竟两者都兼容标准 Registry API。1.2 方案选型官方版、发行版与功能取舍选型阶段我整理过几个方向。官方registry:2镜像是首选因为它是 Docker 官方的默认实现兼容性最好文档也齐全。社区里还有一些基于 Registry 的增强发行版比如带 UI 控制台的、带认证插件的但它们要么需要额外组件集成要么维护活跃度不稳定。我的观点是核心服务一定用官方原版附加功能通过独立组件补充比如 Nginx 做反向代理htpasswd 做认证这样每个环节都可控出问题容易排查。功能取舍上我给自己定的基线是第一存储要支持持久化容器重启镜像不丢第二要有基本的认证机制不能裸奔在网络上第三要在生产环境用 TLS 加密传输第四可以配置存储限额和清理策略。至于 Web UI、日志审计、多租户这些第一版先不做等真正有需要再加。这里必须强调一个认知Docker Registry 本身是高度模块化的它允许你通过配置文件切换存储驱动、认证方式、TLS 设置甚至还能接 Redis 做缓存。也就是说虽然第一版跑起来很简单但它的架构并不简陋只是把复杂度藏在了 YAML 配置里。理解这一点你后面遇到问题就不会慌——不是你的部署错了而是配置项没有配对。2. 核心细节解析与实操要点2.1 镜像仓库的核心工作流程在动手安装之前有必要把 Registry 的工作机制理清楚。它本质上是一个 HTTP API 服务兼容 OCI Distribution Spec。当客户端执行docker push时流程大致是客户端请求 Registry 获取上传凭证然后把镜像分层逐个上传最后提交 manifestdocker pull则是反过来先拉取 manifest再按需下载各层。这个设计决定了两个关键配置点一是客户端要和 Registry 协商认证方式二是 Registry 要有地方持久化这些镜像层数据。最常见的误区是很多人想当然地以为 Registry 会把镜像存成一个一个的 tar 包。实际上它内部有自己的一套目录结构比如docker/registry/v2/repositories目录下按仓库名组织再往下是_layers存放去重后的镜像层_manifests存放标签引用。理解这个结构的意义在于当你需要备份或迁移数据时直接把整个数据目录拷贝走就行当你发现磁盘占用异常增长时也知道应该看哪一层目录。另一个重要的运作机制是镜像层的去重与引用计数。同一个基础镜像的不同业务镜像底层的几十 MB 甚至几百 MB 层是共享的。Registry 通过 digest 就能唯一标识某个层所以大量镜像共存时实际占用空间远小于各镜像大小之和。后面讲到垃圾回收时这个概念还会再次出现——简单说删除镜像并不等于释放磁盘空间只有执行 GC 才能清理掉那些不再被引用的层。2.2 安装方式的演进从裸容器到编排部署安装 Registry 有多条路径我建议按场景选择。最简单的是直接docker run单容器启动适合本地开发测试、临时搭建。第二种是用 docker-compose 管理适合固定环境把端口映射、数据卷、环境变量、配置文件都以声明式方式固化下来。第三种是放进 Kubernetes 当成 Deployment 服务运行适合生产环境配合 Service 暴露端口、PVC 存储还能利用集群的滚动更新能力。我先讲最直接的安装方法。在 Docker 环境下执行以下命令就能拉起一个最基本、监听 5000 端口的 Registrydocker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ registry:2参数解释一下-d是后台运行--name给容器命名-p 5000:5000把容器内的 5000 端口映射到宿主机-v则把容器内部的/var/lib/registry挂载到宿主机的数据目录。这里最重要的一步就是挂载数据卷否则容器一旦删掉重建里面存放的所有镜像都会随之消失等于白搭。验证是否安装成功可以用curl -I http://localhost:5000/v2/查看响应头。正常情况下 Registry 会返回200 OK表示服务已经就绪。如果是远程访问则需要把localhost换成实际 IP。这一步通过后Registry 就已经具备 push 和 pull 能力了。2.3 配置文件 YAML 的字段详解Registry 的高级玩法都集中在配置文件里。默认情况下Registry 会使用内置的默认配置启动但生产环境基本都需要自定义。我整理了一份精简的config.yml并逐项说明version: 0.1 log: level: info storage: filesystem: rootdirectory: /var/lib/registry delete: enabled: true http: addr: :5000 headers: X-Content-Type-Options: [nosniff] auth: htpasswd: realm: basic-realm path: /auth/htpasswdversion表示配置文件的版本号目前固定为 0.1不要随意改动。log.level可以按需调整排障时切成debug会输出更多请求细节。storage段是整个配置的核心filesystem指定存储驱动和根目录delete.enabled决定是否允许通过 API 删除镜像我建议开启否则后面清理仓库时会很被动。http段配置监听地址和响应头其中X-Content-Type-Options是一个安全加固选项能防止浏览器解析错误类型导致的安全问题。auth段配置认证方式这里用了 htpasswd 基础认证realm是认证提示信息path是存放用户名密码哈希的文件路径容器内部映射。比较常踩的坑是用户把配置文件复制到宿主机时没注意权限导致容器内读取失败。有一个快速验证配置的方法用registry serve --dry-run可以只校验配置不启动服务。我第一次部署时就是靠这个命令抓出 YAML 格式错误的。3. 实操过程与核心环节实现3.1 从零搭建流程持久化、数据卷与端口规划我给的这套搭建方案是一个可以平滑迁移到生产环境的起点。假设宿主机数据目录为/opt/registry先创建好再启动容器mkdir -p /opt/registry/data /opt/registry/auth /opt/registry/certs三个目录分别用于镜像数据、认证文件、证书文件。这样做的好处是后续加认证、加 TLS 时不用重建容器只需改配置重启。端口规划上标准 Registry 端口是 5000但如果宿主机 5000 被占用可以换一个高位端口比如 15000映射到容器内仍然是 5000。客户端使用时镜像 tag 里的地址要写成宿主机IP:端口。下一步把认证加上。首先用 htpasswd 工具生成密码文件容器镜像自带了htpasswd命令可以直接生成docker run --rm --entrypoint htpasswd registry:2 \ -Bbn admin 你的密码 /opt/registry/auth/htpasswd这里我特意用了-B参数强制 bcrypt 算法加密比默认的 crypt 更安全。再追加一个用户时不能重定向覆盖要用追加。之前我把两个用户写进一条命令第二个直接覆盖了第一个导致第一个账号怎么都登不上花了不少时间排查。接着准备一个带认证的配置/opt/registry/config.yml内容参考上文然后用挂载方式重新启动docker run -d \ --name registry \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/auth:/auth \ -v /opt/registry/config.yml:/etc/docker/registry/config.yml \ registry:2重启后执行docker login 宿主机IP:5000输入刚刚创建的账号密码。如果 login 成功说明认证已经生效。这里有个经验密码文件路径在 YAML 里写的是/auth/htpasswd对应容器内路径而不是宿主机的绝对路径很多新手第一次就栽在这里。3.2 镜像推送、拉取全流程tag 命名规范镜像能 push 到自己的 Registry关键点是 tag 的命名规范。Docker 原生的规则是镜像名中出现IP:端口前缀时就会解析为一个 Registry 地址。所以推送前需要先把本地镜像重打 tagdocker tag ubuntu:latest 宿主机IP:5000/library/ubuntu:latest docker push 宿主机IP:5000/library/ubuntu:latest我在实际部署中习惯用一个内网域名作为仓库地址比如docker.internal:5000这样本地docker tag的命令看起来更整洁。使用域名时记得在 DNS 或/etc/hosts里做好解析否则客户端根本找不到服务。还有一个团队协作的技巧规定所有镜像统一加项目名/服务名前缀例如myapp/api-server避免仓库混乱。平台上展示的镜像列表是扁平化的名称层级组织好前缀之后用脚本扫出来就是一目了然的列表。push 到带认证的仓库时Docker 会把登录信息缓存在~/.docker/config.json中并做 base64 编码存起来。一旦密码轮换需要先docker logout再重新登录否则旧的凭证会一直生效给你的直觉是密码没改成功其实只是客户端缓存了旧凭证。拉取时同样指定完整地址docker pull 宿主机IP:5000/myapp/api-server:latest如果你的客户端和 Registry 之间有网络代理记得给 Docker daemon 配置代理环境变量否则大镜像的 push/pull 可能在中途断开。3.3 TLS 加密与证书信任链配置默认的纯 HTTP Registry 在局域网里可以跑但跨网段、跨机房传输时镜像层数据是明文裸奔的被截获的风险不小。生产环境我会强制开启 TLS。如果公司有内部 CA可以直接向它申请证书如果没有就用 openssl 生成自签名证书openssl req -newkey rsa:2048 -nodes -keyout /opt/registry/certs/domain.key \ -x509 -days 365 -out /opt/registry/certs/domain.crt \ -subj /CNdocker.internal生成后修改config.yml的http段http: addr: :5000 tls: certificate: /certs/domain.crt key: /certs/domain.key重启容器时挂载证书目录。客户端这边如果你使用的是自签名证书需要将其加入 Docker daemon 的信任列表做法是把 crt 文件放到/etc/docker/certs.d/docker.internal:5000/ca.crt然后重启 Docker 服务。这一步很多教程省略导致排障时一直报x509: certificate signed by unknown authority其实和 Registry 本身无关是客户端不信任证书。这里我就被绕过一个下午一开始怀疑证书生成命令出错后来查 Docker 日志才发现 service 没重启证书没有被加载。TLS 配好之后http.port 之前的配置文件里的其他默认值基本不用动。有一点要提醒如果以后迁移到 Kubernetes用 Ingress 暴露服务时证书管理往往交给 Ingress ControllerRegistry 容器本身可以不配tls字段否则会有双重证书的混乱问题。4. 常见问题与排查技巧实录4.1 push 失败HTTP 响应错误与证书警告最经典的报错是error parsing HTTP 400 response body: unexpected end of JSON input或者http: server gave HTTP response to HTTPS client这两个错误基本都是客户端和 Registry 之间协议不一致造成的。前者常见于 Registry 自身返回了非 JSON 响应比如启动了但不健康、或者某个后端存储异常后者则是因为 Registry 只监听 HTTP但 Docker 客户端默认以 HTTPS 连接远端仓库。解决办法是向 Docker daemon 的/etc/docker/daemon.json添加入口{ insecure-registries: [docker.internal:5000] }注意这里的 JSON 一定要合法否则 Docker 服务会直接起不来。修改完执行systemctl restart docker不同系统的命令可能不同我这边是重启进程。如果你已经把 TLS 配好了就不应该设置insecure-registries了否则客户端会跳过证书校验等于把加密通道退化成裸通道。另一个常见情况是 push 大镜像时中途报blob upload invalid之类的 500 错误。多数原因是 Registry 容器所在宿主机磁盘满了或者 Nginx 反向代理的超时时间太短。查看 Registry 日志最有效的命令是docker logs --tail 100 registry我遇到过磁盘分区被镜像填满导致 push 失败日志里反复报no space left on device。清理磁盘之后还要手动执行一次 GC 才能真正释放这个我放到下一节细说。4.2 磁盘空间泄漏与垃圾回收机制先纠正一个认知误区通过 API 删除镜像后磁盘空间不会立刻释放。Registry 的delete只是把镜像标记为不可引用真正的数据块还留在存储里必须触发垃圾回收GC才会删除孤儿层。GC 的方式有两种。老的方案是停止 Registry 容器后执行docker exec registry registry garbage-collect /etc/docker/registry/config.yml如果 Registry 正在运行新的方案允许在线执行registry garbage-collect并加上--delete-untagged参数可以顺带清理没有 tag 的镜像。实际操作中我强烈建议先备份存储目录再执行 GC因为一旦误删了仍在使用的层是不可恢复的。关于镜像删除目前标准的方式是调用 API例如删除某个仓库的特定 tagcurl -X DELETE http://docker.internal:5000/v2/myapp/api-server/manifests/digest这里有几个坑一是 API 要求的删除参数是 manifest 的 digest 而不是 tag你需要先GET /v2/name/manifests/tag并读取响应头里的Docker-Content-Digest字段二是如果在配置里没开delete.enabled这个 DELETE 请求会返回 405 或 400三是即使删除成功访问列表时该 tag 还可能被缓存一段时间。我习惯的清理流程是先用 API 删除不用的镜像和 tag然后用 GC 命令删除引用计数为零的层最后检查数据目录的实际占用是否下降。如果占用没变化多半是 GC 没有迁移 storage 里delete字段开启或者还有容器在引用这些层。4.3 认证与多用户管理的常见陷阱htpasswd 认证模式有一个隐藏缺陷所有用户共享同一个镜像仓库命名空间没有项目隔离。如果团队内必须为不同项目设置不同权限单靠 Registry 的 htpasswd 是不够的需要引入更完整的权限系统。不过对于大多数内部场景一个账号 规范命名前缀就够用了。我碰到过的最尴尬的认证问题是配置文件里的 htpasswd 路径和容器挂载路径不一致导致每次docker login都失败日志里提示找不到/auth/htpasswd。排查思路是直接docker exec -it registry cat /auth/htpasswd确认文件在容器内是否可见。如果文件不存在优先检查挂载参数和配置文件里的路径是否完全对应。多用户管理方面我给团队定的流程是为每个人开通独立账号密码强制轮换每个项目的流水线使用单独的机器账号机器账号密码不和任何人共用。这样某人离开团队时只需禁用对应账号即可不用把所有密码都换一遍。4.4 日志与监控的调试心得Registry 默认日志会打到容器 stdout用docker logs -f registry就能实时查看。生产环境我会把日志接入日志收集系统确保服务异常时有记录可查。日志级别建议日常用info遇到问题时临时改成debug并重启服务不过 debug 日志会很啰嗦排查完记得改回来。如果你用 Nginx 做反向代理记得在 Nginx 配置里调大client_max_body_size和代理超时时间。我一开始没设置client_max_body_sizepush 稍大一点的镜像就返回 413。这个教训提醒我Registry 自身的配置只是链路的一部分链路里任何一个代理组件都可能成为瓶颈。5. 扩充从单机到高可用与自动化运维5.1 存储驱动的扩展选型单机 filesystem 存储是默认方案但服务器一旦宕机或数据盘损坏所有镜像都会丢失。如果条件允许我会把存储切到对象存储上比如云厂商的对象存储服务、自建的 MinIO、或者是支持 S3 协议的存储。Registry 的存储配置切换非常直观只要修改config.yml的storage段storage: s3: accesskey: 你的AK secretkey: 你的SK region: endpoint bucket: my-docker-registry rootdirectory: /registry前提是网络连通性足够好并且密钥权限最小化——只给这个 bucket 的读写权限。这个配置的好处是多个 Registry 实例可以共享同一个存储后端天然实现多副本读。但要注意多实例部署时如果每个实例都开启缓存可能出现缓存不一致的问题官方推荐配置 Redis 来缓存层信息。我的实际经验是小规模团队先别急着上对象存储单机加定时备份是成本最低的方案。5.2 数据备份与迁移策略Registry 的数据备份本质上就是备份整个数据目录但要注意备份期间的一致性避免出现半写状态。我用的方法是先用registry garbage-collect把临时数据清一遍然后用tar对存储目录打快照tar -czf registry-backup-$(date %F).tar.gz -C /opt/registry data备份文件要放到另一台机器不能和 Registry 数据存放在同一个磁盘上。迁移到新服务器时把备份解压到新的数据目录用同样的配置启动容器镜像就会被无缝承接。这里我特别提醒一个细节tar 解压后的文件属主必须是容器内写数据的用户 UID通常是 1000 或 100999否则容器没权限写入新数据。第一次迁移时我没注意新服务器上 push 任何镜像都报权限错误后来 chown 一下就好了。5.3 自动化脚本清理过期镜像我在实践中写过简单的清理脚本原理是把所有仓库的 tag 列表拉出来和“保留策略”比对然后调用 DELETE API 删除过期 tag 的 digest。例如保留每个仓库最近 10 个版本。脚本里还要把这一步的输出记录保存下来方便追溯。需要注意这种动态删除操作对正在运行的服务有一定风险建议先在一个测试仓库验证再对生产仓库执行并且执行前先备份数据目录。由于篇幅关系完整脚本我就不贴出来了但逻辑核心就三步列出仓库、列出 tag 和 digest、按策略调用 DELETE。这个脚本后来我们放在计划任务里每周跑一次磁盘占用长期维持在一个合理的水平省去了手动清理的负担。6. 需要你自行验证的细节与个性化调整这篇文章覆盖了 Registry 从零到可用的各个环节但当你真正动手部署时还是会有一些变数需要根据自己的环境调整。下面这一节是我建议你重点验证的部分不同操作系统、不同 Docker 版本下重启 Docker 服务的命令可能不同磁盘挂载权限规则也不同。我文中给的是最常见的做法但你在 Windows 容器环境、macOS 的 Docker Desktop、Linux 发行版之间切换时路径和权限的差异会非常明显。Registry 容器的镜像 tag 版本会持续更新建议你在启动前先查询一下最新的稳定版而不是直接使用文中示例的registry:2通用 tag。大版本升级前仔细阅读官方变更说明避免因 API 变化导致兼容问题。文中提到的insecure-registries、证书信任配置都需要根据你实际的域名、IP、证书有效时间来适配。证书过期对服务的影响是很大的记得给自己设置证书到期提醒。如果你把 Registry 部署在带防火墙的环境里5000 端口的入站规则可能默认没开如果用了反向代理代理的配置文件也要贴合实际路径和端口。不同的配置组合导致的服务调整是运维过程中最消耗精力的部分。容器资源限制值得考虑。Registry 默认会使用宿主机所有可用内存做缓存如果同一台机器上还跑着其他业务可以用--memory参数限制容器内存上限避免 OOM 影响其他服务。说实话Registry 的门槛并不高难就难在“跑起来只是开始跑得稳才是目的”。我在多台服务器上重复部署过 Registry每一次都能学到新东西第一次学会挂载持久化第二次学会配置认证第三次遇到证书问题第四次开始琢磨脚本清理。如果你照着这篇文章搭了一套后来也遇到了本文没有覆盖的奇葩问题那反而说明你在走进这个领域的深水区。能踩到别人没踩过的坑本身就是很大的收获。