我为什么要搭建自己的 docker 镜像仓库?

  1. 互联网上国内各类大学和公司的 docker 镜像仓库基本都挂了;
  2. 公司项目在内网搭建了私有的镜像仓库,借机深入研究一下;
  3. 容器技术大有可能,为了更实际的目的,更好的跨平台管理自己的各种服务;

这篇博客记录了从搭建 docker 仓库这个想法开始的,若干折腾的过程和解决的问题。我不想写一篇完美的,循规蹈矩的文章,就是以小标题组织的各类折腾的记录。

客户端和服务端

当我们提到一个应用程序,同时还提到它的仓库,那么大概率是有客户端和服务端的区分。比如典型的git,本地安装的 git cli 或 图形 app 是客户端,git仓库(github,gitlab..)就是服务端,docker 也不例外。

Linux 客户端可以用各类发行版的包管理器安装,Windows 和 Mac 一般推荐直接安装桌面软件,官网都有下载。服务端就是 docker 的镜像仓库,官方有一个镜像仓库 docker.io,不过需要代理访问。如果你没有代理,在我寻遍了国内许多大学和大厂曾经的公开仓库,发现全部已经无法使用之后(无法理解原因??),找到了一个靠谱的方式:docker_image_pusher,使用 Github Action 将国外的 Docker 镜像转存到阿里云私有仓库,然后再使用阿里云的服务拉取镜像到本地使用。

修改镜像存储目录

由于镜像占用的存储空间一般都比较大,需要修改默认(/var/lib/docker)的本地镜像的存储目录。编辑 /etc/docker/daemon.json,添加 data-root 即可:

{
  "data-root":"/mnt/ds923plus/docker",
}

这里我将镜像的存储目录设置为了挂载到本机的NAS目录。如何挂载NAS的目录到当前机器?

NAS(192.168.31.100) 中建立 /volume1/raspi4 目录,设置 nfs 访问后,执行下面的 mount 命令将内网中的 NAS 的 /volume1/raspi4 挂载到本机的 /mnt/ds923plus 目录:

sudo mount -t nfs 192.168.31.100:/volume1/raspi4 /mnt/ds923plus

编辑 /etc/fstab 文件,主机重启后自动挂载:

192.168.31.100:/volume1/raspi4 /mnt/ds923plus nfs defaults 0 0

搭建私有仓库

docker 仓库也可以使用 docker 来获取,就像编译器也可以用来编译编译器自己😂。docker 的镜像仓库的镜像是: registry:2。参考 docker_image_pusher 中的步骤,从阿里云服务拉取镜像仓库的镜像,下面是一个例子:

docker pull registry.cn-hangzhou.aliyuncs.com/gkimage/linux_arm64_registry:2

启动容器:

docker run -d --name my-registry \
  --network host \
  -v /mnt/ds923plus/docker-registry:/var/lib/registry \
  --restart=always \
  registry.cn-hangzhou.aliyuncs.com/gkimage/linux_arm64_registry:2

编辑 /etc/docker/daemon.json,添加你的仓库地址,如果不做端口映射,仓库的默认端口就是 5000:

{
  "insecure-registries": ["192.168.31.99:5000"]
}

重启 docker 服务,并测试 curl 调用:

sudo systemctl restart docker
curl http://192.168.31.99:5000/v2/_catalog

现在我们便拥有了一个地址为 192.168.31.99:5000 的内网镜像仓库。这是内网版本的,没有 https,没有登陆验证。先操作一遍验证可行性,稍后我们再考虑安全性。

给私有仓库添加镜像

我们拥有了私有镜像,自然想推送一些常用的镜像进去(如:ubuntu,nginx等),以便 hack 或直接使用,这些常用的镜像我们无需自己制作,可以从其他公开的镜像仓库 pull,再 push 到私有仓库中去。

如果你没有国外代理,可以使用前文提到的阿里云服务来获取镜像。如果你有代理,可以直接从官方仓库拉取镜像,然后push到私有仓库中。拉取都本地的镜像名的前缀是那个远程仓库的名称,需要使用 docker tag 重命名为即将推送的私有仓库的名称,因为推送镜像会解析镜像名前面的 域名(ip):端口 作为要推送的仓库服务的地址。

比如下面这个从官方仓库拉取镜像 ubuntu:22.04 并重命名为 192.168.31.99:5000/ubuntu:22.04

docker pull docker.io/ubuntu:22.04
docker tag ubuntu:22.04 192.168.31.99:5000/ubuntu:22.04

推送到私有仓库:

docker push 192.168.31.99:5000/ubuntu:22.04

值得注意的是,从官方拉取的镜像 ubuntu:22.04 省略了前面的仓库信息 docker.io/library/, 实际的完整的镜像名称应该是 docker.io/library/ubuntu:22.04。只有官方仓库可以省略,其余私有仓库都需要显式指定仓库地址。

多版本镜像管理

从其他仓库中拉取镜像,再 push 到私有仓库存在的问题是,无法创建多平台兼容的镜像版本。

同一个镜像在不同的平台上(linux/amd64/arm64/..)不一样,为了让自己的镜像能够适配不同的平台,需要拉取不同的平台版本的镜像,然后合并推送到私有仓库里面,一开始我是这样想得。后来发现,默认只能拉取当前平台的镜像(即使指定–platform也不行),此路不通。

可以使用下面的命令直接将官方的多版本镜像拉取到私有仓库,无需经过本地中转:

docker buildx imagetools create --tag <私有仓库地址>/nginx:1.25 docker.io/nginx:1.25

powershell 设置代理

本地访问官方的镜像仓库(docker.io)需要设置代理,linux 设置代码的方式很熟悉,windows 的 powershell 如何设置代理呢?

临时设置,仅当前 session 有效:

$env:HTTP_PROXY = "http://127.0.0.1:7890"
$env:HTTPS_PROXY = "http://127.0.0.1:7890"
$env:NO_PROXY = "localhost,127.0.0.1,192.168.*,.docker.internal"
docker info | Select-String "Proxy"  # 检查输出中是否包含代理信息

永久设置,写入环境变量中:

[System.Environment]::SetEnvironmentVariable("HTTP_PROXY", "http://127.0.0.1:7890", "User")
[System.Environment]::SetEnvironmentVariable("HTTPS_PROXY", "http://127.0.0.1:7890", "User")
[System.Environment]::SetEnvironmentVariable("NO_PROXY", "localhost,127.0.0.1,192.168.*,.docker.internal", "User")

暴露到公网

如果我们并非所有时候都在内网使用 或 需要与朋友分享自己的仓库,那么将私有仓库服务暴露到公网,以便随时随地可以获取是很实际的需求。

我的方式是路由器层面做端口转发到内网的一台主机,在内网的这台机器上用 ngnix 反向代理到仓库服务的地址。

比如我在路由器的设置中将 22022 端口转发到了内网 192.168.31.98(反向代理机器) 的 20011 端口。

下面是 192.168.31.98 的 nginx 反向代理的配置的例子,监听 20011 端口的请求,并将请求转发到内网的仓库服务地址 http://192.168.31.99:5000 :

# docker
server {
    listen       20011 ssl;
    server_name  zkai.fun;

    ssl_certificate      <ssl证书>;
    ssl_certificate_key  <ssl证书的key>;
    ssl_session_cache    shared:SSL:1m;
    ssl_session_timeout  5m;
    ssl_ciphers  HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers  on;

    location / {
        proxy_pass http://192.168.31.99:5000; # 内网服务的IP和端口
        client_max_body_size 1G;
        proxy_set_header X-Forwarded-Host $host:$server_port;
        proxy_set_header Host $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 $scheme;
    }

    error_page  404              /404.html;
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

此时,我们便获得了公网可访问的 docker 私有仓库:zkai.fun:22022,此时镜像的拉取和推送操作均需要使用这个地址。

再次使用 curl 测试,获取当前仓库中的镜像列表:

curl https://zkai.fun:22022/v2/_catalog

考虑安全性

如果不设置登陆认证,任何人都可以从公网操作镜像仓库。为了保证安全性,需要设置镜像仓库的加密验证。

使用 htpasswd 生成密码认证信息,并写入到文件,我们使用该存储用户名和加密密码的文件来启动 docker registry 即可完成 docker 的登陆验证设置

sudo htpasswd -Bbn <用户名> <密码> | sudo tee <加密文件> >/dev/null

生成加密文件之后,设置相关环境变量,启动容器:

docker run -d --name registry \
  --network host \
  --restart=always \
  -v /mnt/ds923plus/docker-registry:/var/lib/registry \
  -v /mnt/ds923plus/docker-auth:/auth \
  -e "REGISTRY_AUTH=htpasswd" \
  -e "REGISTRY_AUTH_HTPASSWD_REALM=Registry Realm" \
  -e "REGISTRY_AUTH_HTPASSWD_PATH=<加密文件>" \
  192.168.31.99:5000/registry:2

容器拉起来后,再次使用就需要验证用户名密码了:

docker login zkai.fun:22022

此时,使用 curl 测试也需要加上用户名密码了:

curl https://<用户名>:<密码>@zkai.fun:22022/v2/_catalog

进阶:Harbor

如果我们需要更丰富的仓库管理功能和安全性,可以使用 VMware 开源的企业级容器镜像仓库:https://github.com/goharbor/harbor