本文使用「署名 4.0 国际 (CC BY 4.0)」许可协议,欢迎转载、或重新修改使用,但需要注明来源。 [署名 4.0 国际 (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/deed.zh) 本文作者: 苏洋 创建时间: 2021年04月13日 统计字数: 9826字 阅读时间: 20分钟阅读 本文链接: https://soulteary.com/2021/04/13/use-docker-to-build-a-simple-and-reliable-container-registry.html ----- # 使用容器搭建简单可靠的容器仓库 提到容器仓库,我们一般会想到 Nexus、Harbor ,那么有没有更轻量可靠的方案呢。尤其是在频繁构建的 CI 流水线中、或是分布式的环境中需要高频拉取镜像的场景中。 [《使用容器搭建 APT Cacher NG 缓存代理服务》](https://soulteary.com/2021/04/12/use-docker-to-build-apt-cacher-ng-caching-proxy-service.html)一文提到了缓存,虽然可以使用文末中的 Nginx 的补充方式来提供容器镜像导出文件的缓存托管,但是这种方式相比较使用镜像仓库而言,不能够直接使用 Docker Client 与之交互,需要借助导出和导入命令,使用起来颇有不便。 本篇文章继续聊聊,如何使用容器搭建轻量可靠的镜像仓库:distribution。 ## 写在前面 提起 [distribution](https://github.com/distribution/distribution) ,你可能会觉得陌生,但是如果下面几个大型的仓库都是由它为基础,你或许会对它信任会更多一些: - Docker Hub - GitHub Container Registry - GitLab Container Registry - DigitalOcean Container Registry - The CNCF Harbor Project - VMware Harbor Registry 没错,distribution 作为基础依赖存在于上面这些大名鼎鼎的社区(商业)仓库中。 如果你不需要更细粒度的镜像管理,只是需要对“数据”进行托管,或者说,只需要基础的镜像存储和推送服务,那么直接使用 distribution 或许是当下最轻量可靠的不二选择,如果你对于存储稳定性有顾虑,它原生支持公有云对象存储,可以保证数据存储的稳定和安全。 当然,如果你的需求除了 Docker 仓库之外,你还有 Maven/Java、npm、NuGet、Helm、Docker、P2、OBR、APT、GO、R 等软件包仓库的需求,distribution 是无法胜任的,可以选择开源仓库的扛把子 Nexus。感兴趣的话,可以参考早些时候[关于 Nexus 搭建和使用的内容](https://soulteary.com/tags/nexus.html)。 为了行文方便,我们假设这个提供服务的容器仓库的域名为 **docker.soulteary.cn**,这个域名可以被正确解析到我们启动服务的那台机器上(比如本地、或者某台云主机)。 参与演示的镜像,为了省事,我们选择搭建仓库使用的 distribution 镜像 `registry:2`,为了方便使用,提前使用 `docker pull registry:2` 下载至本地,并添加一个 `docker.soulteary.cn/registry:2` 的标签,确保我们后续能推送这个镜像到私有仓库。 ```bash docker pull registry:2 2: Pulling from library/registry 9b794450f7b6: Pull complete 6ba25693af03: Pull complete 9eb68e7589ff: Pull complete 6cf77150f665: Pull complete 339e0c26c7cc: Pull complete Digest: sha256:5bb9b919833aa955dfe1d1121cc038330b025ec6506ce47066c9192927e3dc3d Status: Downloaded newer image for registry:2 docker.io/library/registry:2 docker tag registry:2 docker.soulteary.cn/registry:2 ``` ## 系统环境准备 系统环境和上篇[《使用容器搭建 APT Cacher NG 缓存代理服务》](https://soulteary.com/2021/04/12/use-docker-to-build-apt-cacher-ng-caching-proxy-service.html)一样,我们只需要安装容器引擎和基础的编排工具即可。 如果你有参考上一篇文章进行缓存服务搭建,可以配置缓存服务来进行安装加速,如果没有,那么可以执行下面的命令,完成初始环境安装: ```bash apt update && apt upgrade -y && apt install -y apt-transport-https ca-certificates curl software-properties-common curl -fsSL https://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu/gpg | apt-key add - add-apt-repository "deb http://mirrors.tuna.tsinghua.edu.cn/docker-ce/linux/ubuntu $(lsb_release -cs) stable" apt install -y docker-ce curl -L https://github.com/docker/compose/releases/download/1.29.0/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose ``` 下面我将介绍几种不同的搭建方式,你可以根据你的需求来进行选择或者组合使用。 ## 配置无须身份验证的容器仓库 如果你只是需要在 CI 中使用,不考虑公开提供服务,将下面的配置保存为 `docker-compose.yml`,执行 `docker-compose up -d` 即可得到一个不需要身份认证即可使用的容器仓库(注册表服务),相关数据会被存储在 `registry` 目录中。 如果你需要更高的数据可靠性,可以[翻阅文档](https://github.com/distribution/distribution/blob/main/docs/configuration.md)中关于 `storage` 配置的章节,将存储配置到你认为可靠的公有云服务中,它目前支持 GCS、Azure、S3、Swift、OSS等多个服务商。如果在意私密性,也可以考虑使用 MinIO 搭建私密的 S3 对象存储服务。 ```yaml version: "3.0" services: registry: restart: always image: registry:2 ports: - 80:80 environment: REGISTRY_HTTP_ADDR: 0.0.0.0:80 REGISTRY_LOG_LEVEL: warn REGISTRY_LOG_ACCESSLOG_DISABLED: "true" volumes: - ./registry:/var/lib/registry logging: driver: "json-file" options: max-size: "1m" ``` Docker Client 默认访问注册表会使用 HTTPS 方式,如果我们想使用 HTTP 方式访问仓库,还需要在 Docker 的 daemon.json 配置文件中添加一项配置,告诉 Docker Client 在下载这个域名的镜像的时候不使用 HTTPS : ```json { "insecure-registries" : [ "docker.soulteary.cn"], ... } ``` 如果你的 deamon.json 中没有特别的设置,只需要添加这一个配置项,可以使用下面的命令快速进行设置: ```bash echo "{\"insecure-registries\" : [ \"docker.soulteary.cn\" ]}" >/etc/docker/daemon.json service docker restart ``` 如果你没有正确配置,并重启 Docker 服务的话,尝试拉取或者推送镜像的时候,会遇到类似下面的错误: ```TeXT docker pull docker.soulteary.cn/registry:2 Error response from daemon: Get https://docker.soulteary.cn/v2/: dial tcp 192.168.93.38:443: connect: connection refused ``` 当配置添加完毕之后,使用 `service docker restart` 重启服务,就可以正常的使用 `docker pull` 和 `docker push` 和仓库进行交互了。 ```bash docker push docker.soulteary.cn/registry:2 The push refers to repository [docker.soulteary.cn/registry] b2335c628697: Layer already exists 3cb95fe83bcd: Layer already exists d2ecc62f3d1a: Layer already exists 8e95b38dd51d: Layer already exists 2b2bcc6e6724: Layer already exists 2: digest: sha256:160c621b9bd98c4becce1c3b14e4866524dbe898d3af2e48d81fa1821b82c615 size: 1363 ``` ## 配置标准的 HTTPS 容器仓库 如果你不想在各种系统和 CI 中配置“insecure-registries”,并且能够得到服务的证书(购买、免费申请、自签名),那么可以使用下的配置,将服务运行于 443 端口,并提供 HTTPS 服务。 需要注意的是,如果你使用自签名证书,则发起调用的的系统需要配置信任自签证书。 ```yaml version: "3.0" services: registry: restart: always image: registry:2 ports: - 443:443 environment: REGISTRY_HTTP_ADDR: 0.0.0.0:443 REGISTRY_LOG_LEVEL: warn REGISTRY_LOG_ACCESSLOG_DISABLED: "true" REGISTRY_HTTP_TLS_CERTIFICATE: /ssl/soulteary.pem REGISTRY_HTTP_TLS_KEY: /ssl/soulteary.key volumes: - ./registry:/var/lib/registry - ./ssl:/ssl logging: driver: "json-file" options: max-size: "1m" ``` 这里的证书可以使用 `pem`、`crt`等合法的证书格式。如果使用这个模式,则可以将上一小节中对于 daemon.json 中的特殊配置去掉。 ## 使用 Nginx 配置同时支持两种协议的仓库 参考[官方配置](https://github.com/distribution/distribution/blob/main/docs/configuration.md)文档,我们知道 distribution 排除掉调试接口外,仅支持使用单个端口提供服务,所以如果我们想要同时支持 HTTP 和 HTTPS 就需要借助其他的软件,比如 Nginx、Traefik。 以 Nginx 为例,我们先进行容器编排配置的编写,将 Nginx 和 distribution 编排至同一网络: ```yaml version: "3.0" services: nginx: image: nginx:1.19.8-alpine restart: always ports: - 80:80 - 443:443 volumes: - /etc/localtime:/etc/localtime:ro - /etc/timezone:/etc/timezone:ro - ./ssl/:/etc/ssl/:ro - ./default.conf:/etc/nginx/conf.d/default.conf networks: - dockerhub healthcheck: test: ["CMD-SHELL", "wget -q --spider --proxy off localhost/get-health || exit 1"] interval: 10s timeout: 1s retries: 3 logging: driver: "json-file" options: max-size: "1m" registry: restart: always image: registry:2 networks: - dockerhub environment: REGISTRY_HTTP_ADDR: 0.0.0.0:80 REGISTRY_LOG_LEVEL: warn REGISTRY_LOG_ACCESSLOG_DISABLED: "true" volumes: - ./registry:/var/lib/registry logging: driver: "json-file" options: max-size: "1m" networks: dockerhub: ``` 在上面的容器网络中,对外提供服务的职责由 Nginx 承担,而 distribution 仅需要监听容器网络内部的请求即可。 接着继续完成 Nginx 配置文件 default.conf 的编写: ```bash server { listen 80; client_max_body_size 0; client_body_buffer_size 16k; access_log off; location / { add_header 'Docker-Distribution-Api-Version' 'registry/2.0'; proxy_pass http://registry:80; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto "http"; } location = /get-health { access_log off; default_type text/html; return 200 'alive'; } } server { listen 443 ssl; ssl_certificate /etc/ssl/black.com.pem; ssl_certificate_key /etc/ssl/black.com.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; client_max_body_size 0; client_body_buffer_size 16k; access_log off; location / { proxy_pass http://127.0.0.1; } } ``` 上面的配置中,我们主要做了几件事情: 1. 使用 Nginx 同时监听 80 和 443 端口,提供 HTTP 和 HTTPS 服务,而不是简单的使用端口转发,让同时支持两种客户端请求成为可能。 2. 将 Nginx 接收到的请求转发到 distribution 服务中,并在请求转发过程中,添加并携带应用所需要的 Header。 3. 对于 HTTPS 请求,在 Nginx 内部转发给 HTTP 请求,减少重复的代码配置,提升整体可维护性。 重启服务,你会发现现在仓库同时支持 HTTP 和 HTTPS 两种访问模式了。 ## 配置需要身份验证的容器仓库 如果我们不想要复杂的身份角色认证,但是还是期望有一些基础的身份验证,避免容器镜像被覆盖,或者被未授权下载,可以使用 `Auth Realm` 为仓库添加一层简单的,能够被 Docker Client 支持的身份认证。 我们以前文中“配置无须身份验证的容器仓库”的配置为例,只需要添加几行`REGISTRY_AUTH` 相关的环境变量即可开启基础的身份认证功能。 ```yaml version: "3.0" services: registry: restart: always image: registry:2 ports: - 80:80 environment: REGISTRY_HTTP_ADDR: 0.0.0.0:80 REGISTRY_LOG_LEVEL: warn REGISTRY_LOG_ACCESSLOG_DISABLED: "true" REGISTRY_AUTH: htpasswd REGISTRY_AUTH_HTPASSWD_PATH: /htpasswd REGISTRY_AUTH_HTPASSWD_REALM: Registry Realm volumes: - ./htpasswd:/htpasswd - ./registry:/var/lib/registry logging: driver: "json-file" options: max-size: "1m" ``` 细心的同学会看到,这里的认证使用了一个名为 `htpasswd` 的文件,如何生成这个文件呢?其实很简单,为了保证各平台兼容,我们可以使用 docker 镜像来一键生成。例如,生成一个用户名和密码都是 soulteary 的“认证信息”: ```bash docker run --rm -it httpd:alpine htpasswd -Bbn soulteary soulteary Unable to find image 'httpd:alpine' locally alpine: Pulling from library/httpd ca3cd42a7c95: Pull complete cd86b11706ae: Pull complete 2dd1cdb18fe6: Pull complete 7e270a528f77: Pull complete f781adcbe79a: Pull complete Digest: sha256:50ebd27377c8ac9f6ad44ae57b747b40ccac67bed59d448bf39514a9814b644d Status: Downloaded newer image for httpd:alpine soulteary:$2y$05$9HIf7tpwZZu7nD4TXGP4dONmlrTouHpC8ozMVyO6Vs5fmx1UJLlrS ``` 执行完毕上面的命令,docker 将会自动下载工具镜像,并计算出我们指定的用户名和密码的认证文件内容,上面日志中最后一行即是我们所需要的“认证信息”。 我们将 `soulteary: $2y$05$9HIf7tpwZZu7nD4TXGP4dONmlrTouHpC8ozMVyO6Vs5fmx1UJLlrS` 这个内容保存至 `htpasswd` 文件后,重启服务,进行简单的功能验证。 先尝试再下载一次镜像,会看到因为没有登陆,缺少凭证而无法下载镜像内容。 ```bash docker pull docker.soulteary.cn/registry:2 Error response from daemon: Head http://docker.soulteary.cn/v2/abc/registry/manifests/2: no basic auth credentials ``` 接着,使用刚刚指定的用户名和密码进行仓库登陆: ```bash docker login --username soulteary --password soulteary docker.soulteary.cn ``` 然后,尝试再次推送或者拉取镜像,会看到一切顺利: ```bash docker push docker.soulteary.cn/registry:2 The push refers to repository [docker.soulteary.cn/registry] b2335c628697: Layer already exists 3cb95fe83bcd: Layer already exists d2ecc62f3d1a: Layer already exists 8e95b38dd51d: Layer already exists 2b2bcc6e6724: Layer already exists 2: digest: sha256:160c621b9bd98c4becce1c3b14e4866524dbe898d3af2e48d81fa1821b82c615 size: 1363 docker pull docker.soulteary.cn/registry:2 2: Pulling from abc/registry Digest: sha256:160c621b9bd98c4becce1c3b14e4866524dbe898d3af2e48d81fa1821b82c615 Status: Image is up to date for docker.soulteary.cn/registry:2 docker.soulteary.cn/registry:2 ``` ## 切换使用 Nginx 提供仓库认证 虽然使用前文“使用 Nginx 配置同时支持两种协议的仓库”小节中的方式,也可以让容器仓库同时支持在 HTTP 和 HTTPS 模式下都能够支持认证功能。 但是考虑核心应用职责应该单一,以及后续服务扩展灵活,比如将认证服务外接之类的场景,更推荐的是使用 Nginx 来处理认证鉴权,让 distribution 只单纯负责容器镜像相关数据的 “CRUD”。 compose 配置同“使用 Nginx 配置同时支持两种协议的仓库”,这里不再赘述,我们仅需要调整 Nginx 配置文件即可: ```bash server { listen 80; client_max_body_size 0; client_body_buffer_size 16k; access_log off; location / { auth_basic "Registry realm"; auth_basic_user_file /etc/nginx/conf.d/nginx.htpasswd; add_header 'Docker-Distribution-Api-Version' 'registry/2.0'; proxy_pass http://docker-registry.black.com:80; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto "http"; } location = /get-health { access_log off; default_type text/html; return 200 'alive'; } } server { listen 443 ssl; ssl_certificate /etc/ssl/black.com.pem; ssl_certificate_key /etc/ssl/black.com.key; ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_ciphers HIGH:!aNULL:!MD5; client_max_body_size 0; client_body_buffer_size 16k; access_log off; location / { proxy_pass http://127.0.0.1; } } ``` 在配置中添加 “`auth_basic`” 配置后,重启服务,就可以完成认证功能的职责切换啦。 ## 最后 关于容器镜像仓库先聊到这里。 如果你在生产使用,再次提醒,建议搭配支持 S3 协议的对象存储一起使用,让生产数据更安全。 --EOF