Colima代理

对于中国的软件开发者、运维者来说,要顺利使用 dockerhub 来获取镜像, Proxy代理服务 是必须采用的技术,所以也要为Colima解决绕过GFW阻塞的问题。

我最初没有使用代理,发现 Debian镜像(tini进程管理器) 无法拉取镜像:

执行镜像构建
docker build --rm -t debian-ssh-tini .

始终报错:

无法下载镜像导致构建失败
[+] Building 32.9s (2/2) FINISHED                                                                                                                                     
 => [internal] load build definition from Dockerfile                                                                                                             0.0s
 => => transferring dockerfile: 1.04kB                                                                                                                           0.0s
 => ERROR [internal] load metadata for docker.io/library/debian:latest                                                                                          32.8s
------
 > [internal] load metadata for docker.io/library/debian:latest:
------
Dockerfile:1
--------------------
   1 | >>> FROM debian:latest
   2 |     MAINTAINER vincent huatai <vincent@huatai.me>
   3 |     
--------------------
error: failed to solve: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: debian:latest: failed to copy: httpReadSeeker: failed open: failed to do request: Get "https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/19/19fa7f391c55906b0bbe77bd45a4e7951c67ed70f8054e5987749785450c0442/data?verify=1723911692-vjk1SJX8qBVoT%2FQRQSQuHdu%2BCsQ%3D": dial tcp 31.13.87.34:443: i/o timeout
FATA[0033] no image was built                           
FATA[0033] exit status 1

此外,在 colima ssh 进入 Lima: Linux Machines 虚拟机内部,就会发现即使 Ubuntu Linux 系统更新( apt update )也是存在和 Docker Atlas 更新相关错误:

Lima: Linux Machines 虚拟机内执行 apt update 报错
...
Ign:31 https://download.docker.com/linux/ubuntu noble InRelease
Ign:31 https://download.docker.com/linux/ubuntu noble InRelease
Ign:31 https://download.docker.com/linux/ubuntu noble InRelease
Err:31 https://download.docker.com/linux/ubuntu noble InRelease
  Could not connect to download.docker.com:443 (157.240.13.8), connection timed out
Fetched 26.3 MB in 37s (703 kB/s)
Reading package lists... Done
Building dependency tree... Done
Reading state information... Done
14 packages can be upgraded. Run 'apt list --upgradable' to see them.
W: Failed to fetch https://download.docker.com/linux/ubuntu/dists/noble/InRelease  Could not connect to download.docker.com:443 (157.240.13.8), connection timed out
W: Some index files failed to download. They have been ignored, or old ones used instead.

解决实践小结

如果你没有耐心看完本文,这里给出一个结论:

  • 只需要在物理主机上配置好代理服务器的用户环境变量,例如我使用 SSH隧道 访问远端服务器的 Squid代理服务 / Privoxy代理服务

  • 执行 colima start 启动colima虚拟机的时候,会自动将物理主机环境变量中有关代理配置设置注入虚拟机,不过只有 APT包管理 解决了 越过长城 (此时可以顺利执行 apt update && apt upgrade

  • 需要同时配置 Docker服务器Proxycontainerd服务端代理 (目前我的验证,尚未验证是否可以只配置其中之一)

  • colima 虚拟机内部配置 Docker客户端的Proxy 这样执行 docker build 就能够在docker客户端获取meta信息,再进一步盗用docker/containerd服务器端下载镜像

警告

配置代理需要同时满足 docker 客户端和服务器的代理设置,单方面配置客户端和服务器端都不能实现代理跨越GFW

分析和解决思路

这个问题需要采用 配置Docker使用代理 方式解决:

代理服务器(之前的尝试,可行但复杂,留作参考)

我个人的经验是使用轻量级的HTTP/HTTPS代理 Privoxy代理服务 最为简单(服务器无缓存),如果希望更为稳定和企业级,则选择 Squid代理服务 (服务器有缓存),不过对实际效果没有太大影响,都是非常好的选择。

  • 首先通过 SSH隧道 构建一个本地到远程服务器代理服务端口(服务器上代理服务器仅监听回环地址)的SSH加密连接。我实际采用的是在 ~/.ssh/config 配置如下:

~/.ssh/config 配置 SSH隧道 构建一个本地到远程服务器Proxy端口加密连接
Host *
  ServerAliveInterval 60
  ControlMaster auto
  ControlPath ~/.ssh/%h-%p-%r
  ControlPersist yes
  StrictHostKeyChecking no
  Compression yes

Host MyProxy
  HostName <SERVER_IP>
  User admin
  LocalForward 3128 127.0.0.1:3128
  LocalForward 172.17.0.1:3128 127.0.0.1:3128
  IdentitiesOnly yes
  IdentityFile ~/.ssh/proxy/id_rsa
  • 执行构建SSL Tunnel:

通过SSH构建了本地的一个SSH Tunneling到远程服务器的 Proxy代理服务 服务
ssh MyProxy

apt代理

  • 修改Colima虚拟机内部配置 /etc/apt/apt.conf.d/01-vendor-ubuntu 添加一行 APT包管理 代理配置:

在 apt 配置中添加代理设置
Acquire::http::Proxy "http://127.0.0.1:3128/";
...

现在再次执行 apt update && apt upgrade 就不会有任何GFW的阻塞,顺利完成虚拟机操作系统更新

Colima虚拟机内部运行的 docker/containerd 需要设置代理以便能够下载镜像运行容器:

Docker服务器代理

我的实践中虚拟机中运行containerd取代默认的Docker:

使用 vz 模式虚拟化的 4c8g 虚拟机运行 colima
colima start --runtime containerd --cpu 4 --memory 8 --vm-type=vz

所以这段Docker服务器代理设置是我之前实践 Docker服务器Proxy ( 👈 请参考)

Containerd服务器代理

Colima虚拟机内部使用的操作系统是 Ubuntu Linux ,完整使用了 Systemd进程管理器 系统来管理进程服务,所以可以采用我之前的实践 containerd代理 相同方法:

  • 修订 /etc/environment 添加代理配置:

/etc/environment 设置代理环境变量
HTTP_PROXY="http://127.0.0.1:3128"
HTTPS_PROXY="http://127.0.0.1:3128"
NO_PROXY="*.baidu.com,192.168.0.0/16,10.0.0.0/8"
  • colima ssh 重新登陆系统使上述代理环境变量生效,然后执行以下脚本为containerd服务添加代理配置:

生成 /etc/systemd/system/containerd.service.d/http-proxy.conf 为containerd添加代理配置
if [ ! -d /etc/systemd/system/containerd.service.d ];then
    mkdir -p /etc/systemd/system/containerd.service.d
fi

cat <<EOF >/etc/systemd/system/containerd.service.d/http-proxy.conf    
[Service]    
Environment="HTTP_PROXY=${HTTP_PROXY:-}"    
Environment="HTTPS_PROXY=${HTTPS_PROXY:-}"    
Environment="NO_PROXY=${NO_PROXY:-localhost},${LOCAL_NETWORK}"    
EOF

systemctl daemon-reload
systemctl restart containerd

代理服务器再次尝试(建议方案)

发现colima会将HOST主机proxy环境变量注入虚拟机

在晚上折腾时偶然发现,如果我的 macOS 操作系统环境变量设置了代理,例如:

如果macOS的host环境配置了代理变量
export HTTP_PROXY="http://127.0.0.1:3128"
export HTTPS_PROXY="http://127.0.0.1:3128"
export NO_PROXY="*.baidu.com,192.168.0.0/16,10.0.0.0/8"
export http_proxy="http://127.0.0.1:3128"
export https_proxy="http://127.0.0.1:3128"
export no_proxy="*.baidu.com,192.168.0.0/16,10.0.0.0/8"

则重新启动colima虚拟机之后,这个环境变量会注入到虚拟机内部,但是会做修改(IP地址从 127.0.0.1 调整为 192.168.5.2 ),而且这个修改是写到虚拟机内部 /etc/environment 中:

Colima启动时会自动将HOST物理主机proxy环境变量注入到虚拟机 /etc/environment
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
#LIMA-START
HTTPS_PROXY=http://192.168.5.2:3128
HTTP_PROXY=http://192.168.5.2:3128
NO_PROXY=*.baidu.com,192.168.0.0/16,10.0.0.0/8
http_proxy=http://192.168.5.2:3128
https_proxy=http://192.168.5.2:3128
no_proxy=*.baidu.com,192.168.0.0/16,10.0.0.0/8
#LIMA-END

我忽然想到,既然Colima将我的HOST物理主机的 PROXY 相关环境变量在启动 Lima: Linux Machines 虚拟机时候注入到虚拟机内部作为环境变量,那么说明Colima开发者默认就是让虚拟机继承物理服务器的代理配置。同时,观察到Colima在虚拟机的 /etc/environment 标准配置中添加了代理配置,但是很巧妙地将物理主机 127.0.0.1 回环地址转变成了 192.158.5.2 ,也就是对应虚拟机( 192.168.5.1 )的默认网关( 192.168.5.2 )。这说明,Colima会借助物理主机的代理服务器访问外网。

综上所述,看起来完全不用手工配置虚拟机内部服务的代理,而是之际在启动 colima 虚拟机时,操作命令所在的HOST物理主机shell环境变量PROXY相关设置会自动注入,来解决Colima虚拟机内部的代理。这是Colima的feature。

通过HOST物理主机 HTTP_PROXY 配置注入虚拟机

  • 首先删除掉刚才测试的虚拟机,准备干净地启动一个全新虚拟机:

执行 colima delete 删除不需要的colima虚拟机(所有数据丢失!!!)
colima delete
  • 在启动 colima 虚拟机之前,先确保发起启动的用户的环境变量如下(配置到 ~/.zshrc 中,或者直接在SHELL中执行):

macOS的host环境 colima start 用户的环境变量配置代理
export HTTP_PROXY="http://127.0.0.1:3128"
export HTTPS_PROXY="http://127.0.0.1:3128"
export NO_PROXY="*.baidu.com,192.168.0.0/16,10.0.0.0/8"
export http_proxy="http://127.0.0.1:3128"
export https_proxy="http://127.0.0.1:3128"
export no_proxy="*.baidu.com,192.168.0.0/16,10.0.0.0/8"
  • 重新创建colima虚拟机:

使用 vz 模式虚拟化的 4c8g 虚拟机运行 colima
colima start --runtime containerd --cpu 4 --memory 8 --vm-type=vz
  • 果然,这次干净启动的 colima 虚拟机内部注入了原先在HOST物理主机配置的PROXY相关配置, colima ssh 登陆后检查 /etc/environment 可以看到配置:

Colima启动时会自动将HOST物理主机proxy环境变量注入到虚拟机 /etc/environment
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin"
#LIMA-START
HTTPS_PROXY=http://192.168.5.2:3128
HTTP_PROXY=http://192.168.5.2:3128
NO_PROXY=*.baidu.com,192.168.0.0/16,10.0.0.0/8
http_proxy=http://192.168.5.2:3128
https_proxy=http://192.168.5.2:3128
no_proxy=*.baidu.com,192.168.0.0/16,10.0.0.0/8
#LIMA-END

注意,这里配置环境变量 HTTP_PROXY / http_proxy ,有全大写也有全小写,这是因为不同的程序的默认差异,比较搞...

HTTP_PROXY 配置注入虚拟机的 colima.yaml

实际上还有一个更为方便的注入方法,就是使用 $HOME/.colima/default/colima.yaml 直接添加PROXY配置:

$HOME/.colima/default/colima.yaml 直接添加PROXY配置
env:
  HTTP_PROXY: http://127.0.0.1:3128
  HTTPS_PROXY: http://127.0.0.1:3128
  NO_PROXY: localhost,127.0.0.1,*.baidu.com,192.168.0.0/16,10.0.0.0/8

新的困扰

其实上述两种方案(虚拟机内部配置 containerd服务端代理 和 通过注入HOST主机PROXY配置到colima虚拟机)都是完成相同的工作,看起来都很完善。但是,我实际构建 Colima镜像 还是再次遇到了报错(两个方法都是一样的报错):

containerd 开始同步时是使用代理的(因为我看到如果不启动SSH tunnel,则出现如下访问代理报错:

error: failed to solve: debian:latest: failed to authorize: failed to fetch anonymous token: Get "https://auth.docker.io/token?scope=repository%3Alibrary%2Fdebian%3Apull&service=registry.docker.io": proxyconnect tcp: dial tcp 127.0.0.1:3128: connect: connection refused

这里代理IP地址也可能是 ``192.168.5.2`` ,取决于采用上述两个方案之一

但是我发现接下来的https请求居然不再走代理,原因是我发现它报错信息解析的地址 production.cloudflare.docker.com => 210.209.84.142 是我本地虚拟机解析DNS的结果:

error: failed to solve: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: debian:latest: failed to copy: httpReadSeeker: failed open: failed to do request: Get "https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/19/19fa7f391c55906b0bbe77bd45a4e7951c67ed70f8054e5987749785450c0442/data?verify=1724172530-5QFH8JiRFjY5RRAQqyHkaNW0Kb4%3D": dial tcp 210.209.84.142:443: i/o timeout

而不是远在墙外squid服务器解析的域名地址(不同地区解析同一个域名返回的地址不同)。看起来 containerd运行时(runtime) 的代理设置并不是和 配置Docker使用代理 一致,这让我很困扰。

那么怎么解决这个问题呢?

Colima是Docker/Containerd混合体?

我原本以为我在 colima start 运行时传递了参数 --runtime containerd 就会在 colima 虚拟机中只单纯运行 containerd运行时(runtime) 从而避免运行 dockerd 。然而,事实证明不管怎样,实际上服务器上是通过 dockerd 去访问 containerd ( containerd.sock )。

从服务器上 systemctl status dockerdsystemctl status conainerd 可以看到,两个服务同时在运行:

root@colima:~# systemctl status containerd
● containerd.service - containerd container runtime
     Loaded: loaded (/usr/lib/systemd/system/containerd.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-08-21 14:56:45 CST; 37s ago
       Docs: https://containerd.io
    Process: 1880 ExecStartPre=/sbin/modprobe overlay (code=exited, status=0/SUCCESS)
   Main PID: 1882 (containerd)
      Tasks: 9
     Memory: 16.2M (peak: 16.8M)
        CPU: 212ms
     CGroup: /system.slice/containerd.service
             └─1882 /usr/bin/containerd

Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768595311+08:00" level=info msg="Start subscribing containerd event"
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768664312+08:00" level=info msg="Start recovering state"
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768706979+08:00" level=info msg="Start event monitor"
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768722711+08:00" level=info msg="Start snapshots syncer"
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768731059+08:00" level=info msg="Start cni network conf syncer for default"
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768736430+08:00" level=info msg="Start streaming server"
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768754558+08:00" level=info msg=serving... address=/run/containerd/containerd.sock.ttrpc
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768790457+08:00" level=info msg=serving... address=/run/containerd/containerd.sock
Aug 21 14:56:45 colima containerd[1882]: time="2024-08-21T14:56:45.768857303+08:00" level=info msg="containerd successfully booted in 0.025767s"
Aug 21 14:56:45 colima systemd[1]: Started containerd.service - containerd container runtime.
root@colima:~# systemctl status docker
● docker.service - Docker Application Container Engine
     Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; preset: enabled)
     Active: active (running) since Wed 2024-08-21 14:56:39 CST; 51s ago
TriggeredBy: ● docker.socket
       Docs: https://docs.docker.com
   Main PID: 1228 (dockerd)
      Tasks: 10
     Memory: 99.6M (peak: 100.2M)
        CPU: 670ms
     CGroup: /system.slice/docker.service
             └─1228 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Aug 21 14:56:39 colima dockerd[1228]: time="2024-08-21T14:56:39.584977473+08:00" level=info msg="Default bridge (docker0) is assigned with an IP address 172.17.0.0/16. Daemon option --bip can be used to set a preferred IP address"
Aug 21 14:56:39 colima dockerd[1228]: time="2024-08-21T14:56:39.662021438+08:00" level=info msg="Loading containers: done."
Aug 21 14:56:39 colima dockerd[1228]: time="2024-08-21T14:56:39.678308259+08:00" level=info msg="Docker daemon" commit=662f78c containerd-snapshotter=false storage-driver=overlay2 version=27.0.3
Aug 21 14:56:39 colima dockerd[1228]: time="2024-08-21T14:56:39.678675509+08:00" level=info msg="Daemon has completed initialization"
Aug 21 14:56:39 colima systemd[1]: Started docker.service - Docker Application Container Engine.
Aug 21 14:56:39 colima dockerd[1228]: time="2024-08-21T14:56:39.721738274+08:00" level=info msg="API listen on /run/docker.sock"
Aug 21 14:56:45 colima dockerd[1228]: time="2024-08-21T14:56:45.700065516+08:00" level=error msg="Failed to get event" error="rpc error: code = Unavailable desc = error reading from server: EOF" module=libcontainerd namespace=plugins.moby
Aug 21 14:56:45 colima dockerd[1228]: time="2024-08-21T14:56:45.700111600+08:00" level=info msg="Waiting for containerd to be ready to restart event processing" module=libcontainerd namespace=plugins.moby
Aug 21 14:56:45 colima dockerd[1228]: time="2024-08-21T14:56:45.700110522+08:00" level=error msg="Failed to get event" error="rpc error: code = Unavailable desc = error reading from server: EOF" module=libcontainerd namespace=moby
Aug 21 14:56:45 colima dockerd[1228]: time="2024-08-21T14:56:45.700159153+08:00" level=info msg="Waiting for containerd to be ready to restart event processing" module=libcontainerd namespace=moby

这说明需要同时设置 Docker Atlascontainerd运行时(runtime) 的代理配置,特别是 Docker服务器Proxy

Systemd进程管理器Docker Atlas 方法见 Docker服务器Proxy ( /etc/default/docker 配置是针对 SysVinit 配置,对systemd不生效) ,其实也是设置 Systemd进程管理器 启动配置的环境变量

生成 /etc/systemd/system/docker.service.d/http-proxy.conf 为docker服务添加代理配置
if [ ! -d /etc/systemd/system/docker.service.d ];then
    mkdir -p /etc/systemd/system/docker.service.d
fi

cat <<EOF >/etc/systemd/system/docker.service.d/http-proxy.conf    
[Service]    
Environment="HTTP_PROXY=${HTTP_PROXY:-}"    
Environment="HTTPS_PROXY=${HTTPS_PROXY:-}"    
Environment="NO_PROXY=${NO_PROXY:-localhost},${LOCAL_NETWORK}"    
EOF

systemctl daemon-reload
systemctl restart docker

现在,加上前面配置 containerd服务端代理 ,实际上服务器端运行时(containerd)和管控(docker)都已经启用的PROXY代理。可以通过在colima虚拟机内部检查 systemctl show <service_name> --property Environment 查看:

通过 systemctl show 检查 Environment 属性
# systemctl show containerd --property Environment
Environment=HTTP_PROXY=http://192.168.5.2:3128 HTTPS_PROXY=http://192.168.5.2:3128 "NO_PROXY=*.baidu.com,192.168.0.0/16,10.0.0.0/8,"

# systemctl show docker --property Environment
Environment=HTTP_PROXY=http://192.168.5.2:3128 HTTPS_PROXY=http://192.168.5.2:3128 "NO_PROXY=*.baidu.com,192.168.0.0/16,10.0.0.0/8,"

输出显示 dockercontainerd 都已经具备了PROXY环境配置

然而很不幸,我发现 nerdctl build 输出依然是报错, httpReadSeeker 复制错误。奇怪,为何没有通过代理来访问 docker 官方仓库?:

error: failed to solve: DeadlineExceeded: DeadlineExceeded: DeadlineExceeded: debian:latest: failed to copy: httpReadSeeker: failed open: failed to do request: Get "https://production.cloudflare.docker.com/registry-v2/docker/registry/v2/blobs/sha256/19/19fa7f391c55906b0bbe77bd45a4e7951c67ed70f8054e5987749785450c0442/data?verify=1724231825-HSnthXcWUnRbhLGA8eMXCizsEq8%3D": dial tcp 199.59.150.43:443: i/o timeout

我理解实际上 nerdctl builddocker build 并不仅仅是服务器端需要访问internet,有一部分数据是通过客户端这边下载的,也就是META数据是通过客户端下载,来定位需要下载的镜像,再由服务器端去pull镜像。这个逻辑导致客户端和服务器端都要能够跨越GFW。

我突然感觉到是 nerdctl 客户端的问题,看起来 nerdctl build 不支持代理? 我尝试在客户端(macOS HOST主机以及colima虚拟机内部都设置了 http_proxyHTTP_PROXY 环境变量,避免大小写差异),但是始终没有解决通过代理访问问题。

nerdctl build --help 输出来看,没有提供 proxy 相关配置 -- nerdctl这样的docker复刻工具实际上功能做了精简,并不能完全支持docker丰富的功能

最终的解决之道

最终解决,说来难以置信地简单,就是: 如果要使用代理服务器来下载docker镜像,务必使用 docker 客户端来管理,支持代理; nerdctl 客户端不支持代理。

具体解决方法是: 在 colima 虚拟机内部执行 docker build 命令,这样结合前面的的服务器配置:

  • colima start 通过HOST物理主机 HTTP_PROXY 配置注入虚拟机,此时仅解决 APT包管理 翻墙

  • 配置 Docker服务器Proxycontainerd服务端代理 (目前我验证两者都配置,没有验证是否可以只配置其中之一) 确保服务器端能够翻墙拉取镜像

  • 一定要在 colima 虚拟机内部,配置 docker 客户端使用代理,即配置 ~/.docker/config.json 如下:

配置 colima 虚拟机内部 docker 客户端使用代理 ~/.docker/config.json
{
 "proxies":
 {
   "default":
   {
     "httpProxy": "http://192.168.5.2:3128",
     "httpsProxy": "http://192.168.5.2:3128",
     "noProxy": "*.baidu.com,192.168.0.0/16,10.0.0.0/8"
   }
 }
}
  • 最后一定要使用 docker build 才能支持客户端使用代理, nerdctl 客户端不支持代理

参考