
最早接触容器化的时候我把大部分精力都花在折腾镜像构建上反而忽略了镜像分发这一环。直到测试环境里要部署多台机器还在用docker save打成 tar 包再一个个拷过去才意识到没有仓库的容器环境根本没法规模化。Docker Registry镜像仓库就是解决这个问题的核心组件它负责存储镜像、管理版本、提供拉取和推送接口。今天这篇不讲虚的专门把 Registry 的概念、内部机制、私有部署和日常维护讲清楚让新手也能照着搭出一套能用的私有镜像仓库。下面内容偏实际操作我会把自己踩过的坑和思考一并写出来想直接上手的可以直接拉到第 3 节看部署命令。1. 先把概念理清楚Registry、Repository、Tag 与 Image1.1 为什么需要一个专门存放镜像的仓库Docker 镜像本质上是文件快照的集合构建完成后如果只存在本地那换一台机器就跑不了。单机场景还好一旦涉及多台服务器、多个环境、自动化发布镜像分发就成了绕不开的问题。手动传 tar 包这种方式最大的痛点是不能增量复用。同一个基础镜像比如一个应用基础环境构建了二十个不同服务的镜像每台机器都要重复导入同一份基础层。网络不好的时候传一次能等十分钟传十台机器就是一个小时。而且没有人能准确知道当前生产环境跑的是哪一次构建产物回滚更是靠记忆。引入 Registry 之后镜像的存储和分发就变成标准的“客户端-服务端”模式。构建机把镜像推到仓库部署机再从仓库拉取。所有节点的镜像来源统一版本信息通过 tag 管理回滚就是改一下 tag 的事。这跟代码仓库解决多人协作问题是一个逻辑本地代码需要靠 Git 服务器同步镜像分发也需要一个中心化的存储节点。1.2 Registry 是开放协议不是某个平台的专属功能很多新手会以为“镜像仓库”就是指 Docker 官方提供的那个公共仓库这是最常见的误解。实际上Docker 的镜像分发依靠的是一套开放的 HTTP API 规范只要服务端和客户端都实现了这套协议就能完成镜像的推送和拉取。这套协议的好处是解耦。客户端是 Docker Engine服务端可以是官方提供的轻量镜像仓库实现也可以是云厂商提供的托管仓库服务甚至可以自己用代码实现一个符合协议的服务端。选择私有化部署时最常见的方式就是直接运行一个registry:2容器它内部实现了完整的仓库存储和 API 逻辑。因为协议是开放的迁移成本也很低。今天在自建仓库上存的镜像明天把服务端换成另一套兼容协议的产品客户端配置改个地址就能继续使用。这个特性在团队扩张、机房迁移、多云部署时尤其重要没有人愿意被某个封闭服务锁死。1.3 镜像、仓库、标签、摘要之间的关系要理解 Registry必须先分清四个概念Image镜像、Repository仓库、Tag标签和 Digest摘要。一个镜像是一个可运行的只读模板包含运行应用所需的所有文件、依赖和配置。Repository 是镜像的集合通常来说一个 Repository 对应一个应用或服务。Tag 是 Repository 内某个镜像版本的别名比如1.2.3或者latest。Digest 是通过内容计算出来的 SHA-256 哈希值真正唯一标识镜像的内容。以地址myregistry.example.com:5000/backend/api:1.2.3为例myregistry.example.com:5000是指定的 Registry 服务地址backend/api是 Repository 名1.2.3是 Tag而实际拉取后Docker 会拿到对应内容的摘要类似sha256:abc123...。Tag 可以变化允许用户把latest指向任意版本Digest 不可变只要镜像内容变了摘要必然变。生产环境追求可重复部署时优先用 Digest 固定版本这是我后来踩过回滚坑之后才彻底明白的。1.4 一个 Registry 实例里到底存了什么东西Registry 服务端在磁盘上主要管理两类数据blob 和 manifest。blob 是镜像层的二进制数据也就是实际文件内容manifest 是描述镜像元数据的 JSON 文档记录了镜像包含哪些层、每层的地址、容量、暴露端口、环境变量等信息。在访问接口层面Registry 还提供了按 Repository 组织的目录结构以及查看 tag 列表、检查 blob 是否存在的 API。日常使用者不需要关心内部结构但了解这一点对后面做数据备份和磁盘清理非常有帮助。2. Registry 的关键机制分层存储与推拉流程2.1 镜像层与内容寻址存储Docker 镜像由多层组成每一层本质上是文件系统变更的记录。构建 Dockerfile 时每条指令都会产生一层比如FROM、RUN、COPY。这些层是只读的不能被修改只能被替换。Registry 采用“内容寻址”的存储方式每个 blob 的存储位置由内容的哈希决定内容本身既是标识又是校验手段。这意味着同一个基础镜像层被多个不同镜像引用时仓库里只需要保存一份物理数据。比如服务 A 和服务 B 都基于同一个基础环境构建它们的一千多个镜像层里可能绝大部分都重复分层存储能省下非常可观的磁盘空间。内容寻址还带来了完整性校验。下载任何一层时Docker 客户端都会计算哈希并与摘要比对一旦传输损坏就会重新拉取该层。这比我之前用 tar 包分发可靠得多——tar 包传丢一个字节都没人知道镜像层校验失败会立刻暴露问题。2.2 推送镜像时到底发生了什么执行docker push后客户端并不会傻傻地把整个镜像一次性传到服务端。完整的流程大致是客户端读取本地镜像的 manifest确认需要推送哪些层。客户端向 Registry 发起 HEAD 请求询问每个层是否已经存在。不存在的层通过分片上传接口逐个上传上传过程中会校验每一段的哈希。所有层上传完成后客户端上传 manifest 文件并用该 manifest 更新对应的 Tag 引用。Registry 返回摘要推送结束。这套流程看起来比单纯的文件拷贝复杂但好处非常多。首先是支持断点续传大镜像上传中断后可以接着上次的进度继续不需要从头再来。其次增量上传效率很高一个只改了一行代码的应用镜像可能只需要上传一个新层其余层全部通过“已存在”检查跳过。我在实际使用中最大的感受是推送并不是越快越好重点是完整性和可复用性。曾经为了省时间用直传方式分发镜像结果线上环境出现了基础层不一致导致的诡异问题之后再也不敢绕过标准流程。2.3 拉取镜像时流量如何节省docker pull的流程和推送呼应客户端请求 manifest解析出需要的层列表然后逐个检查本地是否已有相同哈希的层。已存在的层直接复用缺失的层才真正下载。这个设计让多台服务器从同一个仓库拉取相同镜像时基础层只会在首次完整下载后续拉取只传输新增的部分。典型场景是生产环境 20 台节点运行同一个服务第一次滚动发布时流量较大之后每次发布如果只是应用代码更新大部分基础层都能复用网络压力远小于直接传输整个镜像。拉取完成后客户端还会做哈希校验确保内容没有在传输中损坏。如果 Registry 服务器磁盘上的层数据已经损坏校验失败时拉取报错这能帮助运维及时发现存储问题。2.4 固定 Tag 与固定 Digest 的实际差异平时我们习惯用latest或版本号 tag 来拉取镜像这很直观但存在隐患。Tag 是可变的同一个 Tag 在不同时间可能指向不同镜像内容。如果发布流程不严谨生产环境很可能在某个时间点拉到了一个未经过充分验证的“latest”。Digest 则是内容本身的指纹。比如myregistry.example.com:5000/backend/apisha256:abc123...只要这个摘要存在拉取到的内容就是确定的和 Tag 被改成什么无关。我的建议是给用于生产发布的版本同时记录 Digest在部署脚本中使用摘要进行拉取或者在 CI 过程中把 Digest 写入部署清单。Tag 只用于人类可读的版本标识Digest 才是机器可信的唯一依据。3. 从零搭建一个私有 Registry3.1 技术选型为什么直接用官方 Registry 镜像私有镜像仓库的解决方案不少有带 web 管理界面的完整平台也有只提供核心存储的轻量实现。对绝大多数团队来说直接使用官方registry:2镜像足够了。理由很简单它的功能边界非常克制只负责镜像存储和分发没有花哨的管理页面也不带用户中心。这样反而降低了故障面和运维复杂度镜像仓库这种基础设施稳定性远比界面美观重要。需要管理界面时可以后续在 Registry 前面加一层代理或另搭管理工具并不一定要替换底层存储。而且registry:2镜像非常轻量不依赖外部数据库所有数据都写在本地目录。只要把数据卷挂载好迁移就是拷贝文件夹的事。初期用一台低配置服务器就能跑起来没有太大学习成本。3.2 快速启动一条命令跑起来先创建数据目录再启动容器mkdir -p /opt/registry/data docker run -d \ --name registry \ --restartalways \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ registry:2启动后验证服务是否正常curl http://127.0.0.1:5000/v2/这个接口会返回一个空的 JSON 对象{}表示服务可用。如果本机部署的 Docker 想往这个仓库推送镜像因为还没有启用 TLS需要在 Docker daemon 配置中加入insecure-registries否则默认客户端会拒绝明文 HTTP。这是最精简的部署方式适合内网可信环境或临时测试。不要直接用于公网暴露没有认证和加密的 Registry 相当于把磁盘开放给所有能访问该端口的人非常危险。3.3 给 Registry 加上 TLS 加密镜像仓库传输的是业务代码和配置绝不能明文裸奔。为 Registry 配置 TLS 是必要的一步。我这里用自签名证书做演示证书文件需要同时挂载到容器中。先生成证书mkdir -p /opt/registry/certs openssl req -newkey rsa:2048 -nodes -keyout /opt/registry/certs/domain.key \ -x509 -days 365 -out /opt/registry/certs/domain.crt \ -subj /CNmyregistry.example.com \ -addext subjectAltName DNS:myregistry.example.com这里需要注意CN和SAN都要填写实际访问仓库时使用的域名。如果客户端通过 IP 访问还需要加入IP类型的 SAN否则现代 Docker 版本在证书校验时会直接报错。然后重新启动带 TLS 的 Registrydocker run -d \ --name registry \ --restartalways \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/certs:/certs \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/domain.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/domain.key \ registry:2使用 HTTPS 访问验证curl https://myregistry.example.com:5000/v2/这时候客户端会因为不信任自签名证书而报警需要在客户端节点上导入 CA 才能正常使用。3.4 让 Docker 客户端信任自签名证书客户端信任自签 CA 有两种常见方式。第一种是修改 Docker daemon 配置文件/etc/docker/daemon.json加入{ insecure-registries: [myregistry.example.com:5000] }这种方式会跳过该地址的 TLS 证书校验甚至允许明文 HTTP配置最省事。但代价是放弃了证书的安全校验局域网内还好生产环境我不推荐长期使用。第二种是把自签 CA 证书放到 Docker 的证书信任目录mkdir -p /etc/docker/certs.d/myregistry.example.com:5000 cp /opt/registry/certs/domain.crt /etc/docker/certs.d/myregistry.example.com:5000/ca.crtDocker 在访问该仓库时会把这个地址对应的 CA 作为信任锚点证书校验正常生效。这种方式更接近生产实践缺点是每台需要访问仓库的机器都要部署一次证书。如果是大规模集群最好把证书下发和节点初始化流程自动化。无论用哪种方式修改完 daemon 配置后都要重启 Docker 服务才能生效。重启会造成节点容器中断生产环境要做变更评估不要直接在生产节点上手滑。3.5 配置登录认证私有仓库一定要有访问控制否则任何人都能推镜像覆盖生产版本后果很严重。Registry 官方镜像内置了基于 htpasswd 的简单认证。生成认证文件mkdir -p /opt/registry/auth docker run --rm --entrypoint htpasswd registry:2 -Bbn deployuser StrongPass123 /opt/registry/auth/htpasswd-B表示使用 bcrypt 算法加密密码-b表示命令行直接提供密码-n表示直接输出到标准输出。htpasswd 文件挂载进容器后再设置环境变量启用认证docker run -d \ --name registry \ --restartalways \ -p 5000:5000 \ -v /opt/registry/data:/var/lib/registry \ -v /opt/registry/certs:/certs \ -v /opt/registry/auth:/auth \ -e REGISTRY_HTTP_TLS_CERTIFICATE/certs/domain.crt \ -e REGISTRY_HTTP_TLS_KEY/certs/domain.key \ -e REGISTRY_AUTHhtpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALMRegistry Realm \ -e REGISTRY_AUTH_HTPASSWD_PATH/auth/htpasswd \ registry:2客户端使用前先登录docker login myregistry.example.com:5000登录信息会写入 Docker 配置文件之后执行 push 和 pull 就带着认证信息。如果推到了其他仓库需要重新登录对应地址。3.6 使用 Compose 统一管理部署如果不想记一长串docker run参数可以用 Compose 文件组织services: registry: image: registry:2 restart: always ports: - 5000:5000 volumes: - /opt/registry/data:/var/lib/registry - /opt/registry/certs:/certs - /opt/registry/auth:/auth environment: REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswddocker compose up -d启动配置文件纳入版本管理后续修改更方便。这里还要提醒一句Compose 文件里的密码不要硬编码到版本库用环境变量或其它的密钥管理方式。4. 日常维护与数据管理4.1 镜像数据在磁盘上的形态Registry 的数据目录挂在/var/lib/registry内部结构是分目录存放的。在docker/registry/v2下可以看到blobs目录和repositories目录。blobs按哈希前两位拆分存放所有层数据repositories记录每个仓库的 tag 与 manifest 引用关系。日常使用中不需要直接操作这些文件但做备份时要明白整个数据卷是一个整体。不能只备份某个子目录否则容易丢失映索引信息。查看当前挂载点的实际位置可以用docker inspect registry --format {{ json .Mounts }}保管好这个数据卷相当于保管了整个仓库的所有镜像资产。如果磁盘损坏导致数据丢失即使 Dockerfile 还在重新构建所有历史镜像也是一笔不小的成本。4.2 删除镜像不等于释放磁盘空间很多人会习惯性地执行docker exec registry registry garbage-collect /etc/docker/registry/config.yml这句话的意思是启动 Registry 自带的垃圾回收它在 manifest 不再引用的 blob 中进行清理。但要注意先用 API 删除了 manifest 之后blob 并不会马上消失必须主动执行 GC 才会真正清理。GC 执行时不能同时进行 push 操作否则未上传完成的层可能被误认为孤儿数据并删除导致镜像损坏。安全流程是先把 Registry 改成只读维护模式然后执行 GC完成后恢复写权限。把环境变量设置为只读模式-e REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLEDtrue再执行 GC。GC 结束后把该环境变量移除并重启容器。这样做的代价是维护期间不能推送新镜像但比起丢数据风险这个停顿完全值得。另外Registry 的 GC 不会对磁盘空间做普通意义上的“归零”删除后的块可能不会立刻显示为磁盘可用空间操作后观察du才能看到实际变化。如果对 GC 机制不熟悉宁可少删多观察也不要盲目触发。4.3 备份与恢复备份私有镜像仓库最可靠的方式是整体备份数据卷。先停掉写操作docker stop registry然后打包数据目录tar czf registry-backup-$(date %Y%m%d).tar.gz -C /opt/registry/data .恢复时只要把备份内容解压到新环境相同路径再启动容器即可。mkdir -p /opt/registry/data tar xzf registry-backup-20250701.tar.gz -C /opt/registry/data docker start registry恢复完成后用下面的接口检查一下仓库列表是否完整curl -s https://myregistry.example.com:5000/v2/_catalog这个接口会返回所有 Repository 名称如果里面能看到预期列表说明恢复成功。因为我见过有人只备份了 blobs 目录而忽略 repositories恢复后发现仓库列表为空镜像无法拉取。备份频率取决于镜像更新频率。如果每天都有多个版本发布至少保证每日备份并保留 7 份左右的轮转记录。4.4 日志与监控Registry 容器日志默认输出到 stdout可以通过docker logs registry查看。日志里记录了每个请求的 HTTP 方法、路径、状态码和耗时是排查问题的主要入口。当客户端报“unauthorized”或者“manifest invalid”之类的问题一定要回头翻 Registry 日志往往原因就藏在服务端视角。比如认证失败、上传层不完整、权限问题等在日志中都会留下痕迹。还可以用轮转避免日志无限增长。在 Docker daemon 配置中加入以下参数{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 5 } }然后重启 Docker新进程日志就会按大小轮转。没有这个限制一个长期运行的 Registry 可能把磁盘填满挤占镜像存储空间。监控方面至少要看磁盘使用率和请求成功率。磁盘打满后 push 会直接失败非常影响发布流程。建议在磁盘使用率到达 70% 时就开始关注到 85% 时准备清理或扩容。5. 常见问题排查与避坑实录5.1 push 和 pull 报证书、连接超时的排查顺序客户端推送或拉取失败时不要一上来就怀疑镜像本身。按照下面的顺序快速定位第一步确认网络能通curl -v https://myregistry.example.com:5000/v2/如果这一步都不通先查 DNS、防火墙、端口映射别在 Docker 配置里浪费时间。第二步确认证书是否被信任。报错里只要包含x509: certificate signed by unknown authority就是 CA 没有被正确安装到/etc/docker/certs.d/对应地址/ca.crt或者使用的是insecure-registries但没有生效。第三步确认 daemon 配置的修改是否重启。改完daemon.json后必须重启 Docker配置才会被重新读取否则即使文件没错也会继续走旧的校验逻辑。第四步确认命令里写的仓库地址有没有带规范端口。因为如果你写的是myregistry.example.com/backend/api:1.0而没带:5000Docker 会默认把它当作官方公共仓库的地址然后走公网解析自然无法命中私有仓库。5.2 配置 htpasswd 认证后一直 401通常原因是 htpasswd 文件生成方式不对。-B参数必须使用否则默认的密码哈希格式可能在容器内解析失败。另外htpasswd文件里的用户名和密码字段不能有额外空格粘贴配置时要小心格式被复制变形。如果密码本身包含$、!等特殊字符注意 shell 转义问题最好用单引号包裹密码传入命令。生成后可以拿文件内容的第一行看一眼正常应该是用户名:bcrypt中的$2y$...这类格式。还有一点容易被忽略Registry 容器读取 htpasswd 文件的路径必须和环境变量REGISTRY_AUTH_HTPASSWD_PATH一致。我遇到过一次路径挂了/auth/htpasswd但环境变量写成了/etc/htpasswd结果无论密码多正确都是 401。5.3 磁盘空间被镜像层打满先查看实际占用du -sh /opt/registry/data如果发现数据量远超预期可以先列出仓库目录找出哪些 Repository 占用最多du -sh /opt/registry/data/docker/registry/v2/repositories/* | sort -rh | head然后确认确实不再需要的旧镜像通过 Registry API 删除对应 manifest。删除后执行 GC并等待 GC 完成。如果空间仍没有明显回收检查 Registry 日志里是否有 GC 期间冲突的警告。还要注意latest或某类 tag 如果始终指向最新构建历史版本即使 tag 已删可能仍被其它镜像关联GC 不会删除仍然被引用的层。所以在决定删除前先确认没有其它地方还引用着。5.4 数据卷迁移后镜像消失把整个/var/lib/registry目录从旧机器拷贝到新机器启动容器后却发现无法拉取原有镜像往往是没有保留目录结构权限。Registry 容器默认用root运行如果解压后的文件属主不对或者 SELinux、AppArmor 规则阻止了容器读取导致启动时扫描不到已有数据。处理方法是先检查宿主机权限ls -la /opt/registry/data确保目录可读可写必要的时候执行chown -R root:root修正属主。如果使用的是支持 SELinux 的系统还需要给目录设置正确的安全上下文或者临时用:z挂载参数规避权限限制。更重要的是迁移前先做一次功能验证而不是假设文件拷过去就万事大吉。启动后立刻调用_catalog接口检查仓库列表确认无误再接入发布流程。5.5 日志增长过快导致磁盘问题Registry 启动后如果被频繁请求日志增长非常快尤其是在公网直接暴露、被扫描器反复探测的情况下。我见过一台测试服务器因为没配日志轮转json-file日志涨到几十 GB最终把镜像数据卷的磁盘挤爆。解决方法是提前配置 Docker daemon 的 log 参数同时给 Registry 端口设置防火墙白名单只允许已知构建机和部署节点访问。如果已经出现大日志文件可以先清空日志再重启容器truncate -s 0 /var/lib/docker/containers/*/*-json.log然后再确认日志配置已经生效不让日志问题反复出现。如果你团队里还没人专职维护这一套至少先把日志轮转和定期备份跑起来否则哪天磁盘被打满的时候你会在半夜爬起来对着屏幕发呆。