参照Docker的官方文档,搭建一套Docker Registry服务,对于很多人来讲是很容易的一件事情。为什么我要单独拿出来说,这个过程又遇到了什么坑爹的事儿呢?
场景描述
首先描述下我们的场景。我们的Docker Registry服务器有公网IP,但由于某种限制,我们对外的80、443端口被屏蔽。(你想到了什么?嗯,对了)
对于docker而言,除非手动加入白名单,否则默认都是通过HTTPS协议(443端口)去指定的地址访问镜像的。比如对于镜像 myhost.com/user1/repo
来说。就会默认访问https://myhost.com
来获取镜像信息。当然您可以在镜像地址中手动加上端口号来强制docker走指定端口。但这显然不是我们想要的,谁想弄一个又长又有端口号的地址在镜像里呢?
如果你正好和我们的想法一样,恭喜你,这篇文章就是为你准备的。那么我们的解决方案是什么呢?答案就是——
解决方案——CDN
听起来是不是有一种“还特么要你说,老子也知道,然并卵”的感觉。为什么会有这种感觉呢?
- 大多数CDN回源是同端口或者要求是标准端口(80、443)。
- 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)。那么:
- Registry中配置
http.relativeurls
为false
(环境变量的话,就是REGISTRY_HTTP_RELATIVEURLS=false
), - 然后参考下面的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的相对地址了。