一、背景
首先,Docker Hub是一个很好的用于管理公共镜像的地方,我们可以在上面找到想要的镜像(Docker Hub的下载量已经达到数亿次);而且我们也可以把自己的镜像推送上去。但是,有的时候,使用场景需要我们有一个私有的镜像仓库用于管理自己的镜像,这个时候我们就通过Registry来实现此目的。本文详细介绍了本地镜像仓库Docker Registry & Portus的搭建过程。
Registry作为Docker的核心组件之一负责镜像内容的存储与分发,客户端的docker pull以及push命令都将直接与registry进行交互。最初版本的registry 由Python实现。由于设计初期在安全性,性能以及API的设计上有着诸多的缺陷,该版本在0.9之后停止了开发,新的项目distribution(新的docker register被称为Distribution,你可以在这里找到文档 。)来重新设计并开发下一代registry。新的项目由go语言开发,所有的API,底层存储方式,系统架构都进行了全面的重新设计已解决上一代registry中存在的问题。2016年4月份rgistry 2.0正式发布,docker 1.6版本开始支持registry 2.0,而八月份随着docker 1.8 发布,docker hub正式启用2.1版本registry全面替代之前版本 registry。新版registry对镜像存储格式进行了重新设计并和旧版不兼容,docker 1.5和之前的版本无法读取2.0的镜像。
另外,Registry 2.4版本之后支持了回收站机制,也就是可以删除镜像了。在2.4版本之前是无法支持删除镜像的,所以如果你要使用最好是大于Registry 2.4版本的。
二、Registry V2的变化
Docker build镜像时会为每个layer生成一串layer id,这个layer id是一个客户端随机生成的字符串,和镜像内容无关。我们可以通过一个简单的例子来查看。提供一个简单的dockerfile。
1 2 3 4 5 |
$ cat dockerfile FROM nginx MAINTAINER dkey EXPOSE 80 ENTRYPOINT nginx -g "daemon off;" |
加上--no-cache
强制重新build。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
$ docker build --no-cache=true -t nginx:dockerfile . Sending build context to Docker daemon 230.9 MB Step 1 : FROM nginx ---> 19146d5729dc Step 2 : MAINTAINER dkey ---> Running in e3f81ad4b150 ---> 7164bae33eb7 Removing intermediate container e3f81ad4b150 Step 3 : EXPOSE 80 ---> Running in 7e73d2d24587 ---> 3610eda3790c Removing intermediate container 7e73d2d24587 Step 4 : ENTRYPOINT nginx -g "daemon off;" ---> Running in 1733ff25370e ---> 60792ac79d11 Removing intermediate container 1733ff25370e Successfully built 60792ac79d11 |
接下来再次build。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ docker build -t nginx:dockerfile . Sending build context to Docker daemon 230.9 MB Step 1 : FROM nginx ---> 19146d5729dc Step 2 : MAINTAINER dkey ---> Using cache ---> 7164bae33eb7 Step 3 : EXPOSE 80 ---> Using cache ---> 3610eda3790c Step 4 : ENTRYPOINT nginx -g "daemon off;" ---> Using cache ---> 60792ac79d11 Successfully built 60792ac79d11 |
可以看到使用cache层的layer id一致,其余layer的id都发生了变化。这种随机layer id以及layer id与内容无关的设计会带来很多的问题。
首先, registry v1通过id来判断镜像是否存在,客户需不需要重新push,而由于镜像内容和id无关,再重新build后layer在内容不变的情况下很可能id发生变化,造成无法利用registry中已有layer反复push相同内容。服务器端也会有重复存储造成空间浪费。
其次,尽管id由32字节组成但是依然存在id碰撞的可能,在存在相同id的情况下,后一个layer由于id和仓库中已有layer相同无法被push到registry中,导致数据的丢失。用户也可以通过这个方法来探测某个id是否存在。
最后,同样是由于这个原因如果程序恶意伪造大量layer push到registry中占位会导致新的layer无法被push到registry中。Docker官方重新设计新版registry的一个主要原因也就是为了解决该问题。
新版的registry吸取了旧版的教训,在服务器端会对镜像内容进行哈希,通过内容的哈希值来判断layer在registry中是否存在,是否需要重新传输。这个新版本中的哈希值被称为digest是一个和镜像内容相关的字符串,相同的内容会生成相同的digest。由于digest和内容相关,因此只要重新build的内容相同理论上讲无需重新push,但是由于安全性的考量在特定情况下layer依然要重新传输。由于layer是按digest进行存储,相对v1按照随机id存储可以大幅减小磁盘空间占用。registry服务端会对冲突digest进一步进行处理,同时由于digest是由registry服务端生成,用户无法伪造digest也很大程度上保证了registry内容的安全性。
1)安全性改进
除了对image内容进行唯一性哈希外,新版registry还在鉴权方式以及layer权限上上进行了大幅度调整。鉴权方式:
旧版本的服务鉴权模型如下图所示:
该模型每次client端和registry的交互都要多次和index打交道,新版本的鉴权模型去除了上图中的第四第五步,如下图所示:
新版本的鉴权模型需要registry和authorization service在部署时分别配置好彼此的信息,并将对方信息作为生成token的字符串,已减少后续的交互操作。新模型客户端只需要和authorization service进行一次交互获得对应token即可和registry进行交互,减少了复杂的流程。同时registry和authorization service一一对应的方式也降低了被攻击的可能。
2)权限控制
旧版的registry中对layer没有任何权限控制,所有的权限相关内容都由index完成。在新版registry中加入了对layer的权限控制,每个layer都有一个manifest来标识该layer由哪些repository共享,将权限做到repository级别。
3)Pull性能改进
旧版registry中镜像的每个layer都包含一个ancestry的json文件包含了父亲layer的信息,因此当我们pull镜像时需要串行下载,下载完一个layer后才知道下一个layer的id是多少再去下载。如下图所示:
新版registry在image的manifest中包含了所有layer的信息,客户端可以并行下载所有的layer如下图所示:
4)其他改进
– 全新的API。
– push和pull支持断点。
– 后端存储的插件。
– notification机制。
– 支持删除镜像,有了回收站机制。
三、安装配置Registry
直接下载 Registry 镜像。
1 |
$ docker pull registry:2.4.1 |
v2.4.1版本的registry是把image文件放到了/var/lib/registry下。
最简单方式启动,启动一个registry是很容易的,如下:
1 2 3 4 5 |
# 下载Registry镜像; $ docker pull registry:2.4.1 # 启动Registry; $ docker run -d -p 5000:5000 --restart=always --name registry --privileged=true -v /data/:/var/lib/registry registry:2.4.1 |
--name
:指定容器名称。
--privileged=true
:CentOS7中的安全模块selinux把权限禁掉了,参数给容器加特权,不加上传镜像会报权限错误。
这里指定了一个/var/lib/registry的卷,是为了把真实的镜像数据储存在主机上,而别在容器挂掉之后丢失数据。就算这样,也还是不保险。要是主机挂了呢?Docker官方建议可以放到ceph 、 swift这样的存储里,或是亚马逊S3 、微软Azure 、谷歌GCS 、阿里云OSS之类的云商那里。Docker registry提供了配置文件,可以从容器里复制出来查看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
$ docker cp registry:/etc/docker/registry/config.yml /data/config.yml $ cat /data/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 |
配置文件里有一个storage ,按照这里写的配置,然后执行以下命令重新挂载这个文件来启动registry就可以了,有条件的话可以去试一试:
1 2 3 4 5 6 7 |
$ docker rm -fv registry $ docker run -d -p 5000:5000 \ --restart=always \ --name registry \ -v /data/:/var/lib/registry \ -v /data/config.yml:/etc/docker/registry/config.yml \ registry:2.4.1 |
Docker Registry配置完了,然后可以在本机通过docker push 127.0.0.1:5000/xxx的方式推送镜像到registry中(推送镜像必须使用docker images可查看)。
1 2 |
$ docker tag wordpress 127.0.0.1:5000/wordpress $ docker push 127.0.0.1:5000/wordpress |
但是只能在本地使用127.0.0.1进行推送,不能在其他主机push镜像,包括本机通过IP地址也不可以推送镜像。当在其他主机或者在本机通过IP推送镜像时,docker默认会认为地址是HTTPS加密的,而实际上我们启动registry时并没有加密,所以会报错。如下:
1 2 3 4 |
$ docker tag wordpress 10.99.73.10:5000/wordpress $ docker push 10.99.73.10:5000/wordpress The push refers to a repository [172.17.0.1:5000/wordpress] Get https://172.17.0.1:5000/v1/_ping: http: server gave HTTP response to HTTPS client |
解决方案
第一种:在需要推送镜像的服务器上修改dockerd启动参数【官方资料】,然后重启docker。再推送镜像时就会认为这个地址是HTTP,不会报错了,但在每一台主机添加这个配置是很麻烦和危险的。另外我参照官方的做法和网上的做法根本没有办法解决。后来就在网上翻了很久找到了一个解决办法,在docker host端的/etc/docker目录下添加一个daemon.json文件,内容如下:
1 2 |
$ cat /etc/docker/daemon.json { "insecure-registries":["10.99.73.10:5000"] } |
然后重启docker,就OK了。
1 |
$ systemctl restart docker |
如果有多个地址,可以这么写。
1 |
{ "insecure-registries":["10.99.73.10:5000","10.106.201.12:5000"] } |
再次PUSH镜像就成功了。
1 2 3 4 5 6 7 |
$ docker tag nginx 10.99.73.10:5000/nginx $ docker push 10.99.73.10:5000/nginx The push refers to a repository [10.99.73.10:5000/nginx] bc1394447d64: Pushed 6591c6f92a7b: Pushed f96222d75c55: Mounted from wordpress latest: digest: sha256:dedbce721065b2bcfae35d2b0690857bb6c3b4b7dd48bfe7fc7b53693731beff size: 948 |
第二种:自建证书,让 Register 以TLS的方式启动,【官方资料】
1. 创建你自己的CA证书
安装之前首先需要在 Register 服务器上创建ca证书和私钥,以及签发Harbor要使用的证书和私钥。假设你的 register 要访问的域名 dockerhub.ywnds.com,并且其DNS记录指向你正在运行 Register 的主机。你首先应该从CA获得一个证书。证书通常包含 .crt 文件和 .key 文件,例如 ywnds.com.crt 和 ywnds.com.key。创建你自己的CA证书,我这里使用的是私钥和证书一起生成,你也可以先生成私钥然后再生成证书。
1 2 3 4 5 6 7 8 |
$ openssl req -newkey rsa:4096 -nodes -sha256 -keyout /data/cert/ca.key -x509 -days 365 -out /data/cert/ca.crt Country Name (2 letter code) [XX]:cn State or Province Name (full name) []:sh Locality Name (eg, city) [Default City]:sh Organization Name (eg, company) [Default Company Ltd]:ca Organizational Unit Name (eg, section) []:ca Common Name (eg, your name or your server's hostname) []:10.99.73.10 Email Address []:admin@ca.com |
2. 生成Register证书签名请求
如果使用像 dockerhub.ywnds.com 这样的FQDN连接register主机,则必须使用 dockerhub.ywnds.com 作为CN(Common Name)。否则,如果你使用IP地址连接你的 register 主机,CN可以是任何类似你的名字等等:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
$ openssl req -newkey rsa:4096 -nodes -sha256 -keyout /data/cert/ywnds.com.key -out /data/cert/ywnds.com.csr Country Name (2 letter code) [XX]:cn State or Province Name (full name) []:sh Locality Name (eg, city) [Default City]:sh Organization Name (eg, company) [Default Company Ltd]:ywnds Organizational Unit Name (eg, section) []:tech Common Name (eg, your name or your server's hostname) []:10.99.73.10 Email Address []:admin@ywnds.com Please enter the following 'extra' attributes to be sent with your certificate request A challenge password []: An optional company name []: |
3. 生成Register主机的证书
如果你使用的是像 dockerhub.ywnds.com 这样的FQDN来连接您的register主机,请运行以下命令以生成register主机的证书:
1 2 3 4 5 6 |
$ openssl x509 -req -days 365 \ -in /data/cert/ywnds.com.csr \ -CA /data/cert/ca.crt \ -CAkey /data/cert/ca.key \ -CAcreateserial \ -out /data/cert/ywnds.com.crt |
如果你使用的是IP,比如你的register主机 10.99.73.10,你可以运行下面的命令生成证书:
1 2 3 4 5 6 7 8 |
$ echo subjectAltName = IP:10.99.73.10 > extfile.cnf $ openssl x509 -req -days 365 \ -in /data/cert/ywnds.com.csr \ -CA /data/cert/ca.crt \ -CAkey /data/cert/ca.key \ -CAcreateserial \ -extfile extfile.cnf \ -out /data/cert/ywnds.com.crt |
启动register:
1 2 3 4 5 6 7 8 9 10 |
$ docker rm -fv registry $ docker run -d \ -p 5000:5000 \ --restart=always \ --name registry \ -v /data/cert/:/cert \ -v /data/docker/registry:/var/lib/registry \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/cert/ywnds.com.crt \ -e REGISTRY_HTTP_TLS_KEY=/cert/ywnds.com.key \ registry:2.4.1 |
启动后访问会报错:certificate signed by unknown authority,因为这是个自签名的证书(没有经过CA签证的)。docker在验证TLS时会自动读取这个目录下的证书。然后重启docker即可。
为Docker配置服务器证书,密钥和CA
在具有Docker守护进程的机器上,确保选项--insecure-registry
不存在,并且你必须将上述步骤中生成的ca.crt复制到/etc/docker/certs.d/reg.yourdomain.com(或你的register主机IP),如果该目录不存在,请创建它。如果你将nginx端口443映射到另一个端口,则应该创建目录/etc/docker/certs.d/reg.yourdomain.com:port(或你的 register 主机IP:端口)。
Docker守护程序将 .crt 文件解释为CA证书,将 .cert 文件解释为客户端证书。
将服务器 ywnds.com.crt 转换为 ywnds.com.cert:
1 |
$ openssl x509 -inform PEM -in /data/cert/ywnds.com.crt -out /data/cert/ywnds.com.cert |
上传 ywds.com.cert,ywnds.com.key和ca.crt 文件到Docker客户端,如下操作:
1 |
$ mkdir /etc/docker/certs.d/10.99.73.10:443 |
以下说明了使用自定义证书的配置结构(测试只有ca.crt证书也可行):
1 2 3 4 5 |
/etc/docker/certs.d/ └── 10.99.73.10:443 ├── ywnds.com.cert <-- CA签名的服务器证书 ├── ywnds.com.key <-- CA签名的服务器密钥 └── ca.crt <-- CA证书 |
请注意,你可能需要在操作系统级别信任该证书。
此时再push就ok了,如下:
1 2 3 4 |
$ docker push 10.99.73.10:5000/wordpress The push refers to a repository [10.99.73.10:5000/wordpress] 2ff5b2ab6416: Layer already exists .............. |
如果报 cannot validate certificate for 10.99.73.10 because it doesn’t contain any IP SANs 错误,检查一下ca.crt证书是否正确,以及生成register主机的证书的时候使用的是域名还是IP,其方式是否正确。
至此, docker registry私有仓库安装成功。但是还是有些缺点:只要有了证书,还是谁都可以往库里推镜像。简单的解决方案就是使用用户认证。
四、操作Registry镜像
下面都是以http方式访问,如果你加了证书就需要使用https进行访问了。
1)列出当前所有镜像
1 2 |
$ curl http://10.99.73.10:5000/v2/_catalog {"repositories":["busybox_1","nginx","wordpress"]} |
2)列出当前指定镜像
1 |
$ curl http://10.99.73.10:5000/v2/_catalog?n=100 |
3)搜索镜像
1 2 |
$ curl http://10.99.73.10:5000/v2/wordpress/tags/list {"name":"wordpress","tags":["latest"]} |
4)确认Registry是否正常工作
1 2 |
$ curl http://10.99.73.10:5000/v2/ {} |
返回{}就表示正常工作。
5)删除镜像
Docker仓库在2.1版本中支持了删除镜像的API,但这个删除操作只会删除镜像元数据,不会删除层数据。在2.4版本中对这一问题进行了解决,增加了一个垃圾回收命令,删除未被引用的层数据。但有一些条件限制,具体操作步骤如下:
启动仓库容器
1 2 3 4 5 6 7 |
$ docker run -d \ -p 5000:5000 \ --restart=always \ --name registry \ -v /data/:/var/lib/registry \ -v /data/config.yml:/etc/docker/registry/config.yml \ registry:2.4.1 |
这里需要说明一点,在启动仓库时,需在配置文件中的storage配置中增加delete=true配置项,允许删除镜像,本次试验采用如下配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
version: 0.1 log: fields: service: registry storage: delete: enabled: true 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 |
查看数据进行仓库容器中,通过du命令查看大小,可以看到当前仓库数据大小为339M。
1 2 |
$ du -sh /data/docker/registry/v2/ 339M /data/docker/registry/v2/ |
删除镜像对应的API如下:
1 |
DELETE /v2/<name>/manifests/<reference> |
name:镜像名称。
reference:镜像对应sha256值。
首先查看要删除镜像的sha256
1 2 |
$ ls /data/docker/registry/v2/repositories/wordpress/_manifests/revisions/sha256/ 4eefa1b7fdce1b6e6953ca18b6f49a68c541e9e07808e255c3b8cc094ff085da |
进行删除操作
1 2 3 4 5 6 7 |
$ curl -I -X DELETE http://10.99.73.10:5000/v2/wordpress/manifests/sha256:4eefa1b7fdce1b6e6953ca18b6f49a68c541e9e07808e255c3b8cc094ff085da HTTP/1.1 202 Accepted Docker-Distribution-Api-Version: registry/2.0 X-Content-Type-Options: nosniff Date: Thu, 15 Dec 2016 06:27:19 GMT Content-Length: 0 Content-Type: text/plain; charset=utf-8 |
执行垃圾回收
命令:registry garbage-collect config.yml
1 2 |
$ docker exec -ti registry bash root@ef45a8a624c1:/# registry garbage-collect /etc/docker/registry/config.yml |
再看数据大小
1 2 |
$ du -sh /data/docker/registry/v2/ 88K /data/docker/registry/v2/ |
可以看到镜像数据已被删除,从339M变成了88K。
PS:尝试过直接在目录中把镜像删除,然后重启docker daemon,此镜像也会删除。
下载镜像
1 |
$ docker pull 10.99.73.10:5000/wordpress |
PS:注意后面还可以跟上tags,默认就是latest。
五、用户认证
首先在registry生成用户名hello和密码world:
1 2 |
$ mkdir /data/auth $ sh -c "docker run --entrypoint htpasswd registry:2.4.1 -Bbn hello world > /data/auth/htpasswd" |
还得指定认证方式和认证文件等参数,重新启动registry容器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
$ docker rm -f registry $ docker run -d \ -p 5000:5000 \ --name registry \ --restart=always \ -v /data/docker/:/var/lib/registry \ -v /data/auth:/auth \ -v /data/cert:/cert \ -e REGISTRY_HTTP_TLS_CERTIFICATE=/cert/ywnds.com.crt \ -e REGISTRY_HTTP_TLS_KEY=/cert/ywnds.com.key \ -e REGISTRY_AUTH=htpasswd \ -e REGISTRY_AUTH_HTPASSWD_REALM="Registry Realm" \ -e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \ registry:2.4.1 |
再次push就会失败啦。
1 2 3 4 |
$ docker push 10.99.73.10:5000/wordpress The push refers to a repository [10.99.73.10:5000/wordpress] ........ no basic auth credentials |
但是我们可以用用户名hello和密码world登录,然后在进行push:
1 2 |
$ docker login -u hello -p world 10.99.73.10:5000 Login Succeeded |
登录成功后,再次push就会成功了。如果想退出登录,使用logout即可。
1 2 |
$ docker logout 10.99.73.10:5000 Remove login credentials for 10.99.73.10:5000 |
Docker私有仓库到这里就结束了,个人感觉还是有很多不足。有兴趣可以看看: