[TOC]
描述:本来我想直接写Harbor的docker镜像仓库搭建配置与使用,但是觉得还是应该从基础的Docker的Registry镜像讲起从安全构建到GC回收同时加深学习映像;
官网介绍:Registry是一个无状态的,高度可扩展的服务器端应用程序,存储,让你分发图像。它是开源的根据许可Apache许可证。
此时就不在详细讲解Registry的介绍了,有兴趣的朋友可以参见官网文档或者前面我笔记中的介绍; 以下是一些知识点复习:
(1) Registry是一个几种存放image并对外提供上传下载以及一系列API的服务,Registry与镜像的关系可以想象成自己机器上的源码和远端SVN或者Git服务的关系,可以很容易和本地源代码以及远端Git服务的关系相对应。(2) Registry开源常用于构建私有镜像仓库;Q:为什么不直接采用Docker官网Hub作为存储镜像的地方?
答:我认为主要是以下几个方面的影响 1.存储空间有限 2.上传/拉取速度有限 3.企业内部敏感开发项目(如果是您肯定不会上传到别人的服务器中) 4.免费开源
反之使用Registey好处:
镜像存储位置由您掌握全面管理控制自己的镜像镜像存储和分配紧密集成到您的内部开发流程Registry 版本说明:
Docker Registry 1.0版本(hub/docker.io等公共的镜像仓库还支持,安全性以及兼容性不如V2.0)Docker Registry 2.0版本在安全性和性能上做了诸多优化,并重新设计了镜像的存储的格式;(Docker目前1.6之后支持V2)名词解释:
1.repository name(存储库名词) 存储库指在库中存储的镜像。/project/redis:latest
经典存储库名称由2级路径构成,每级路径小于30个字符,V2的api不强制要求这样的格式。每级路径名至少有一个小写字母或者数字,使用句号,破折号和下划线分割。更严格来说,它必须符合正则表达式:[a-z0-9]+[._-][a-z0-9]+)多级路径用/分隔存储库名称总长度(包括/)不能超过256个字符2.digest(摘要) 摘要是镜像每个层的唯一标示。虽然算法允许使用任意算法,但是为了兼容性应该使用sha256。
例如 - sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b # 用一个简单的例子,在伪代码来演示摘要计算 let C = 'a small string' let B = sha256(C) let D = 'sha256:' + EncodeHex(B) let ID(C) = D # python伪代码 import hashlib C = 'a small string' B = hashlib.sha256(C) D = 'sha256:' + B.hexdigest()
测试环境:
# OS CentOS Linux release 7.8.2003 (Core) # Linux Server Version: 19.03.9 Storage Driver: overlay2
基础实例:
# 1.Start your registry docker run -d -p 5000:5000 --restart=always --name registry registry:2 # 2.拉取镜像并修改 docker pull ubuntu docker run --name ubuntu-test -d ubuntu docker commit -a "Weiyigeek" -m "镜像描述" ubuntu-test ubuntu-custom:1.0 # 3.Tag the image so that it points to your registry docker image tag ubuntu-custom:1.0 127.0.0.1:5000/ubuntu-custom:1.0 # 4.从镜像仓储上传与下载镜像 docker push localhost:5000/ubuntu-custom:1.0 docker pull localhost:5000/ubuntu-custom:1.0 # 5.删除本地/远程的ubuntu-custom:1.0 docker image remove ubuntu-custom:1.0 docker image remove localhost:5000/ubuntu-custom:1.0 # 6.registry仓库删除与清除卷; docker container stop registry && docker container rm -v registry
Registry 镜像环境变量:
# Environment variable # 自定义Registry仓库镜像存放的物理地址 REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY=/var/lib/registry # 启用DELETE操作否则不能执行 -X DELETE REGISTRY_STORAGE_DELETE_ENABLED=true # 绑定Registry地址与端口 REGISTRY_HTTP_ADDR=0.0.0.0:5000 # 证书Certificate设置 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/htpasswd
指定环境变量运行Registry仓库:
# (1) docker run 时指定环境变量 $ docker run -d --restart=always --name registry -v "$(pwd)"/auth:/auth -v /opt/docker/registry:/var/lib/registry -v "$(pwd)"/certs:/certs -e REGISTRY_HTTP_ADDR=0.0.0.0:443 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key -e REGISTRY_STORAGE_DELETE_ENABLED=true -p 443:443 registry:2
# (2) Yaml 配置 docker run -d -p 5000:5000 --restart=always --name registry -v `pwd`/config.yml:/etc/docker/registry/config.yml registry:2 # 例如:开发配置的config.yaml version: 0.1 log: level: debug storage: filesystem: rootdirectory: /var/lib/registry http: addr: localhost:5000 host: https://registry.weiyigeek.top:5000 secret: asecretforlocaldevelopment debug: addr: localhost:5001 auth: htpasswd: realm: basic-realm path: /path/to/htpasswd
参考地址:https://github.com/docker/distribution/blob/master/cmd/registry/config-example.yml
# (3) docker-compose 构建镜像仓库 # docker-compose.yml registry: restart: always image: registry:2 ports: - 5000:5000 environment: REGISTRY_HTTP_TLS_CERTIFICATE: /certs/domain.crt REGISTRY_HTTP_TLS_KEY: /certs/domain.key REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_PATH: /auth/htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm REGISTRY_STORAGE_DELETE_ENABLED=true volumes: - /opt/docker/registry:/var/lib/registry - /opt/docker/certs:/certs - /opt/docker/auth:/auth
描述:此处以实际生产环境为例进行Docker Registry私有仓库搭建;
3.1) 服务器拉取Docker Registry镜像并创建Autho认证的.htpasswd文件;
$ docker pull registry:2 # Digest: sha256:8be26f81ffea54106bae012c6f349df70f4d5e7e2ec01b143c46e2c03b9e551d # Status: Downloaded newer image for registry:2 # docker.io/library/registry:2 $ mkdir -vp /opt/registry/ && cd $_ htpasswd -Bbn weiyigeek 123456 # -n 不更新文件输入到终端标准输出 htpasswd -bB -c auth.htpasswd weiyigeek 123456 # -c 创建存储认证字符串,-b 强制加密密码,-B 在命令行中接收密码
Tips:在Push或者Delete镜像是通过HTTP请求Registry的API完成的,每个请求都需要一个Token才能完成操作,而此Token需要使用auth文件(明文用户/密码编码)来进行鉴权;
3.2) Docker Registry 自签证书 描述:如果Registry仓库使用TLS认证时必须带有证书,当外部访问该Registry仓库时候提供安全通道,我们可以在认证机构购买签名证书或者自签证书也可以; 使用 OpenSSL 来生成 CA (证书授权中心,certificate authority)、 中级 CA(intermediate CA) 和末端证书(end certificate)。包括 OCSP、CRL 和 CA颁发者Issuer信息、具体颁发和失效日期。
# 方式1:交互式证书生成 $openssl req -newkey rsa:4096 -nodes -sha256 -keyout ./certs/domain.key -x509 -days 365 -out ./certs/domain.crt # 方式2:配置文件方式生成 cat >ca.conf <<EOF [ req ] default_bits = 2048 distinguished_name = req_distinguished_name prompt = no encrypt_key = no x509_extensions = v3_ca [ req_distinguished_name ] CN = localhost [ CA_default ] copy_extensions = copy [ alternate_names ] DNS.2=localhost [ v3_ca ] subjectAltName=@alternate_names subjectKeyIdentifier=hash authorityKeyIdentifier=keyid:always,issuer:always basicConstraints = critical,CA:true keyUsage=keyCertSign,cRLSign,digitalSignature,keyEncipherment,nonRepudiation EOF openssl req -days 365 -x509 -config ca.conf -new -keyout certs/domain.key -out certs/domain.crt
3.3) 挂载安全认证与自签证书并启动registry容器
docker run -d -p 0.0.0.0:8443:5000 --name registry-net -v /var/lib/registry-net:/var/lib/registry -v /opt/registry/certs:/certs -v /opt/registry/auth.htpasswd:/etc/docker/registry/auth.htpasswd -e REGISTRY_AUTH="{htpasswd: {realm: localhost, path: /etc/docker/registry/auth.htpasswd}}" -e REGISTRY_HTTP_ADDR=0.0.0.0:5000 -e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt -e REGISTRY_HTTP_TLS_KEY=/certs/domain.key -e REGISTRY_STORAGE_DELETE_ENABLED=true registry # 查看容器(只能本地访问) $docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 86bc983e8183 registry "/entrypoint.sh /etc…" About a minute ago Up About a minute 127.0.0.1:443->5000/tcp registry
3.4) 验证registry是否可登录,登录后账号密码将会base64存储在 /root/.docker/config.json 之中,为后续使用skopeo的时候做准备;
$docker login localhost -u weiyigeek -p 123456 # WARNING! Using --password via the CLI is insecure. Use --password-stdin. # WARNING! Your password will be stored unencrypted in /root/.docker/config.json. Login Succeeded cat /root/.docker/config.json { "auths": { "https://index.docker.io/v1/": { "auth": "d2VpxOQ=="}, "localhost": {"auth": "d2VpeWlnZWVrOjEyMzQ1Ng==" } }, "HttpHeaders": {"User-Agent": "Docker-Client/19.03.9 (linux)"} } # base64 编码 $echo "d2VpeWlnZWVrOjEyMzQ1Ng==" | base64 -d weiyigeek:123456[
3.5) 上传一个镜像到registry之中
$docker tag alpine localhost/alpine:latest $docker images localhost/alpine latest a24bb4013296 2 months ago 5.57MB $docker push localhost/alpine:latest # The push refers to repository [localhost/alpine] # 50644c29ef5a: Pushed # latest: digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 size: 528
3.6)registry 查看上传的镜像
# (1) 查看registry中存储的镜像仓库名称 (注意--cacert参数如果证书未导入到系统中则必须加上) $curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog {"repositories":["alpine"]}
3.7) 利用skopeo转储镜像到registry之中和操作镜像:
# (1) 首先信任CA证书,根据不同的发行版选择相应的路径和命令行即可,为后面skopeo命令使用做准备。 # CentOS update-ca-trust force-enable cp certs/domain.crt /etc/pki/ca-trust/source/anchors/localhost.crt update-ca-trust # Ubuntu cp certs/domain.crt /usr/local/share/ca-certificates/localhost.crt $ update-ca-certificates # Debian cp certs/domain.crt /usr/share/ca-certificates/localhost.crt echo localhost.crt >> /etc/ca-certificates.conf update-ca-certificates # (2) COPY 镜像到 registry skopeo inspect docker://docker.io/alpine # 以 localhost/library/alpine:3.10 为例 # localhost 就是该 registry 的域名或者 URL # library 就是项目名称 project # alpine:3.12 就是镜像名和镜像的 tag skopeo copy docker://alpine:3.12 docker://localhost/library/alpine:3.12 # Getting image source signatures # Copying blob df20fa9351a1 done # Copying config a24bb40132 done # Writing manifest to image destination # Storing signatures # (3) 删除registry仓库中的镜像(删除后并不彻底) skopeo delete docker://localhost/alpine # (4) 查看仓库里的镜像 curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog {"repositories":["library/alpine"]} skopeo inspect docker://localhost/library/alpine:3.12 { "Name": "localhost/library/alpine", "Digest": "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65", "RepoTags": ["3.12"], "Created": "2020-05-29T21:19:46.363518345Z", "DockerVersion": "18.09.7", "Labels": null, "Architecture": "amd64", "Os": "linux", "Layers": ["sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c"], "Env": ["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"] }
1.证书颁发者可以用中间证书提供给您。在这种情况下,你必须用中间证书串连您的证书形成的证书捆绑。
cat domain.crt intermediate-certificates.pem > certs/domain.crt
2.Let’s Encrypt:https://letsencrypt.org/how-it-works/
描述: 此时以上传的library/alpine:3.12镜像为例查看registry目录中文件变化;
registry 仓库结构目录如下:
# registry 持久化位置 $ls /var/lib/registry/docker/registry/v2/ blobs repositories # registry 树形结构 tree -h /var/lib/registry/docker/registry/v2/ /var/lib/registry/docker/registry/v2/ ├── [ 20] blobs │ └── [ 36] sha256 │ ├── [ 78] a1 │ │ └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 镜像data manifest (存储了data config与data layer 的 Digest摘要信息) │ │ └── [ 528] data │ ├── [ 78] a2 │ │ └── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e # 镜像data config │ │ └── [1.5K] data # "mediaType": "application/vnd.docker.container.image.v1+json", │ └── [ 78] df │ └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c # 镜像data layer (通常体积最大) │ └── [2.7M] data # "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", └── [ 21] repositories └── [ 20] library └── [ 55] alpine ├── [ 20] _layers │ └── [ 150] sha256 │ ├── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e # 指向 > blobs/sha256/a24bb401......6b63d83e │ │ └── [ 71] link │ └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c # 指向 > blobs/sha256/df20fa9.......9a752eb4c │ └── [ 71] link ├── [ 35] _manifests │ ├── [ 20] reVisions # 修订记录 │ │ └── [ 78] sha256 │ │ └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 修订指向 manifest > blobs/sha256/a15790640......c90fd65 │ │ └── [ 71] link │ └── [ 18] tags │ └── [ 34] 3.12 │ ├── [ 18] current │ │ └── [ 71] link # 指向 blobs 中 data manifest digest a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ └── [ 20] index │ └── [ 78] sha256 │ └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ └── [ 71] link # 同上 └── [ 6] _uploads # 镜像上传过程中的临时目录 26 directories, 8 files
Q: 那 registry 存储目录到底长什么样? ????
答: 结合下面这张图可以看见,registry 存储目录下只有两种文件名的文件即data与link文件 (1) link 文件: 是普通的文本文件存放在 repositories 目录下,其内容是指向 data 文件的 sha256 digest值, 从字面意义上就很好理解它; (2) data 文件: 存放在 blobs 目录下文件且分为三个文件(即镜像的layer/config/manifest等文件)
# (1) Repositories $ls /var/lib/registry/docker/registry/v2/repositories/alpine/ _layers _manifests _uploads # - _layers/sha256 目录下的文件夹名是镜像的Layer和Config的Digest,通该目录下的link文件找到对应 blobs 目录下的 data 文件,实际上当我们 pull 一个镜像的 layer 时,是通过 link 文件找到 layer 在 registry 中实际的存储位置的。 # - _manifests 文件夹下的 tags 和 revisions 目录下的 link 文件则指向该镜像的 manifest 文件,保存在所有历史镜像tag的manifest文件的link。当删除一个镜像时,只会删除该镜像最新的 tag 的 link 文件。 # - revisions 目录记录了镜像修订版本 $ls /var/lib/registry/docker/registry/v2/repositories/alpine(镜像名称)/_manifests/revisions/sha256/ a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # - tags 目录下的文件夹名例如3.10就是镜像的Tag,在它的子目录下的 current/link 文件则记录了当前 tag 指向的 manifest 文件的位置; # 比如我们的 alpine:latest 镜像,每次 push 新的 latest 镜像时current/link 都会更新成指向最新镜像的 manifest 文件。 $ ls /var/lib/registry/docker/registry/v2/repositories/alpine(镜像名称)/_manifests/tags/3.12(镜像标记)/ current index # 目前都指向 a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 即 镜像 data manifest # (2) Blobs $ls /var/lib/registry/docker/registry/v2/blobs/sha256/ a1/ a2/ df/ $find /var/lib/registry/docker/registry/v2/ -name "data" -exec ls -sh {} ; # image layer 文件是 gzip 格式的 tar 包,是镜像层真实内容的 tar.gzip 格式存储形式。 2.7M ./blobs/sha256/df/df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c/data # image config 文件是 json 格式的,它是在构建时候生成的根据DockerFile和宿主机的一些信息; (记录了"rootfs"."diff_ids") 4.0K ./blobs/sha256/a2/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e/data # image manifest 文件json 文件格式的,存放该镜像 layer 和 image config 文件的索引。(镜像拉取首先需拉取此文件) 4.0K ./blobs/sha256/a1/a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65/data
WeiyiGeek.registry存储结构
此时我们可以再往Registry中COPY一个镜像方便后面进行对比分析:
$skopeo copy docker://debian:buster-slim docker://localhost/library/debian:buster-slim $curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog {"repositories":["library/alpine","library/debian"]} # 对比分析 registry 中 alpine:3.12 和 debian:buster-slim 两个基础镜像,此时在registry 存储目录的结构如下: $tree -h /var/lib/registry/docker/registry/v2/ /var/lib/registry/docker/registry/v2/ ├── [ 20] blobs │ └── [ 66] sha256 │ ├── [ 78] a1 │ │ └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ │ └── [ 528] data │ ├── [ 78] a2 │ │ └── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e │ │ └── [1.5K] data │ ├── [ 78] bf │ │ └── [ 18] bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb # debian:buster-slim 的 Data Layer │ │ └── [ 26M] data │ ├── [ 78] c7 │ │ └── [ 18] c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773 # debian:buster-slim 的 Data Config │ │ └── [1.5K] data │ ├── [ 78] df │ │ └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c │ │ └── [2.7M] data │ └── [ 78] e0 │ └── [ 18] e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 # debian:buster-slim 的 Data Manifest │ └── [ 529] data └── [ 21] repositories └── [ 34] library ├── [ 55] alpine # 此处不做过多说明 │ ├── [ 20] _layers │ │ └── [ 150] sha256 │ │ ├── [ 18] a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e │ │ │ └── [ 71] link │ │ └── [ 18] df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c │ │ └── [ 71] link │ ├── [ 35] _manifests │ │ ├── [ 20] revisions │ │ │ └── [ 78] sha256 │ │ │ └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ │ │ └── [ 71] link │ │ └── [ 18] tags │ │ └── [ 34] 3.12 │ │ ├── [ 18] current │ │ │ └── [ 71] link │ │ └── [ 20] index │ │ └── [ 78] sha256 │ │ └── [ 18] a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ │ └── [ 71] link │ └── [ 6] _uploads └── [ 55] debian ├── [ 20] _layers │ └── [ 150] sha256 │ ├── [ 18] bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb │ │ └── [ 71] link # 摘要指向 blobs 中debian:buster-slim 的 Data Layer │ └── [ 18] c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773 │ └── [ 71] link # 摘要指向 blobs 中debian:buster-slim 的 Data Config ├── [ 35] _manifests │ ├── [ 20] revisions │ │ └── [ 78] sha256 │ │ └── [ 18] e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 │ │ └── [ 71] link # 摘要指向 blobs 中 debian:buster-slim 的 Data Manifest │ └── [ 25] tags │ └── [ 34] buster-slim # 以下摘要都是指向 blobs 中 debian:buster-slim 的 Data Manifest │ ├── [ 18] current │ │ └── [ 71] link │ └── [ 20] index │ └── [ 78] sha256 │ └── [ 18] e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 │ └── [ 71] link └── [ 6] _uploads 48 directories, 16 files
然后我们再采用skopeo删除我们上传到Registry仓库中的镜像,再进行目录的对比:
$skopeo delete docker://localhost/library/alpine:3.12 --debug # 列出变化的目录结构部分 repositories └── library ├── alpine │ ├── _layers │ │ └── sha256 │ │ ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e │ │ │ └── link │ │ └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c │ │ └── link │ ├── _manifests │ │ ├── revisions │ │ │ └── sha256 │ │ │ └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ │ └── tags # tags 下面的目录与文件被删除 │ └── _uploads
总结上述:
1.上述可以看见当skopeo delete删除一个镜像时,只是对_manifests下的文件revisions/sha256/a15790640a6690...ka9f2b0d7cc90fd65/link与tags即其子目录文件进行删除;实际上两者删除的是同一个内容,即对记录了该镜像 manifests 文件 digest摘要 的 link 文件。
2.运行后从–debug参数中得到DEBU[0000] DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65可以得出通过 registry API 来 DELETE 一个镜像实质上是删除 repositories 元数据文件夹下的 tag 名文件夹和该 tag 的 revisions 下的 link 文件。
# 我们也可以采用Registry API 进行操作达到同样的效率 我们定义摘要字符串匹配以下语法: digest := algorithm ":" hex algorithm := /[A-Fa-f0-9_+.-]+/ hex := /[A-Fa-f0-9]+/ # digest = sha256:6c3c624b58dbbcd3c0dd82b4c53f04194d1247c6eebdaab7c610cf7d66709b3b # (1) 获取到镜像 Docker-Content-Digest: $curl -I -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 # HTTP/1.1 200 OK # Content-Length: 528 # Content-Type: application/vnd.docker.distribution.manifest.v2+json # Docker-Content-Digest: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # Docker-Distribution-Api-Version: registry/2.0 # Etag: "sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65" # X-Content-Type-Options: nosniff # Date: Fri, 21 Aug 2020 02:30:06 GMT # (2) 删除registry仓库中的指定Docker-Content-Digest的镜像tags 与 links 文件 $curl -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65
可以看出删除仓库中的镜像,只是把在registry仓库中镜像的tags子目录与revisions/sha256/…/link文件进行删除,而blobs中的镜像 data 与 repositories 中镜像目录library/alpine/并未被删除,这样导致的后果是registry仓库的服务器将会占用一部分存储资源导致资源的浪费,那如何解决这个问题就是我们下面提到的Registry GC 机制;
描述:可以通过registry API操作管理镜像或者获取镜像manifest相关信息; 官方参考地址: https://docs.docker.com/registry/spec/api/
描述:通过API遇到的错误代码如下表所示: https://docs.docker.com/registry/spec/api/#errors-2
API方法和URI列表涵盖如下表:
Method | Path | Entity | Description |
---|---|---|---|
GET | /v2/ | Base | Check that the endpoint implements Docker Registry API V2. |
GET | /v2/_catalog /v2/_catalog?n=<integer> | Catalog | 检索注册中心中可用的存储库的排序json列表 |
GET | /v2/<name>/tags/list | Tags | 获取存储库下由“name”标识的标记。 |
GET | /v2/manifests/ | Manifest | 获取由“name”和“reference”标识的清单,其中“reference”可以是标记或摘要。还可以向这个端点发出一个’ HEAD ‘请求,在不接收所有数据的情况下获取资源信息。 |
PUT | /v2/manifests/ | Manifest | 把由“name”和“reference”标识的清单放在“reference”可以是标签或摘要的地方。 |
DELETE | /v2/manifests/ | Manifest | 删除由“name”和“reference”标识的清单。注意,一个清单只能被“摘要”删除。 |
GET | /v2/blobs/ | Blob | 从由“摘要”标识的注册表中检索blob。还可以向这个端点发出一个’ HEAD ‘请求,在不接收所有数据的情况下获取资源信息。 |
DELETE | /v2/blobs/ | Blob | 删除由“name”和“digest”标识的blob |
POST | /v2/blobs/uploads/ | Initiate Blob Upload | 如果成功,将提供一个上传位置来完成上传。可选地,如果“digest”参数存在,请求主体将用于在单个请求中完成上传。 |
GET | /v2/blobs/uploads/ | Blob Upload | 此端点的主要目的是解决可恢复上传的当前状态利用uuid。 |
PATCH | /v2/blobs/uploads/ | Blob Upload | 上传指定上传的数据块。 |
PUT | /v2/blobs/uploads/ | Blob Upload | 完成’ uuid ‘指定的上传,可选附加主体作为最后块 |
DELETE | /v2/blobs/uploads/ | Blob Upload | 取消未完成的上传进程,释放相关资源。如果没有调用此操作,未完成的上传最终将超时。 |
(Important)结合registry仓库解释镜像PULL与PUSH过程:
(1) PULL 镜像: 镜像由一个json清单和层叠文件组成,pull镜像的过程就是检索这两个组件的过程。
- 第一步就是获取清单,清单由下面几个字段组成: registry:5000/v2/redis/manifests/latest(获取redis:latest清单文件) # 字段 描述 # name 镜像名称 # tag 镜像当前版本的tag # fsLayers 层描述列表(包括摘要) # signature 一个JWS签名,用来验证清单内容 - 第二步当获取清单之后,客户端需要验证前面(signature),以确保名称和fsLayers层是有效的。确认后客户端可以使用digest去下载各个fs层。在V2api中层存储在blobs中已digest作为键值. 1.首先拉取镜像清单(pulling an Image Manifest) $ HEAD /v2/<image/manifests/<reference>#检查镜像清单是否存在 $ GET /v2/<image>/manifests/<reference>#拉取镜像清单 提示:reference可是是tag或者是digest 2.开始拉取每个层(pulling a Layer) $ GET /v2/<image>/blobs/<digest> 提示:digest是镜像每个fsLayer层的唯一标识,存在于清单的fsLayers里面。
(2) PUSH 镜像: 推送镜像和拉取镜像过程相反,先推各个层到registry仓库,然后上传清单.(Pushing a Layer(上传层)分为2步)
# 2.1) 使用post请求在registry仓库启动上传服务,返回一个url这个url用来上传数据和检查状态。 # 首先Existing Layers(检查层是否存在),若返回200 OK 则表示存在,不用上传 $ HEAD /v2/image/blobs/<digest> # 开始上传服务(Starting An Upload),如果post请求返回202 accepted,一个url会在location字段返回. $ POST /v2/image/blobs/uploads/ # 202 Accepted # Location: /v2/<image>/blobs/uploads/<uuid> # Range: bytes=0-<offset> # Content-Length: 0 # Docker-Upload-UUID: <uuid> # 可以用来查看上传状态和实现断点续传 # 2.2) 开始上传层(Uploging the Layer) > PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest> Content-Length: <size of layer>> Content-Type: application/octet-stream # 上传进度(Upload Progress) $ GET /v2/<image>/blobs/uploads/<uuid> # 204 No Content # Location: /v2/<name>/blobs/uploads/<uuid> # Range: bytes=0-<offset> # Docker-Upload-UUID: <uuid> # 重点-整块上传(Monolithic Upload) > PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest> > Content-Length: <size of layer> > Content-Type: application/octet-stream <Layer Binary Data> # 重点-分块上传(Chunked Upload) > PATCH /v2/<name>/blobs/uploads/<uuid> > Content-Length: <size of chunk> > Content-Range: <start of range>-<end of range> > Content-Type: application/octet-stream <Layer Chunk Binary Data> # 如果服务器不接受这个块,则返回: # 416 Requested Range Not Satisfiable # Location: /v2/<name>/blobs/uploads/<uuid> # Range: 0-<last valid range> # Content-Length: 0 # Docker-Upload-UUID: <uuid> # 成功则返回: # 202 Accepted # Location: /v2/<name>/blobs/uploads/<uuid> # Range: bytes=0-<offset> # Content-Length: 0 # Docker-Upload-UUID: <uuid> # 重点-交叉上传(Cross Repository Blob Mount)可以把客户端有访问权限的已有存储库中的层挂载到当前存储库中 POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name> Content-Length: 0 # 成功返回: # 201 Created # Location: /v2/<name>/blobs/<digest> # Content-Length: 0 # Docker-Content-Digest: <digest> # 失败返回: # 202 Accepted # Location: /v2/<name>/blobs/uploads/<uuid> # Range: bytes=0-<offset> # Content-Length: 0 # Docker-Upload-UUID: <uuid> # 3.3) 上传完成(Completed Upload),但是注意分块上传在最后一块上传完毕后,需要提交一个上传完成的请求 > PUT /v2/<name>/blob/uploads/<uuid>?digest=<digest> > Content-Length: <size of chunk> > Content-Range: <start of range>-<end of range> > Content-Type: application/octet-stream <Last Layer Chunk Binary Data> # 成功返回: # 201 Created # Location: /v2/<name>/blobs/<digest> # Content-Length: 0 # Docker-Content-Digest: <digest>
Tips: 后续不再加--cacert /opt/registry/certs/domain.crt参数,默认大家都已经把证书导入带系统本地;
0.Registry V2协议及其认证请求验证
# Registry 仓库协议 $curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/ HTTP/1.1 200 OK # 采用一个RFC7235兼容的授权头进行认证 $curl -I -H 'Authorization: Basic d2VpeWlnZWVrOjEyMzQ1Ng==' -X GET https://localhost/v2/ HTTP/1.1 200 OK
1.查看Registry仓库中有那些镜像(不精确-当通过delete删除镜像时候此处并未删除需要手动到repositories文件夹中删除)
# Registry 仓库中所有镜像 curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET https://localhost/v2/_catalog {"repositories":["library/alpine","library/debian"]} # 返回仓库中指定条目的镜像(通过-v 参数可看出last的不同) curl --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -X GET "https://localhost/v2/_catalog?n=1&last=a"
2.获取某个镜像的标签列表 (注意加或者未加Project的区别)
curl -u 'weiyigeek:123456' -X GET https://localhost/v2/alpine/tags/list # {"name":"alpine","tags":["latest"]} curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/tags/list # 'library/alpine' # {"name":"library/alpine","tags":["3.12"]} # 列出镜像部分tags(Pagination) curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/tags/list?n=<integer>
3.拉取Registry 仓库镜像中Manifests(清单)文件
# 判断指定镜像与tags的Manifests(清单)是否存在 curl -I -u 'weiyigeek:123456' -X HEAD https://localhost/v2/library/alpine/manifests/3.12 # HTTP/1.1 200 OK # Content-Length: 2783 # Content-Type: application/vnd.docker.distribution.manifest.v1+prettyjws # 仓库中`Manifests`清单 $ curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/manifests/3.12 # { # "schemaVersion": 1, # "name": "library/alpine", # "tag": "3.12", # "architecture": "amd64", # "fsLayers": [{ # "blobSum": "sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4"},{ # "blobSum": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c" # } # ], # "history": [ # { # "v1Compatibility": "{"architecture":"amd64","config":{"Hostname":"","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh"],"ArgsEscaped":true,"Image":"sha256:64771e4514cb653a0fe68e1ceed5bd16640ebf3bd859dc3333efe87dc4709a5d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":null},"container":"ce1874fa1fc1eb128516899352f185645f492c443b5a80d9a3fae8b09d1b6b16","container_config":{"Hostname":"ce1874fa1fc1","Domainname":"","User":"","AttachStdin":false,"AttachStdout":false,"AttachStderr":false,"Tty":false,"OpenStdin":false,"StdinOnce":false,"Env":["PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"],"Cmd":["/bin/sh","-c","#(nop) ","CMD ["/bin/sh"]"],"ArgsEscaped":true,"Image":"sha256:64771e4514cb653a0fe68e1ceed5bd16640ebf3bd859dc3333efe87dc4709a5d","Volumes":null,"WorkingDir":"","Entrypoint":null,"OnBuild":null,"Labels":{}},"created":"2020-05-29T21:19:46.363518345Z","docker_version":"18.09.7","id":"4a28aef4f8a9e13b1df98eaf8e651db2857161cea27134dad697ad0c7a7de12d","os":"linux","parent":"a5213fa3ad8fa7a42f88213945845ef49dcf11328d51576b8f076142ce75bdf8","throwaway":true}" # }, # { # "v1Compatibility": "{"id":"a5213fa3ad8fa7a42f88213945845ef49dcf11328d51576b8f076142ce75bdf8","created":"2020-05-29T21:19:46.192045972Z","container_config":{"Cmd":["/bin/sh -c #(nop) ADD file:c92c248239f8c7b9b3c067650954815f391b7bcb09023f984972c082ace2a8d0 in / "]}}" # } # ], # "signatures": [ # { # "header": { # "jwk": { # "crv": "P-256", # "kid": "P6TV:UOU3:V564:FNEL:DQG2:WQX5:6Z5P:NQF6:XZOR:JTMI:Q2QI:AQZ3", # "kty": "EC", # "x": "n70C5idlCOFB4ubdg5K6MCvRBIH6d5YzhTRumV1i6D8", # "y": "OmZn6AyifVg3kZ67ICPViHTHBXvMui8fPwqXzbTnWw0" # }, # "alg": "ES256" # }, # "signature": "S4Tvfqx0nA7hULgyKdKKdoYpgMsTqxlbQ6JDeQv1HXZie1zMCsafNZfLI59kivzHb7IV8hwEnvxehL0cKuoZ4w", # "protected": "eyJmb3JtYXRMZW5ndGgiOjIxMzYsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAyMC0wOC0yMlQwNzoyNDozM1oifQ" # } # ] # }
4.获取仓库镜像的manifests内容 (go-hello:scratch)
curl -v -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/go-hello/manifests/scratch # { # "schemaVersion": 2, # "mediaType": "application/vnd.docker.distribution.manifest.v2+json", # "config": { # "mediaType": "application/vnd.docker.container.image.v1+json", # "size": 1472, # "digest": "sha256:cb05b87d001253772ae9a212200de5eb8304ab9691c61589332a2f57e7059209" # }, # "layers": [ # { # "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip", # "size": 1106793, # "digest": "sha256:5395625ce01dee311e2f7c879b0b148ac7525de7aad5080a518d7f7e5a99d368" # } # ] # }
5.获取(镜像:版本)标识的data manifests的 digest
curl -I --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/go-hello/manifests/scratch # HTTP/1.1 200 OK # Content-Length: 528 # Content-Type: application/vnd.docker.distribution.manifest.v2+json # 注意Docker-Content-Digest中的内容: 在registry2.3或更高版本删除清单时,必须在HEAD或GET获取清单以获取要删除的正确digest携带以下头; # Docker-Content-Digest: sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e # Docker-Distribution-Api-Version: registry/2.0 # Etag: "sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e" # X-Content-Type-Options: nosniff # Date: Fri, 21 Aug 2020 02:30:06 GMT # 【简约版本:直接提取Docker-Content-Digest头内容】 curl -Is -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "Docker-Content-Digest:" | cut -f 2 -d " " sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 【如不指定Accept则默认为 application/vnd.docker.distribution.manifest.v1+prettyjws 】 curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/manifests/3.12 HTTP/1.1 200 OK # Content-Length: 2783 # Content-Type: application/vnd.docker.distribution.manifest.v1+prettyjws # Docker-Content-Digest: sha256:cae82a43ba96214acd380f3d4ed043445f56f80f0fc99f3f927d5e6eaee40791 # Docker-Distribution-Api-Version: registry/2.0 # Etag: "sha256:cae82a43ba96214acd380f3d4ed043445f56f80f0fc99f3f927d5e6eaee40791" # X-Content-Type-Options: nosniff # Date: Sat, 22 Aug 2020 03:01:16 GMT
6.删除仓库中的镜像即删除(repositories下面的 _manifests 中的Tags 与 revisions 下的link)
# 加入 -v 参数 查看请求返回流程 $curl -v --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/go-hello/manifests/sha256:8dabce532312b587329fe225ef501051c60f81ffdb2c801a5da6348b9cab132e # 202 Accepted # Content-Length: None # 失败 返回404错误
注意:默认情况下,registry不允许删除镜像操作,需要在启动registry时指定环境变量REGISTRY_STORAGE_DELETE_ENABLED=true,或者修改其配置文件即可。reference必须是digest,否则删除将失败。在registry2.3或更高版本删除清单时,必须在HEAD或GET获取清单以获取要删除的正确digest携带以下头: Accept: application/vnd.docker.distribution.manifest.v2+json
7.拉取镜像,由于层被存储在注册表中的blobs中所以是需要通过一个标准的HTTP请求来进行拉取一个层的信息
# (1) 先查看镜像 data 相关的 Digest 码 curl -s -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | jq '"Data Config - " + .config.digest','"Data Layer - " + .layers[0].digest' # "Data Config - sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e" # "Data Layer - sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c" # (2) 获取拉取镜像的data config 与 data layer 文件 curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/blobs/sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e | jq curl -u 'weiyigeek:123456' -X GET https://localhost/v2/library/alpine/blobs/sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c -o /tmp/alpine-3.12.tar.gz [root@k8s tmp]$ ls -lh alpine-3.12.tar.gz -rw-r--r--. 1 root root 2.7M 8月 22 16:27 alpine-3.12.tar.gz [root@k8s tmp]$ tar -zxvf alpine-3.12.tar.gz # 实际上该压缩文件中存放的是rootfs文件系统 # bin/ # bin/arch # bin/ash # bin/base64 # bin/bbconfig
8.镜像通过Registry API上传到仓库中
# 所有层上传使用两个步骤来管理上传过程。 * 第一步开始在注册表中的服务上传,返回一个URL来进行第二步。 * 第二步使用上载URL传递的实际数据。上传都开始返回,可用于将数据推和检查上传状态URL的POST请求。 # Location头将用于每个请求后进行通信的上载位置。虽然它不会在本技术规格改变,客户应使用API返回的最新值。 # (1) 开始上载一个POST请求 # POST /v2/<name>/blobs/uploads/ curl -I -u 'weiyigeek:123456' -X POST https://localhost/v2/test-images/blobs/uploads/ # 如果POST请求是成功的它将返回 202响应 将与Location头上传的URL返回: # HTTP/1.1 202 Accepted # Content-Length: 0 # Docker-Distribution-Api-Version: registry/2.0 # Docker-Upload-Uuid: 9efa5ca7-d009-4532-88de-695c1f945e59 # 必须额 # Location: https://localhost/v2/test-images/blobs/uploads/9efa5ca7-d009-4532-88de-695c1f945e59?_state=XqlcahxfSzts1a43SgEs_MQ9-GAczgadg-Ra3vayoh57Ik5hbWUiOiJ0ZXN0LWltYWdlcyIsIlVVSUQiOiI5ZWZhNWNhNy1kMDA5LTQ1MzItODhkZS02OTVjMWY5NDVlNTkiLCJPZmZzZXQiOjAsIlN0YXJ0ZWRBdCI6IjIwMjAtMDgtMjJUMDg6NDA6NDguMDkwNTk0NzQ4WiJ9 # 指定了位置标头 # Range: 0-0 # X-Content-Type-Options: nosniff # Date: Sat, 22 Aug 2020 08:40:48 GMT # (2) 通过HEAD请求到BLOB存储API进行检查镜像相关层是否存在(可用返回200 OK) # HEAD /v2/<name>/blobs/<digest> curl -I -u 'weiyigeek:123456' -X HEAD https://localhost/v2/test-images/blobs/sha256:a14....jk5 # (3) 上传进度查看此时需要第一步中的Docker-Upload-Uuid之进行请求 # GET /v2/<name>/blobs/uploads/<uuid> curl -I -u 'weiyigeek:123456' -X GET https://localhost/v2/test-images/blobs/uploads/9efa5ca7-d009-4532-88de-695c1f945e59 # (4) Monolithic Upload 简单的单块上传,并可以通过想避免分块的复杂性的 # PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest> # Content-Length: <size of layer> # Content-Type: application/octet-stream # <Layer Binary Data> # (5) Chunked Upload 进行组块的上载,该客户机可以指定一个范围报头和仅包括层文件的一部分: # PATCH /v2/<name>/blobs/uploads/<uuid> # Content-Length: <size of chunk> # Content-Range: <start of range>-<end of range> # Content-Type: application/octet-stream # <Layer Chunk Binary Data> # (6) 跨存储库Blob挂载,可以从客户机具有读访问权的另一个存储库挂载blob,从而不需要将已知的blob上传到注册中心,要发出一个blob挂载而不是一个upload, POST请求应该以以下格式发出(成功将返回202 Accepted): POST /v2/<name>/blobs/uploads/?mount=<digest>&from=<repository name> Content-Length: 0 # (7) Completed Upload 当镜像上传完毕必须进行以下请求否则仓库不认为镜像个层全部上传,当接收到最后一个块和层已被验证时候将返回201 Create 并且返回该镜像的Docker-Content-Digest值; # PUT /v2/<name>/blobs/uploads/<uuid>?digest=<digest> # Content-Length: <size of chunk> # Content-Range: <start of range>-<end of range> # Content-Type: application/octet-stream # <Last Layer Chunk Binary Data> # (8) 取消上传镜像到仓库,可通过发出DELETE请求到registry之中; # DELETE /v2/<name>/blobs/uploads/<uuid> # (9) 删除层(Deleting a Layer) # DELETE /v2/<image>/blobs/<digest> # 成功返回: # 202 Accepted # Content-Length: None # (10) 上传镜像清单(Pushing an Image Manifest),我们上传完镜像层之后,就开始上传镜像清单 # PUT /v2/<name>/manifests/<reference> # Content-Type: <manifest media type> # { # "name": <name>, # "tag": <tag>, # "fsLayers": [ # { # "blobSum": <digest> # }, # ... # ] # ], # "history": <v1 images>, # "signature": <JWS>, # ... # } # 如果清单中有层("blobSum":<digest>)是未知的,则返回 # { "errors:" [{ "code": "BLOB_UNKNOWN", "message": "blob unknown to registry", "detail": { "digest": <digest> # } # }, # ... # ] # } #
删除镜像Manifests与镜像层信息: ```bash curl -Is -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "Docker-Content-Digest:" | cut -f 2 -d " " sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 curl -s -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X GET https://localhost/v2/library/alpine/manifests/3.12 | grep "digest" "digest": "sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e" "digest": "sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c" # 删除 镜像 _Manifests Tags curl -v --cacert /opt/registry/certs/domain.crt -u 'weiyigeek:123456' -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE https://localhost/v2/library/alpine/manifests/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 删除 镜像 _Layer curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c curl -v -u 'weiyigeek:123456' -X DELETE "https://localhost/v2/library/alpine/blobs/sha256:a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4" curl -v -u 'weiyigeek:123456' -X DELETE https://localhost/v2/library/alpine/blobs/sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 # 清空后效果 repositories | library │ ├── alpine │ │ ├── _layers │ │ │ └── sha256 │ │ │ ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e │ │ │ ├── a3ed95caeb02ffe68cdd9fd84406680ae93d633cb16422d00e8a7c22955b46d4 │ │ │ └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c │ │ ├── _manifests │ │ │ ├── revisions │ │ │ │ └── sha256 │ │ │ │ └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ │ │ └── tags │ │ └── _uploads
简单粗暴清空 Registry 仓库:
$tree /var/lib/registry/docker/registry/v2/blobs/sha256/ /var/lib/registry/docker/registry/v2/blobs/sha256/ ├── 0e ├── 32 ├── 3f ├── 53 ├── 59 ├── 8d ├── a1 ├── a2 ├── b7 ├── b9 ├── cb ├── df └── e7 rm -rf /var/lib/registry/docker/registry/v2/blobs/sha256/* rm -rf /var/lib/registry/docker/registry/v2/repositories/*
描述:在上一章节中我们阐述了为什么要引入Registry GC机制,再说其目的用处前我们先对其进行一个简单的介绍;
Q: 什么是Registry GC ?
答 :GC英文全称Garbage collection就是垃圾回收的意思,从 docker 官方文档关于GC偷来的 example 来解释一下吧。 官网文档:https://docs.docker.com/registry/garbage-collection/
#假如镜像 A 和镜像 B 它们分别引用了layer a,b和 a,c。 - A - | | a - b - c | | ---- B ---- # 通过 registry API 删除镜像 B 之后,layer c 并没有删掉,只是删掉了对它的引用所以 c 是多余的(就是我们上诉提到那种清情况)。 - A - | | a - b - c #此时通过GC机制之后 Layer C 被删除掉了即没有被引用的层将被彻底删除; - A - | | a - b
此处可以借鉴registry GC 的源码文件 garbagecollect.go 可以看到 GC 的主要分两个阶段:
(1) marking 阶段:根据上文我们提到的 link 文件,通过扫描所有镜像 tags 目录下的 link 文件就可以得到这些镜像的 manifest,在 manifest 中保存在该镜像所有的 layer 和 config 文件的 digest 值,把这些值标记为不能清除。
markSet := make(map[digest.Digest]struct{}) manifestArr := make([]ManifestDel, 0) err := repositoryEnumerator.Enumerate(ctx, func(repoName string) error { emit(repoName) var err error named, err := reference.WithName(repoName) if err != nil { return fmt.Errorf("failed to parse repo name %s: %v", repoName, err) } repository, err := registry.Repository(ctx, named) if err != nil { return fmt.Errorf("failed to construct repository: %v", err) } manifestService, err := repository.Manifests(ctx) if err != nil { return fmt.Errorf("failed to construct manifest service: %v", err) } manifestEnumerator, ok := manifestService.(distribution.ManifestEnumerator) if !ok { return fmt.Errorf("unable to convert ManifestService into ManifestEnumerator") }
(2) sweep 阶段:删除操作当marking完成之后没有标记blobs(layer 和 config)就会被清理掉;
// sweep vacuum := NewVacuum(ctx, storageDriver) if !opts.DryRun { for _, obj := range manifestArr { err = vacuum.RemoveManifest(obj.Name, obj.Digest, obj.Tags) if err != nil { return fmt.Errorf("failed to delete manifest %s: %v", obj.Digest, err) } } } blobService := registry.Blobs() deleteSet := make(map[digest.Digest]struct{}) err = blobService.Enumerate(ctx, func(dgst digest.Digest) error { // check if digest is in markSet. If not, delete it! if _, ok := markSet[dgst]; !ok { deleteSet[dgst] = struct{}{} } return nil })
WeiyiGeek.marking and sweep
Q:那 GC 都干了啥?
答: 我们可以利用registry容器中的registry garbage-collect命令进行GC回收操作;
$ docker exec -it registry sh -c "/bin/registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml" / # cat /etc/docker/registry/config.yml version: 0.1 log: fields: service: registry storage: cache: blobdescriptor: inmemory filesystem: rootdirectory: /var/lib/registry # 关键点 http: addr: :5000 headers: X-Content-Type-Options: [nosniff] health: storagedriver: enabled: true interval: 10s threshold: 3 # marking 阶段 library/alpine # Registry中的镜像未标记将会被删除 library/debian library/debian: marking manifest sha256:e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 library/debian: marking blob sha256:c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773 library/debian: marking blob sha256:bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb # sweep 阶段 3 blobs marked, 3 blobs and 0 manifests eligible for deletion blob eligible for deletion: sha256:a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a1/a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry blob eligible for deletion: sha256:a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/a2/a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry blob eligible for deletion: sha256:df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c INFO[0000] Deleting blob: /docker/registry/v2/blobs/sha256/df/df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c go.version=go1.11.2 instance.id=fe470a94-4f51-4764-93e5-0f1d5d6172bf service=registry
此时经过GC之后的Registry 存储目录长什么样子?
$ tree /var/lib/registry/docker/registry/v2 /var/lib/registry/docker/registry/v2 ├── blobs │ └── sha256 │ ├── a1 │ ├── a2 │ ├── bf │ │ └── bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb │ │ └── data │ ├── c7 │ │ └── c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773 │ │ └── data │ ├── df │ └── e0 │ └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 │ └── data └── repositories └── library ├── alpine # 可以看见镜像名称并未删除 │ ├── _layers │ │ └── sha256 │ │ ├── a24bb4013296f61e89ba57005a7b3e52274d8edd3ae2077d04395f806b63d83e │ │ │ └── link │ │ └── df20fa9351a15782c64e6dddb2d4a6f50bf6d3688060a34c4014b0d9a752eb4c │ │ └── link │ ├── _manifests │ │ ├── revisions │ │ │ └── sha256 │ │ │ └── a15790640a6690aa1730c38cf0a440e2aa44aaca9b0e8931a9f2b0d7cc90fd65 │ │ └── tags │ └── _uploads └── debian ├── _layers │ └── sha256 │ ├── bf59529304463f62efa7179fa1a32718a611528cc4ce9f30c0d1bbc6724ec3fb │ │ └── link │ └── c7346dd7f20ef06fd3c58446fab0c3edf22e78131d374775f5f947849537b773 │ └── link ├── _manifests │ ├── revisions │ │ └── sha256 │ │ └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 │ │ └── link │ └── tags │ └── buster-slim │ ├── current │ │ └── link │ └── index │ └── sha256 │ └── e0a33348ac8cace6b4294885e6e0bb57ecdfe4b6e415f1a7f4c5da5fe3116e02 │ └── link └── _uploads 40 directories, 10 files
示例1:查看Docker官方镜像仓库中镜像的所有标签
方式1:
#!/bin/sh # set -xe # 其实实现方法就是通过镜像仓库的 restful API,来查询,然后把返回的 json 结果简单处理一下,然后打印出来。 image_name=$1 repo_url=https://registry.hub.docker.com/v1/repositories curl -s ${repo_url}/${image_name}/tags | jq | grep name | awk '{print $2}' | sed -e 's/"//g'
方式2:一条命令搞定
skopeo inspect docker://docker.io/alpine | jq ".RepoTags"
示例2.registry信息查看脚本与RegistryGC回收脚本
#!/bin/bash # Description:查看Registry仓库中的镜像信息并从仓库中删除指定镜像,然后进行垃圾回收 # Author:WeiyiGeek # createTime:2020年8月23日 16:55:57 set -x # [+ Defined] PARM=$1 IMAGE_NAME=${2} ACTION=${PARM:="NONE"} REGISTRY_URL="https://localhost/v2" REGISTRY_NAME="registry" REGISTRY_HOME="/var/lib/registry/docker/registry/v2" MANIFESTS_DIGEST="" AUTH="Authorization: Basic d2VpeWlnZWVrOjEyMzQ1Ng==" function Usage(){ echo -e "e[32mUsage: $0 {view} e[0m" echo -e "e[32m $0 {tags} <image-name> e[0m" echo -e "e[32m $0 {gc} <registry-container-name|container-id> e[0m" echo -e "e[32m $0 {delete} <image-name> <reference> e[0m" echo -e "e[32m #查看仓库中的镜像信息并从仓库中删除指定镜像,然后进行垃圾回收 e[0m" exit; } # [+ 显示仓库中的镜像] function ViewRegistry(){ curl -s -H "${AUTH}" "${REGISTRY_URL}/_catalog" | jq ".repositories" } # [+ 显示仓库中镜像标记] function ViewTags(){ local FLAG=0 local IMAGE_NAME=$1 curl -s -H "${AUTH}" "${REGISTRY_URL}/_catalog" | jq ".repositories" > registry.repo sed -i "s#[##g;s#]##g;s# ##g;s#"##g;s#,##g;/^s*$/d" registry.repo for i in $(cat registry.repo) do if [[ "$i" == "${IMAGE_NAME}" ]];then FLAG=1 break fi done if [[ $FLAG -eq 1 ]];then curl -s -H "${AUTH}" "${REGISTRY_URL}/${IMAGE_NAME}/tags/list" | jq ".tags" else echo -e "e[31m[ERROR]: Registry 不存在 ${IMAGE_NAME} 该镜像e[0m" exit fi } # [+ 仓库废弃镜像回收] function GcRegistry(){ docker exec -it $1 sh -c "/bin/registry garbage-collect -m --delete-untagged=true /etc/docker/registry/config.yml" if [[ $? -ne 0 ]];then echo -e "e[31m[ERROR]:GC Failed! e[0m" exit fi # 删除 blobs/sha256中的空目录 for i in $(find ${REGISTRY_HOME}/blobs/sha256/ | grep -v "data");do if [[ $(ls -A $i|wc -c) -eq 0 ]];then echo -e "[info]delete empty directory : ${i}" rm -rf ${i} fi done echo -e "[+ Registry restart ....]" docker restart $1 } # [+ 删除仓库中的镜像] function Del() { local IMAGE_NAME=$1 local TAGS=$2 if [[ "$TAGS" != "" ]];then # 验证删除的镜像是否存在 curl -s -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${TAGS}" > images.mainfests err_flag=$(grep -c '"errors"' images.mainfests) if [[ $err_flag -ne 0 ]];then echo -e "e[31m[ERROR]:$(cat images.mainfests) e[0m" exit fi # 获取要删除镜像的digest摘要 MANIFESTS_DIGEST=$(curl -s -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${TAGS}" | grep "Docker-Content-Digest:" | cut -f 2 -d " ") grep "digest" images.mainfests | sed 's# ##g;s#"##g;s#digest:##g' > images.digest echo ${MANIFESTS_DIGEST} >> images.digest # 删除 镜像 _Manifests目录中的Tags相关目录 curl -v -H "${AUTH}" -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' -X DELETE "${REGISTRY_URL}/${IMAGE_NAME}/manifests/${MANIFESTS_DIGEST}" # 删除 镜像 _Layer 目录下的link for digest in $(cat images.digest);do curl -v -H "${AUTH}" -X DELETE "${REGISTRY_URL}/${IMAGE_NAME}/blobs/${digest}" done fi # GC 回收(注意参数为容器镜像名称) GcRegistry ${REGISTRY_NAME} # 判断 镜像 是否存在其它 tags 不存在时候直接删除其目录 $flag_tags=$(curl -s -H "${AUTH}" "${REGISTRY_URL}/${IMAGE_NAME}/tags/list" | jq ".tags") if [[ -z $flag_tags ]];then rm -rf "${REGISTRY_HOME}/repositories/${IMAGE_NAME}" fi # 删除 _layers 目录下的digest文件空的目录 for i in $(find ${REGISTRY_HOME}/repositories/${IMAGE_NAME}/_layers/sha256/ | grep -v "link");do if [[ $(ls -A $i|wc -c) -eq 0 ]];then echo -e "[info]delete empty directory : ${i}" rm -rf ${i} fi done # 删除 manifests 目录下的digest文件空的目录 for i in $(find ${REGISTRY_HOME}/repositories/${IMAGE_NAME}/_manifests/revisions/sha256/ | grep -v "link");do if [[ $(ls -A $i|wc -c) -eq 0 ]];then echo -e "[info]delete empty directory : ${i}" rm -rf ${i} fi done } # [Main] if [[ "$ACTION" = "NONE" ]];then Usage elif [[ "$ACTION" = "view" ]];then ViewRegistry elif [[ "$ACTION" = "tags" ]];then ViewTags $2 elif [[ "$ACTION" = "delete" ]];then Del $2 $3 elif [[ "$ACTION" = "gc" ]];then GcRegistry $2 else Usage fi EOF
章节总结:
(1) 在GC之后registry存储目录我们可以看到,原本blobs目录下有6个data文件现在已经变成3个,而alpine:3.12镜像相关的Layer、Config、Manifest文件已经被GC清理掉了; 但是在 repositories 目录下,该镜像的 _layers 下的 link 文件依旧存在????。 (2) 总结以上,用下面这三张图片就能直观地理解这些过程啦 2.1 delete 镜像之前的 registry 存储目录结构WeiyiGeek.1
2.2 delete 镜像之后的 registry 存储目录结构WeiyiGeek.2
2.3 GC 之后的 registry 存储目录结构WeiyiGeek.
(3) GC 之后一定要重启,因为 registry 容器缓存了镜像 layer 的信息当删除掉一个镜像 A 后边 GC 掉该镜像的 layer 之后,如果不重启 registry 容器,当重新 PUSH 镜像 A 的时候就会提示镜像 layer 已经存在,不会重新上传 layer 但实际上已经被 GC 掉了,最终会导致镜像 A 不完整无法 pull 到该镜像。 (4) GC 不是事务性操作,所以在进行 GC 的时候最好暂停 PUSH 镜像,以免把正在上传的镜像 layer 给 GC 掉。Registry 仓库的 config.yaml 文件
# 配置版本 version: 0.1 # 日志配置日志系统的行为 log: # 访问日志系统的ACCESSLOG配置行为 accesslog: disabled: true # 日志输出的格式默认info.Permitted values are error, warn, info, debug level: debug # 日志输出的格式默认 text , json, and logstash formatter: text # 字段名称的地图 fields: service: registry environment: staging # 配置日志记录挂钩的行为 hooks: - type: mail disabled: true levels: - panic options: smtp: addr: mail.example.com:25 username: mailuser password: password insecure: true from: sender@example.com to: - errors@example.com # 日志等级(error, warn, info, debug):deprecated: use "log" loglevel: debug # 存储后端配置必须 storage: # 使用本地磁盘存储注册表文件 filesystem: rootdirectory: /var/lib/registry maxthreads: 100 azure: accountname: accountname accountkey: base64encodedaccountkey container: containername # Google Cloud Storage gcs: bucket: bucketname keyfile: /path/to/keyfile credentials: type: service_account project_id: project_id_string private_key_id: private_key_id_string private_key: private_key_string client_email: client@example.com client_id: client_id_string auth_uri: http://example.com/auth_uri token_uri: http://example.com/token_uri auth_provider_x509_cert_url: http://example.com/provider_cert_url client_x509_cert_url: http://example.com/client_cert_url rootdirectory: /gcs/object/name/prefix chunksize: 5242880 # Amazon Simple Storage Service (S3) s3: accesskey: awsaccesskey secretkey: awssecretkey region: us-west-1 regionendpoint: http://myobjects.local bucket: bucketname encrypt: true keyid: mykeyid secure: true v4auth: true chunksize: 5242880 multipartcopychunksize: 33554432 multipartcopymaxconcurrency: 100 multipartcopythresholdsize: 33554432 rootdirectory: /s3/object/name/prefix # Openstack Swift object storage. swift: username: username password: password authurl: https://storage.myprovider.com/auth/v1.0 or https://storage.myprovider.com/v2.0 or https://storage.myprovider.com/v3/auth tenant: tenantname tenantid: tenantid domain: domain name for Openstack Identity v3 API domainid: domain id for Openstack Identity v3 API insecureskipverify: true region: fr container: containername rootdirectory: /swift/object/name/prefix # Aliyun OSS for object storage oss: accesskeyid: accesskeyid accesskeysecret: accesskeysecret region: OSS region name endpoint: optional endpoints internal: optional internal endpoint bucket: OSS bucket encrypt: optional data encryption setting secure: optional ssl setting chunksize: optional size valye rootdirectory: optional root directory inmemory: # This driver takes no parameters # 使用delete结构允许通过摘要删除映像blob和清单 delete: enabled: false redirect: disable: false # 使用高速缓存结构,以便能够在存储后端访问的数据缓存。目前唯一可用的高速缓存提供对层的元数据,它使用blobdescriptor字段如果配置的快速访问。 cache: # 如果设置为Redis的,一个Redis的缓存池元数据层。如果设置为inmemory,一个inmemory地图缓存层的元数据。 blobdescriptor: redis # 维护维修 maintenance: # 上传清除 uploadpurging: enabled: true age: 168h interval: 24h dryrun: false # 如果在维护只读部分,使设置为true,客户端将不会被允许写入注册表。 readonly: enabled: false # 认证相关(只能配置一个身份验证提供者。) auth: silly: # 使用范围 realm: silly-realm service: silly-service # 令牌的认证您可以从registry中分离认证系统 token: autoredirect: true realm: token-realm service: token-service issuer: registry-token-issuer rootcertbundle: /root/certs/bundle # 支持htpasswd的认证允许您使用的是Apache的htpasswd文件来配置基本身份验证 htpasswd: realm: basic-realm # 唯一支持的密码格式是bcrypt。 path: /path/to/htpasswd middleware: registry: - name: ARegistryMiddleware options: foo: bar repository: - name: ARepositoryMiddleware options: foo: bar storage: - name: cloudfront options: baseurl: https://my.cloudfronted.domain.com/ privatekey: /path/to/pem keypairid: cloudfrontkeypairid duration: 3000s ipfilteredby: awsregion awsregion: us-east-1, use-east-2 updatefrenquency: 12h iprangesurl: https://ip-ranges.amazonaws.com/ip-ranges.json storage: - name: redirect options: baseurl: https://example.com/ # 报表 reporting: bugsnag: apikey: bugsnagapikey releasestage: bugsnagreleasestage endpoint: bugsnagendpoint newrelic: licensekey: newreliclicensekey name: newrelicname verbose: true # HTTP服务器主机上的注册表中的配置 http: addr: localhost:5000 prefix: /my/nested/registry/ host: https://myregistryaddress.org:5000 secret: asecretforlocaldevelopment relativeurls: false draintimeout: 60s tls: certificate: /path/to/x509/public key: /path/to/x509/private clientcas: - /path/to/ca.pem - /path/to/another/ca.pem letsencrypt: cachefile: /path/to/cache-file email: emailused@letsencrypt.com hosts: [myregistryaddress.org] debug: addr: localhost:5001 # 监控 prometheus: enabled: true path: /metrics # 它来指定报头的HTTP服务器应该在响应; headers: X-Content-Type-Options: [nosniff] http2: disabled: false # 通知公告 notifications: events: includereferences: true # 可以接受的事件通知的清单 endpoints: - name: alistener disabled: false url: https://my.listener.com/event headers: <http.Header> timeout: 1s threshold: 10 backoff: 1s ignoredmediatypes: - application/octet-stream ignore: mediatypes: - application/octet-stream actions: - pull # redis 相关配置 redis: addr: localhost:6379 password: asecret db: 0 dialtimeout: 10ms readtimeout: 10ms writetimeout: 10ms pool: maxidle: 16 maxactive: 64 idletimeout: 300s # 健康检查 health: storagedriver: enabled: true interval: 10s threshold: 3 # 文件结构包括要定期检查的路径的列表为一个文件的存在。如果文件存在于指定的路径,健康检查将失败。您可以使用这一机制通过创建一个文件,使注册表进行旋转。 file: - file: /path/to/checked/file interval: 10s http: - uri: http://server.to.check/must/return/200 headers: Authorization: [Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==] statuscode: 200 timeout: 3s interval: 10s threshold: 3 tcp: - addr: redis-server.domain.com:6379 timeout: 3s interval: 10s threshold: 3 proxy: remoteurl: https://registry-1.docker.io username: [username] password: [password] compatibility: schema1: signingkeyfile: /etc/registry/key.json enabled: true # 验证 validation: manifests: urls: allow: - ^https?://([^/]+.)*example.com/ deny: - ^https?://www.example.com/