ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

自建Docker Registry实战:从部署到CI/CD接入与生产排错

自建Docker Registry实战:从部署到CI/CD接入与生产排错 先交代个背景我在生产环境里管过好几套镜像分发系统从团队内部几个人用小仓库到对接 CI 流水线每天跑几百次构建再到跨机房同步镜像这套东西折腾了不少。如果你也在考虑把 Docker Registry 私有化或者公司已经在用 Docker Hub 但拉取老被限流、镜像安全没法管控那这篇文章应该能帮你省不少弯路。这里说的 Docker Registry本质上是 Docker Distribution 这个开源项目编译出来的服务端程序也就是registry:2这个官方镜像。它解决的核心问题很简单给你一个完全自控的镜像存储和分发端点推镜像、拉镜像、管理镜像版本全都走你说了算的基础设施。适合谁看正在搭 CI/CD 的运维需要在内网交付应用镜像的开发以及想彻底摆脱 Docker Hub 限制的个人开发者。下面从思路到踩坑一条龙讲清楚。1. 为什么自建 Registry以及 Docker Distribution 的定位逻辑1.1 直接使用 Docker Hub 的痛点很多人最初觉得没必要自建Docker Hub 免费账户够用了。但真正跑起来会撞到几面墙匿名拉取有 IP 限流免费账户对私有仓库数量有严格限制而企业里只要涉及商业项目镜像就不可能全公开。更麻烦的是内网服务器如果和外网隔离每次构建都要走代理拉基础镜像速度不稳定一旦 Docker Hub 故障整个发布链路直接卡死。自建 Registry 之后镜像的拉取流量完全走内网构建速度提升是立竿见影的。同时你还能自己控制镜像保留策略比如开发环境保留最近 30 天生产环境镜像永久保留这在合规审计场景下很重要。另外镜像的存储位置在自己手里不依赖第三方供应链安全也更有底。1.2 Docker Distribution 与 Harbor、云厂商 Registry 的关系很多人会把 Docker Distribution 和 Harbor 搞混。简单说Harbor 是在 Docker Distribution 之上加了一层 Web 管理界面、项目管理、权限控制、漏洞扫描、镜像复制等企业功能的前端平台它的底层存储和分发引擎仍然是 Docker Distribution。如果团队规模不大或者你只需要一个“能推能拉、带简单认证”的镜像仓库裸的 Docker Distribution 完全够用部署就一个容器配置三五行维护成本极低。但如果你们有多个团队、需要按项目隔离权限、要有审计日志和图形化界面那再考虑 Harbor 也不迟。我的习惯是先跑通 Distribution把镜像流转的流程理顺再评估是否需要上 Harbor。1.3 选型时的几个决定性因素选 Docker Distribution 有几点很实际的好处官方维护和 Docker Engine 的兼容性最稳新版本特性跟得及时。单二进制文件依赖极少甚至可以不用容器方式直接跑在裸机上。配置走 YAML 文件支持环境变量覆盖很适合容器化的部署方式。存储后端可插拔本地磁盘、S3、MinIO、Azure Blob 都支持将来量大了很容易迁移。社区生态成熟各种 CI/CD 工具都原生支持推送镜像到自定义 Registry。说到底它就是个标准的后端服务。你不需要在这个环节引入太重的东西先把管道打通后续需要什么再往外面包一层。2. 搭建前的关键决策版本、存储与安全模型2.1 版本选择的注意事项Docker Registry 的镜像 tag 现在有点乱老的registry:2和近年新发布的registry:3有不少差异。简单说大部分存量部署和教程都在用registry:2它稳定、特性齐全是生产环境的主流选择。registry:3换了一些新的依赖和默认配置但生态过渡还没完全完成。如果你是新项目我建议直接用registry:2的最新 patch 版本比如registry:2.8.3。不要用latesttag因为你不知道什么时候官方就会更新大版本导致行为不一致。也别急着用registry:3除非你明确知道自己的场景需要它带来的新特性。2.2 存储驱动选型Docker Distribution 的存储驱动是storage.driver默认是filesystem也就是直接把镜像数据写到宿主机的某个目录下。这个最简单也最稳单机部署首选。如果你的集群有共享存储需求可以用 NFS但要注意 NFS 的锁和并发写问题别几个节点同时写同一个 Registry 的数据目录很容易出现 blob 损坏。如果将来要上对象存储S3 和 MinIO 都是比较顺滑的选择。改成 S3 驱动之后镜像分块数据会全部落到对象存储里本机不再保留数据。这样做的好处是扩容方便坏盘不丢数据但代价是每次拉镜像都要走网络内网延迟不高的情况下效果也不错。我个人的建议是单机或者双机场景用 filesystem以最少的组件把系统跑起来优先保证可用性。等镜像总量超过几百 GB、或者节点数多了再切换到 S3/MinIO。2.3 为什么必须上 HTTPS 和认证Docker 客户端有一个很硬的规则默认情况下它只会和 HTTPS 的 Registry 通信除非你在 daemon 配置里显式把某个地址加进insecure-registries。更关键的是即使你加了 insecure-registries生产环境也不应该裸奔。内网里如果镜像仓库不需要密码就能推拉意味着任何拿到内网访问权限的人都可以往你的仓库里塞恶意镜像这比代码泄露还危险。推荐做两件事第一给 Registry 配 HTTPS最省事的方式是走 Nginx 反向代理终结 TLS第二在 Registry 前面加 HTTP Basic 认证至少做到“知道账号密码才能推拉镜像”。这两件事都不复杂但能挡住 90% 的随手风险。2.4 端口、目录与命名规划Registry 默认监听5000端口你也可以改成别的比如8443或1443看你的网络策略。但大多数场景下5000 已经约定俗成了CI/CD 配置和镜像 tag 写起来也顺眼。镜像路径的规则也要提前想好尤其是将来有多个团队共用仓库的时候。我推荐命名空间用两层结构registry.example.com/team-a/service-name:v1.0.0。team-a是项目组或产品线service-name是具体服务名版本用语义化版本号。这样在 Web 界面如果接 Harbor或者命令行ls仓库列表的时候层级一目了然。3. 部署 Docker Distribution 的完整实操3.1 最常规官方镜像加 Docker Compose 启动先看一个能直接抄的 Compose 配置。假设域名是registry.example.com证书放在/etc/letsencrypt/live/registry.example.com/下认证文件用 htpasswd 生成。version: 3.8 services: registry: image: registry:2.8.3 container_name: registry restart: always ports: - 5000:5000 environment: REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_REALM: RegistryRealm REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /var/lib/registry REGISTRY_HTTP_TLS_CERTIFICATE: /certs/fullchain.pem REGISTRY_HTTP_TLS_KEY: /certs/privkey.pem volumes: - /data/registry:/var/lib/registry - /etc/letsencrypt/live/registry.example.com:/certs:ro - /data/registry-auth:/auth:ro启动之前先生成认证文件。用官方镜像里的 htpasswd 工具最省事mkdir -p /data/registry-auth docker run --rm --entrypoint htpasswd httpd:2.4 -Bbn admin your-strong-password /data/registry-auth/htpasswd我用的镜像是httpd:2.4因为它的 htpasswd 支持-B参数生成 bcrypt 加密的密码串安全性比默认的 MD5 好得多。-b是命令行直接传密码-n是输出到标准输出。生成完记得确认一下文件内容不是明文密码。然后启动docker compose up -d打一个简单镜像推过去验证docker pull nginx:alpine docker tag nginx:alpine registry.example.com/team-a/nginx-test:v1 docker login registry.example.com docker push registry.example.com/team-a/nginx-test:v1如果一切正常push 成功之后用curl -u admin:密码 https://registry.example.com/v2/_catalog能返回镜像仓库列表。3.2 用 Nginx 做 TLS 终结和代理转发上面方案是直接把证书挂在 Registry 进程上还有一种更灵活的方式Registry 只监听内网端口由 Nginx 统一处理 HTTPS、日志、限流甚至 WAF 规则。这在多服务共用一台机器时尤其有用。Nginx 配置核心思路是upstream registry_backend { server 127.0.0.1:5000; } server { listen 443 ssl; server_name registry.example.com; ssl_certificate /etc/letsencrypt/live/registry.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/registry.example.com/privkey.pem; client_max_body_size 0; location / { proxy_pass http://registry_backend; 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因为镜像分块上传时单个请求可能很大Nginx 默认 1MB 会直接把上传拦下来。反向代理模式下Registry 自身的 TLS 可以关掉直接把裸端口 5000 暴露给 Nginx 即可。3.3 底层原理Registry 镜像仓库的 HTTP API 流程理解了 Registry 的 API 流程排错会轻松很多。Docker 推镜像时客户端会先向 Registry 发起一个POST /v2/name/blobs/uploads/请求获取上传链接然后分块 PUT 数据最后 PUT 一个?digestsha256:...确认完成。拉镜像时先通过/v2/name/manifests/reference拿到 manifest 文件里面记录着每一层的 digest然后客户端再按 digest 逐个 GET 拉取 blob。这意味着 Registry 本身不需要知道“镜像”这个抽象概念它只认 blobs 和 manifests。blobs 是以 digest 为名的内容寻址文件manifests 指向一组 blobs 的清单。同一个层如果被多个镜像引用底层只存一份不会重复占用磁盘。有点像一个图书馆书blob按唯一编号digest存着目录卡manifest记录每本书放在哪个书架。多本目录卡可以共享同一本书所以镜像构建时复用基础层特别快。3.4 裸机部署的备选方案如果你所在环境不允许跑容器下载 Docker Distribution 的二进制文件直接运行也可以。去 GitHub Releases 页面下载registry_2.8.3_linux_amd64.tar.gz解压后用命令行参数或写一个 YAML 配置启动。version: 0.1 log: fields: service: registry storage: filesystem: rootdirectory: /var/lib/registry http: addr: 0.0.0.0:5000 tls: certificate: /certs/fullchain.pem key: /certs/privkey.pem auth: htpasswd: realm: RegistryRealm path: /auth/htpasswd启动命令registry serve /etc/registry/config.yml这种方式适合特殊环境比如某些受控的物理机节点不允许起容器。日常维护上和容器化部署差别不大只是升级时需要手动替换二进制文件。4. 镜像仓库日常使用与 CI/CD 接入要点4.1 管理镜像标签与仓库列表Registry 自身没有提供删除镜像的 Web 按钮所以日常管理基本靠命令行和 API。先看几个实用命令# 查看仓库列表 curl -u admin:密码 https://registry.example.com/v2/_catalog # 查看某个仓库的所有标签 curl -u admin:密码 https://registry.example.com/v2/team-a/nginx-test/tags/list # 获取 manifest 信息注意 Accept 头 curl -u admin:密码 \ -H Accept: application/vnd.docker.distribution.manifest.v2json \ https://registry.example.com/v2/team-a/nginx-test/manifests/v1注意_catalog接口在镜像数量很多时会很慢官方也建议生产环境尽量少用。真要管理大批量镜像直接操作 Registry 的数据目录或者用脚本遍历 blob。但脚本操作有一定风险不熟悉底层结构之前别乱删。4.2 修改 Docker daemon 配置以便内网拉取如果 Registry 没配 HTTPS那么每个要拉镜像的机器都得在/etc/docker/daemon.json里加上{ insecure-registries: [registry.example.com:5000] }改完必须重启 Dockersystemctl restart docker这一步是初学者最容易漏的。很多人配好 Registry在服务器本机 push 成功换一台机器 pull 就报http: server gave HTTP response to HTTPS client原因就是这台机器的 Docker daemon 不知道这个地址允许走 HTTP。注意insecure-registries是 daemon 级别的配置不是客户端环境变量改完一定要重启不是 reload。如果 Registry 已经配好 HTTPS这一步可以完全省略。所以我的建议永远是优先配 HTTPS省掉每台机器都要改配置的麻烦。4.3 对接 CI/CD从 Jenkins 到 GitLab CICI 流水线推送镜像时一个高频问题是密码管理。不要直接在 pipeline 脚本里写明文密码至少用 CI 系统的 secret 变量。以 GitLab CI 为例docker-push: stage: deploy script: - docker login -u $REGISTRY_USER -p $REGISTRY_PASS registry.example.com - docker build -t registry.example.com/team-a/my-app:$CI_COMMIT_SHORT_SHA . - docker push registry.example.com/team-a/my-app:$CI_COMMIT_SHORT_SHA很多 CI runner 默认是 Docker 容器内执行 docker 命令这时候需要用到 Docker-in-Docker 或者挂载宿主机的 docker.sock。如果遇到Cannot connect to the Docker daemon先确认 runner 的工作模式。还建议在 CI 里单独开一个“镜像清理”的任务定期删除构建产生的临时 tag防止 Registry 磁盘被 CI 流量撑爆。4.4 迁移与回填把现有镜像批量导入私有仓库存量镜像回填是切换私有仓库时最大的体力活。如果是内网已有的镜像文件可以用docker save和docker load来迁移但更常见的场景是把 Docker Hub 上的公共镜像同步一份到私有仓库。我常用的做法是写个脚本拉取再重新打 tag#!/bin/bash images( nginx:1.25 redis:7.2 mysql:8.0 ) for img in ${images[]}; do docker pull $img docker tag $img registry.example.com/library/$img docker push registry.example.com/library/$img done注意 tag 里的命名空间一般建议公共基础镜像统一放到library命名空间下和 Docker Hub 的library结构保持一致。这样后续 Dockerfile 里的FROM只要替换前缀即可改动最小。4.5 反向代理下的大文件超时问题用 Nginx 或负载均衡器反代 Registry 时除了client_max_body_size 0还得设置合理的超时时间。比如上传一个大的基础镜像可能持续几分钟如果 Nginx 默认 60 秒超时中间断掉就相当于整个上传失败。proxy_connect_timeout 60s; proxy_read_timeout 300s; proxy_send_timeout 300s;实测下来300 秒对于大多数场景够用但如果你有超大镜像可以调到 600 秒。这个值不是越大越好过大反而会让故障响应变慢。5. 生产环境常见问题与排查技巧实录5.1 镜像推送失败和证书相关的坑最经典的问题是证书不可信。自签名证书如果没被客户端信任报错是x509: certificate signed by unknown authority解决办法有两个一是把自签名 CA 证书加到每台机器的系统信任库二是让 Registry 直接用受信任的公共 CA 签发的证书Lets Encrypt、云厂商证书等。企业内网如果没有公共域名可以考虑搭建内部 CA然后统一推送到所有服务器。这个动作要在 Docker daemon 重启之前做完否则 Docker 客户端不认新证书。还有 Windows 和 macOS 上 Docker Desktop 的证书信任路径和 Linux 不一样需要去 Docker Desktop 的设置里导入 CA 证书否则在开发机上 push 总会失败。5.2 认证失败和权限控制问题如果 push 时报denied: requested access to the resource is denied先看是不是没登录或者登录的账号没有权限。docker login成功后凭证默认存放在~/.docker/config.json如果你在 CI 环境里切换账号失败检查一下这个文件里的 auths 字段是不是被之前的凭证覆盖了。htpasswd 文件里密码更新后需要重启 Registry 容器才生效这是 htpasswd 认证的机制决定的。写脚本定时更新密码时别忘了 reload 或 recreate 容器。5.3 磁盘空间与镜像清理策略Registry 的垃圾回收机制和其他系统不太一样。默认情况下即使你用 API 删除了某个镜像的 manifest底层数据也不会立刻消失必须要手动触发 GC 才能释放空间。手工清理流程是这样的# 假设容器名是 registry docker exec registry registry garbage-collect /etc/docker/registry/config.yml --dry-run先加--dry-run看会删哪些内容确认没问题后再去掉 dry-run 执行。这里有个大坑GC 过程中不能同时有新镜像写入否则可能导致正在推送的 blob 被误删。所以生产环境的 GC 最好放在业务低峰期并且先把服务切到只读模式。# 在 compose 文件或启动参数中加入只读配置 # REGISTRY_STORAGE_MAINTENANCE_READONLY_ENABLEDtrue日常运维上更建议写一个定期任务统计 Registry 数据目录的总大小一旦超过阈值就告警。别指望 GC 能帮你解决磁盘满了的问题最好的策略是“按需保留 定期备份清理”比如开发环境的镜像只保留最近 30 天生产环境镜像全量保留。5.4 一个小众但高价值的技巧镜像仓库的备份恢复很多人以为 Registry 的数据目录直接拷贝就能备份其实不能完全这么干。因为 Registry 在写入时为了保证一致性blobs 和 manifests 的写入顺序很重要如果直接cp正在运行中的容器目录大概率会留下不完整的文件轻则镜像 push 失败重则数据损坏。推荐做法先停容器再打包数据目录或者用docker exec registry registry gc之类的方式先达到一致性状态但最稳妥还是停容器备份。docker stop registry tar -czf registry-backup-$(date %F).tar.gz /data/registry docker start registry恢复时同理先停容器解压备份到原目录再启动。这个操作简单但前提是你真的记得备份而且定期演练恢复流程。我有一次就是备份了半年没试过恢复真到要恢复的时候发现 tar 包是坏的从此每季度至少演练一次全量恢复。5.5 跨机房同步与高可用方向如果业务扩展到多机房单节点 Registry 就不够看了。Docker Distribution 官方支持registry proxy角色的实例可以做 Pull Through Cache让边缘机房只缓存经常用到的镜像回源到中心 Registry。配一个 cache 实例非常轻量proxy: remoteurl: https://registry.example.com不过要注意pull-through cache 只对“读”做缓存不解决跨机房推送的问题。跨机房推送镜像更推荐的做法是做成双活架构用对象存储作为共享后端这样两个机房的 Registry 实例指向同一个 S3/MinIO天然数据一致。但前提是机房之间的网络延迟别太高否则拉取镜像的网络开销会成倍放大。还有一种思路是保留单机主节点配合定时任务把关键镜像从主节点同步到备机房备机房只提供读服务。这属于非常朴素的方案但在网络隔离严格的环境下反而是最可靠的。写在最后的一个经验我见过太多团队把 Registry 搭起来就不管了直到磁盘满、证书过期、CI 突然推不上去才开始救火。这套东西单拆出来每一项都是小问题但合在一起就是一个“看似简单实际需要持续运维”的关键组件。我的建议是如果公司把 Docker 镜像作为核心交付物那至少把部署、备份、监控、GC 四件事一次性都做了后面你会感谢自己现在的决定。
返回列表