这篇文章内容包括搭建 docker 私有仓库的一些配置项和遇到的问题及解决方案。

1
2
3
4
5
6
7
8
9
10
11
12
1.配置项
1.1\\. 数据持久化
1.2\\. TLS 支持
1.3\\. 登录授权验证
1.4\\. docker compose
2\\. 测试
3\\. NGINX做代理
3.1\\. 我的方式和遇到的问题
3.2\\. NGINX 作为一个容器
4\\. 其它方案
5\\. 相关链接

Docker 官方提供了 registry 镜像, 可以方便的搭建私有仓库,详细文档参考这里

配置项

数据持久化

可以通过采用数据卷挂载或者直接挂载宿主机目录的方式来进行。挂载到容器内默认位置: /var/lib/registry
比如可以像如下方式启动, 这里将容器数据存储在了 /mnt/registry.

1
2
3
4
5
6
7
$ docker run -d \\
-p 5000:5000 \\
--restart=always \\
--name registry \\
-v /mnt/registry:/var/lib/registry \\
registry:2

当然,镜像还提供了其它支持的存储方式,比如 OSS 等。

官方文档参见这里

TLS 支持

为了使得私有仓库安全地对外开放,需要配置 TLS 支持。

测试的时候,如果不配置的话 TLS,可以在 docker 客户端中的 “insecure registry” 里添加私有仓库地址,不然默认的都以安全的 tsl 方式来访问私有仓库,具体更改方式可以参考这里

我的 CA 证书是从阿里云获取的(因为域名是在上面注册的,可以提供免费的证书,虽然如果做得很隐蔽)。

registry 镜像可以通过 REGISTRY_HTTP_TLS_CERTIFICATEREGISTRY_HTTP_TLS_KEY 环境参数配置 TLS 支持。
例如下面这样, domain.crtdomain.key 是获得的证书,另外配置容器监听 ssl 默认的 443 端口。

1
2
3
4
5
6
7
8
9
10
$ docker run -d \\
--restart=always \\
--name 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 \\
-p 443:443 \\
registry:2

官方文档参见这里

登录授权验证

可以通过 htpasswd 来配置简单的 authentication (注意:验证需要 TLS 支持)。

首先在 auth 目录下通过 reistry 里的 htpasswd 工具创建 验证文件 auth/htpasswd

1
2
3
4
5
$ mkdir auth
$ docker run \\
--entrypoint htpasswd \\
registry:2 -Bbn testuser testpassword > auth/htpasswd

启动的时候通过 REGISTRY_AUTH, REGISTRY_AUTH_HTPASSWD_REALM, REGISTRY_AUTH_HTPASSWD_PATH 来配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ docker run -d \\
-p 5000:5000 \\
--restart=always \\
--name registry \\
-v `pwd`/auth:/auth \\
-e "REGISTRY_AUTH=htpasswd" \\
-e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \\
-e REGISTRY_AUTH_HTPASSWD_PATH=/auth/htpasswd \\
-v `pwd`/certs:/certs \\
-e REGISTRY_HTTP_TLS_CERTIFICATE=/certs/domain.crt \\
-e REGISTRY_HTTP_TLS_KEY=/certs/domain.key \\
registry:2

这样就启动了一个监听 5000 端口的、支持 TLS 和简单登录验证的 docker 私有仓库。

官方文档参见这里

docker compose

“docker compose” 是一个方便定义和运行多个容器的工具, 安装参见这里, 或者通过 pip 安装: pip install docker-compose

以上配置项通过 docker compose 的方式组织起来如下:

文件命名成 docker-compose.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
volumes:
- /path/data:/var/lib/registry
- /path/certs:/certs
- /path/auth:/auth

docker-compose.yaml 所在目录运行:

1
2
docker-compose up

测试

私有仓库搭建好了如何测试?

1
2
3
4
5
6
7
8
9
10
11
12
# 先拉取官方镜像
$ docker pull ubuntu:16.04

# 打上标签
$ docker tag ubuntu:16.04 myregistrydomain.com/my-ubuntu

# 推到私有仓库
$ docker push myregistrydomain.com/my-ubuntu

# 从私有仓库获取
$ docker pull myregistrydomain.com/my-ubuntu

Nginx 做代理

我的方式和遇到的问题

实际配置中,我采用了 nginx 作为代理,来访问 registry 服务。我将 TLS 支持和登录验证都加到了 nginx 一层。

nginx 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
upstream docker-registry {
server localhost:5000; # !转发到registry 监听的5000 端口!
}

## Set a variable to help us decide if we need to add the
## 'Docker-Distribution-Api-Version' header.
## The registry always sets this header.
## In the case of nginx performing auth, the header is unset
## since nginx is auth-ing before proxying.
map $upstream_http_docker_distribution_api_version $docker_distribution_api_version {
'' 'registry/2.0';
}

server {
listen 443 ssl;
server_name domain.com; # !这里配置域名!

# SSL
ssl_certificate /path/to/domain.pem; # !这里配置CA 证书信息!
ssl_certificate_key /path/to/domain.key; # !这里配置CA 证书信息!

# Recommendations from <https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html>
ssl_protocols TLSv1.1 TLSv1.2;
ssl_ciphers 'EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH';
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;

# disable any limits to avoid HTTP 413 for large image uploads
client_max_body_size 0;

# required to avoid HTTP 411: see Issue #1486 (<https://github.com/moby/moby/issues/1486>)
chunked_transfer_encoding on;

location /v2/ {
# Do not allow connections from docker 1.5 and earlier
# docker pre-1.6.0 did not properly set the user agent on ping, catch "Go *" user agents
if ($http_user_agent ~ "^(docker\\/1\\.(3|4|5(?!\\.[0-9]-dev))|Go ).*$" ) {
return 404;
}

# To add basic authentication to v2 use auth_basic setting.
auth_basic "Registry realm";
auth_basic_user_file /path/to/auth/htpasswd; # !这里配置auth文件位置!

## If $docker_distribution_api_version is empty, the header is not added.
## See the map directive above where this variable is defined.
add_header 'Docker-Distribution-Api-Version' $docker_distribution_api_version always;

proxy_pass <http://docker-registry>;
proxy_set_header Host $http_host; # required for docker client's sake
proxy_set_header X-Real-IP $remote_addr; # pass on real client's IP
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 900;
}
}

其中 /path/to/auth/htpasswd 文件是通过 registry 中的或者本地的 htpasswd 工具生成的

1
2
3
4
$ docker run \\
--entrypoint htpasswd \\
registry:2 -Bbn testuser testpassword > auth/htpasswd

registry 的 docker-compose 文件:

1
2
3
4
5
6
7
8
9
10
version: '2'
services:
my_registry:
restart: always
image: registry:2
ports:
- 127.0.0.1:5000:5000
volumes:
- ./data:/var/lib/registry

启动后,当我从本地视图 login 到私有仓库时,发生错误:

1
2
3
4
5
➜  ~ docker login domain.com
Username: testuser
Password:
Error response from daemon: login attempt to <https://hub.docker.equiz.cn/v2/> failed with status: 500 Internal Server Error

查看日志发现 nginx 错误日志里有个编码相关的错误:

1
2
3
4
# nginx error log

*4 crypt_r() failed (22: Invalid argument)

经过一番研究,发现之前加密时,是采用 Bcrypt 加密方式,看下 htpasswd 的使用说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
root@data1:~# htpasswd
Usage:
htpasswd [-cimBdpsDv] [-C cost] passwordfile username
htpasswd -b[cmBdpsDv] [-C cost] passwordfile username password

htpasswd -n[imBdps] [-C cost] username
htpasswd -nb[mBdps] [-C cost] username password
-c Create a new file.
-n Don't update file; display results on stdout.
-b Use the password from the command line rather than prompting for it.
-i Read password from stdin without verification (for script usage).
-m Force MD5 encryption of the password (default).
-B Force bcrypt encryption of the password (very secure).
-C Set the computing time used for the bcrypt algorithm
(higher is more secure but slower, default: 5, valid: 4 to 31).
-d Force CRYPT encryption of the password (8 chars max, insecure).
-s Force SHA encryption of the password (insecure).
-p Do not encrypt the password (plaintext, insecure).
-D Delete the specified user.
-v Verify password for the specified user.
On other systems than Windows and NetWare the '-p' flag will probably not work.
The SHA algorithm does not use a salt and is less secure than the MD5 algorithm.

可以看到 -B 会使用 bcrypt 的方式来加密,nginx 默认不支持。至于如何让 nginx 支持 bcrypt 我暂时还未找到方案,留待以后研究了 (TODO)

简单的解决方式是换成默认的 MD5 加密(因为安全等级问题又不推荐不用 bcrypt 方式的),

1
2
docker run --rm --entrypoint htpasswd registry:2 -bn testuser testpassword > auth/htpasswd   # 这里少了 -B 选项

关于 bcrypt 加密方式,这里 有一篇不错的文章介绍。不过好像对于这个加密方式,网上有一些争论,我就不详究了。

不依赖 “apche tools” 的 nginx 加密方式参考 这里, 比如 MD5 加密:

1
2
printf "testuser:$(openssl passwd -1 testpassword)\\n" >> .htpasswd # this example uses MD5 encryption

Nginx 作为一个容器

docker 文档也有如何采用 nginx 容器和 registry 配合使用的说明,参考这里

“docker-compose.yaml” 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
nginx:
# Note : Only nginx:alpine supports bcrypt.
# If you don't need to use bcrypt, you can use a different tag.
# Ref. <https://github.com/nginxinc/docker-nginx/issues/29>
image: "nginx:alpine" # !这里一定要采用alpine镜像,因为它里的nginx支持 bcrypt 加密!
ports:
- 5043:443
links:
- registry:registry
volumes:
- ./auth:/etc/nginx/conf.d
- ./auth/nginx.conf:/etc/nginx/nginx.conf:ro

registry:
image: registry:2
ports:
- 127.0.0.1:5000:5000
volumes:
- ./data:/var/lib/registry

这里 nginx 容器监听的是 5043, 所以使用的私有仓库的地址变成了 registrydomain.com:5043,
我不喜欢后面加端口,但本机又存在其他需要 nginx 监听的 443 端口的服务 (不同 doamin 下),所以不能让 nginx 容器直接监听 443 端口,故没有采用这种方式。

另外在寻找解决方案的时候发现一个镜像 nginx-proxy, 可以方便地监听新添加的容器,留待以后探索。

其它方案

或许你想用 letsencrypts 免费证书,不妨看看这个工具:acme.sh

或许你不满足的简易方案,你可能还需要 web 界面来方便查看和管理你的镜像仓库,那么你可以查看下企业级的容器仓库方案: vmware/harbor

相关链接