Docker Registry 官方参考实现,自建最简私有镜像仓库的选择
registry 指的是 distribution/distribution 项目 —— 也就是当年 docker push 背后那个官方 registry 的开源实现。它只做一件事:按照 Docker Registry HTTP API V2 存取镜像。
它的能力边界必须一开始就说清楚:
有 镜像的 pull / push / delete、标签列举、对象存储后端、TLS、基于 htpasswd 的认证
没有 Web 界面、多租户、RBAC、镜像扫描、签名、跨仓库复制、配额
定位因此很明确:单团队、内网、轻量的镜像存放点。需要界面、权限体系或扫描能力时应改用 harbor 这样的上层平台 —— Harbor 内部的 registry 组件其实就是 distribution。
在 Kubernetes 场景里,registry 可以作为集群的私有镜像仓库:节点上的 kubelet 通过 CRI 让 containerd 从这里拉镜像,push 端通常是 CI 或开发者本机。最需要注意的是:registry 不解决「谁有权限推什么」,它把鉴权完全交给你在反向代理或 htpasswd 层面处理。
distribution 2.8.x 长期使用的稳定线,镜像标签 registry:2
distribution 3.0.0 2025 年发布的第一个 3.x 正式版,镜像标签 registry:3
3.x 的主要变化:
默认配置路径改为 /etc/distribution/config.yml(2.x 是 /etc/docker/registry/config.yml)
移除 oss 与 swift 存储驱动
签名库由 docker/libtrust 换成 go-jose/go-jose
这是升级时最容易踩的坑:照搬 2.x 的 garbage-collect /etc/docker/registry/config.yml 到 3.x 镜像上,会因为配置路径变了而报找不到文件。
最快的启动方式:
docker run -d -p 5000:5000 --restart=always --name registry \
-v /mnt/registry:/var/lib/registry \
registry:3
# 验证
curl -k https://registry.example.com:5000/v2/_catalog
在 Kubernetes 上部署则需要持久化存储与配置文件:
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry
namespace: registry
spec:
replicas: 1
selector:
matchLabels:
app: registry
template:
metadata:
labels:
app: registry
spec:
containers:
- name: registry
image: registry:3
ports:
- containerPort: 5000
volumeMounts:
- name: data
mountPath: /var/lib/registry
- name: config
mountPath: /etc/distribution/config.yml
subPath: config.yml
volumes:
- name: data
persistentVolumeClaim:
claimName: registry-data
- name: config
configMap:
name: registry-config
配置文件通过 ConfigMap 挂到 /etc/distribution/config.yml(3.x 的路径,2.x 是 /etc/docker/registry/config.yml),并用 REGISTRY_STORAGE_DELETE_ENABLED=true 打开删除。replicas 只能为 1,除非把存储换成 S3 之类的对象存储 —— 本地文件系统后端不支持多副本共享。
version: 0.1
storage:
filesystem:
rootdirectory: /var/lib/registry
delete:
enabled: true
maintenance:
readonly:
enabled: false
http:
addr: :5000
常用配置项:
storage.filesystem.rootdirectory 镜像数据目录
storage.delete.enabled 是否允许删除(默认 false,必须显式开启)
storage.maintenance.readonly.enabled 只读模式,GC 期间用
storage.cache.blobdescriptor 缓存后端,可设为 inmemory / redis
http.addr 监听地址
http.tls.certificate / key TLS 证书路径
auth.htpasswd.realm / path 基础认证
proxy.remoteurl 配置后变为拉取代理(pull-through cache)
配置项可以用环境变量覆盖,规则是把配置路径转成大写、用下划线连接:
storage.delete.enabled → REGISTRY_STORAGE_DELETE_ENABLED=true
http.addr → REGISTRY_HTTP_ADDR=:5000
docker 与 containerd 默认只走 HTTPS,所以给 registry 配证书是让它真正可用的前提。不配证书直接 docker push 会得到:
Error response from daemon: Get "https://registry.example.com:5000/v2/":
http: server gave HTTP response to HTTPS client
三种处理方式:
1. 配证书(推荐)
http:
tls:
certificate: /certs/domain.crt
key: /certs/domain.key
2. docker 侧放行不安全仓库 /etc/docker/daemon.json(仅测试环境)
{ "insecure-registries": ["registry.example.com:5000"] }
3. containerd 侧写成 http(仅测试环境)
/etc/containerd/certs.d/registry.example.com:5000/hosts.toml
server = "http://registry.example.com:5000"
registry 只内置了 htpasswd 一种认证方式,先用 bcrypt 生成密码文件:
htpasswd -Bbn admin '******' > /auth/htpasswd
auth:
htpasswd:
realm: basic-realm
path: /auth/htpasswd
# 启动时把密码文件挂进去
docker run -d -p 5000:5000 \
-v /mnt/registry:/var/lib/registry -v /mnt/auth:/auth \
-e REGISTRY_AUTH=htpasswd \
-e REGISTRY_AUTH_HTPASSWD_REALM=basic-realm \
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \
registry:3
htpasswd 只能区分「能登录」和「不能登录」,无法做读写分离或按仓库授权。要让 CI 只能推、节点只能拉,得在 registry 前面放一个反向代理自己做鉴权。
registry 默认不允许删除任何东西,storage.delete.enabled 必须显式打开;而删除与释放磁盘又是两个独立的步骤,只做前一步空间不会变:
# 第一步:删除 manifest,让它变成「未被引用」
# 必须按 digest 删,不能按 tag 删
curl -u admin:****** -X DELETE \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://registry.example.com:5000/v2/library/nginx/manifests/sha256:xxxxx
# 或者用 skopeo(它会先解析出 digest 再删)
skopeo delete --creds admin:****** docker://registry.example.com:5000/library/nginx:1.27
# 第二步:垃圾回收,真正释放磁盘
bin/registry garbage-collect [--dry-run] [--delete-untagged] [--quiet] /etc/distribution/config.yml
# 容器里的常见形态:先演练,确认清单无误后正式执行
docker exec registry /bin/registry garbage-collect \
--dry-run --delete-untagged /etc/distribution/config.yml
docker exec registry /bin/registry garbage-collect \
--delete-untagged /etc/distribution/config.yml
# 执行完重启一次 registry(原因见下方注意事项)
docker restart registry
--delete-untagged 决定是否清理「没有标签指向的 manifest」。不带这个参数时,docker push 覆盖过的旧版本会永远留在磁盘上 —— 这是 registry 磁盘只增不减的最主要原因。
# 1. 节点侧信任自签 CA
sudo mkdir -p /etc/containerd/certs.d/registry.example.com:5000
sudo cp ca.crt /etc/containerd/certs.d/registry.example.com:5000/ca.crt
# 2. Pod 里引用凭据,镜像地址要带端口
kubectl create secret docker-registry regcred \
--docker-server=registry.example.com:5000 \
--docker-username=admin --docker-password='******'
# /etc/containerd/certs.d/registry.example.com:5000/hosts.toml
server = "https://registry.example.com:5000"
[host."https://registry.example.com:5000"]
capabilities = ["pull", "resolve"]
ca = "/etc/containerd/certs.d/registry.example.com:5000/ca.crt"
注意 registry 的镜像路径是两段式(<仓库名>/<镜像名>),没有 Harbor 那样的「项目」层。
registry 可以配置成上游仓库的缓存,在没有公网出口的集群里很有用:
proxy:
remoteurl: https://registry-1.docker.io
ttl: 168h
配置后所有镜像都从同一个地址拉,靠前缀区分上游:registry.example.com:5000/library/nginx:1.27 实际来自 Docker Hub 的 library/nginx。好处是不必维护镜像同步清单,代价是上游不可达时它会直接失败。
storage.delete.enabled 不开,任何 DELETE 请求都会被拒绝或静默忽略。这是「删了镜像磁盘却没变」的第一号原因。garbage-collect 的清除阶段才真正删文件。两步缺一不可。--delete-untagged 的 GC 清不掉多少东西。被 docker push 覆盖过的旧 tag 会留下没有标签引用的 manifest,官方文档明确说明该参数用于「删除当前没有被任何标签引用的 manifest」。不开启时磁盘会持续增长。storage.maintenance.readonly.enabled: true。storage.cache.blobdescriptor: inmemory 时,GC 删掉了磁盘上的 blob 但内存里的描述符还在;此时重新推送同一个镜像会「看起来成功」(registry 拿缓存里的层直接应答),实际没有写入任何层,之后拉取报 manifest unknown。重启进程即可规避。/etc/docker/registry/config.yml 改成 /etc/distribution/config.yml。照着旧文档在 registry:3 镜像里跑 GC 会报找不到文件而不是报参数错误,很容易误判成别的问题。storage.cache.blobdescriptor: redis 与删除一起用时需要留意。已有安全公告指出,这种组合下删除会清掉共享的 digest 描述符却留下仓库维度的残留记录,后续从其他仓库 Stat 时可能让已删除的 blob 重新变为可读。不需要跨实例共享缓存时,保持 inmemory 更简单。http: server gave HTTP response to HTTPS client —— 它说的是「服务器对 HTTPS 请求回了 HTTP」,根因是客户端在说 HTTPS 而服务端只会说 HTTP。insecure-registries 只是测试环境的绕行方案。/etc/containerd/certs.d/<host>/ 下的证书目录,不是系统信任库,update-ca-certificates 通常不解决问题;而且主机名必须与证书 SAN 完全一致,用 IP 访问一定会失败。replicas 大于 1 时多个 Pod 各写自己的卷,镜像会时有时无。要多副本必须换对象存储后端。/v2/_catalog 默认分页,一次只返回一部分仓库名。请自行写定时任务做「删除旧 tag + GC」,并监控存储使用率。ImagePullBackOff;清理前请确认没有 Pod 依赖这些 tag。harbor — 在 registry 之上补齐权限、扫描与复制的平台skopeo — 跨仓库搬运与删除镜像containerd — 节点侧执行拉取的运行时docker — 容器管理工具kubelet — 通过 CRI 触发镜像拉取