参照Docker的官方文档,搭建一套Docker Registry服务,对于很多人来讲是很容易的一件事情。为什么我要单独拿出来说,这个过程又遇到了什么坑爹的事儿呢?

场景描述

首先描述下我们的场景。我们的Docker Registry服务器有公网IP,但由于某种限制,我们对外的80、443端口被屏蔽。(你想到了什么?嗯,对了)

对于docker而言,除非手动加入白名单,否则默认都是通过HTTPS协议(443端口)去指定的地址访问镜像的。比如对于镜像 myhost.com/user1/repo 来说。就会默认访问https://myhost.com 来获取镜像信息。当然您可以在镜像地址中手动加上端口号来强制docker走指定端口。但这显然不是我们想要的,谁想弄一个又长又有端口号的地址在镜像里呢?

如果你正好和我们的想法一样,恭喜你,这篇文章就是为你准备的。那么我们的解决方案是什么呢?答案就是——

解决方案——CDN

听起来是不是有一种“还特么要你说,老子也知道,然并卵”的感觉。为什么会有这种感觉呢?

  1. 大多数CDN回源是同端口或者要求是标准端口(80、443)。
  2. CDN是按流量收费的,这种方案的流量费可不少。

正是因为这两个问题,所以很多人知道这方案,然而很少人使用这方案。那么我们准备怎么解决这两个问题呢?

首先,回源端口的问题,其实很多CDN服务商是不限制回源端口的,尤其是非协议跟随的情况下。比如腾讯云。当然你可以找到更多的案例。

其次,CDN按流量收费,镜像直接托管的话,流量会非常高。所以我们用CDN时,源站只会返回HTTP重定向,让docker自动重新直接去源站拉取。而不是直接由CDN从源站获取数据并返回。

听起来还是很简单,无非就是一个HTTP Redirect嘛。如果你是这样想的,那么可以不要继续往下看,自己去折腾折腾试试。为了方便,我会在这里留很多空行给你思考下……

相信尝试过的人,就知道看起来简单的事情,往往蕴涵着很多坑。这些坑没有踩过就觉得一马平川、踩过了就会觉得不值一提。但是踩的过程却是酸甜苦辣。作为一个踩过的人,卖了这么多空行的关子,我就不继续卖了,直接列出所有的坑:

Docker镜像相关的请求不仅仅是GET

还顺道把HTTP其他的POST、PUT、PATCH、HEAD等等都轮了一遍。所以,作为CDN的源站在返回重定向时,不能简单的返回302或者301,要返回307或者308

所以,如果你源站在docker registry前面是用的Nginx的话,默认的配置

rewrite ^/(.*)$ https://myhost:80443/$1 redirect;

并不行,必须使用下面的这种方式:

return 308 https://myhost:80443$request_uri;

才能使得非GET请求的重定向达到预期效果。

Docker的PATCH请求不处理重定向

所以,即便是返回码是307/308。在docker通过PATCH向上传地址上传镜像内容的时候,是不能正确响应这个状态码的。也就是说他不会跟随到新地址去PATCH,而是死磕。这个死磕的本质是什么呢?本质就是他会从PATCH请求的响应中直接获取Range头字段,以决定下一步是继续PATCH镜像的其他部分、还是PUT其他部分,抑或是报错。

如果你第一反应是:

老子忍了,这流量费我出!

然后修改配置,所有的PATCH源站直接处理并响应,不重定向了。那么恭喜你,这流量费你还真没机会出。为什么呢?这就要引出下一个坑。

CDN会过滤掉源站响应中的Range头

也就是说,源站响应中的Range头根本不会透传给客户端。

这一点我最开始也是猜测,后来通过工单和腾讯云的工程师确认了这一单。原因是CDN服务的性质决定。由于CDN自身会用到Range这特性,来做分片缓存。为了避免内部的Range和源站的Range混淆,简单粗暴的方式就是直接对这个HTTP头不做透传。

所以,通过CDN代理后docker的PATCH请求就永远得不到Range头,也就永远不能获得正确的响应。那么解决方法是什么呢?就是禁用Registry的RelativeURLs特性,配合正确的Host和Proto,实现逻辑上的重定向。

举例来说,如果我registry服务的访问地址是http://myhost.com(HTTPS),源站的访问地址是http://docker.myhost.com:8443(HTTPS),反向代理Nginx访问后段registry服务的地址是registry(HTTP)。那么:

  1. Registry中配置http.relativeurlsfalse(环境变量的话,就是REGISTRY_HTTP_RELATIVEURLS=false),
  2. 然后参考下面的Nginx配置文件配置源站:
server{
	listen 8443 ssl http2;
	server_name docker.myhost.com;

	ssl_certificate      /etc/nginx/ssl/docker.myhost.com.crt;
	ssl_certificate_key  /etc/nginx/ssl/docker.myhost.com.key;

	location /v2 {
		client_max_body_size 0;
		proxy_set_header X-Forwarded-Proto https;
		proxy_set_header Host docker.myhost.com:8443;
		proxy_pass http://registry;
	}
}

这样一来,当docker通过POST请求https://myhost.com/v2/myrepo/blobs/uploads/被307重定向后,最终得到的HTTP 202响应里Location头的内容直接就是:

https://docker.myhost.com:8443/v2/myrepo/blobs/uploads/xxxxxxx?_state=xxxxx

这样直接通过源站上传的绝对地址,而不是

/v2/myrepo/blobs/uploads/xxxxxxx?_state=xxx

这样会被docker加上https://myhost.com从而走CDN的相对地址了。