一、背景
首先,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
|
$ 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证书
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. 生成证书签名请求
如果使用像dockerhub.ywnds.com这样的FQDN连接register主机,则必须使用dockerhub.ywnds.com作为CN(通用名称)。否则,如果你使用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
|
$ 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
|
$ 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.crt复制到客户端/etc/docker/certs.d/${registry}:${port}/ca.crt(${registry}是域名或你的register主机IP),如果该目录不存在,请创建它。客户端操作如下,需要把此证书复制到客户端即可(更名为ca.crt),操作如下:
1
2
|
$ mkdir -p /etc/docker/certs.d/10.99.73.10:5000
$ scp /data/certs/docker.crt 10.99.73.9:/etc/docker/certs.d/10.99.73.10:5000/ca.crt
|
此时再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私有仓库到这里就结束了,个人感觉还是有很多不足。有兴趣可以看看: