理解 Docker Client/Server 架构, 找寻 Docker Desktop 替代品

本文继上篇 摆脱 Docker Desktop 即将到来的收费 进一步寻找符合自己需求的 Docker Desktop 替代品,前面试用过 hyperkit + minikube, Podman, 最终还是确定了用 docker-machine。之所以选择了它是基于下面几个需求:

  1. 连接 VPN 后 Docker 还要能继续工作 (通过 socket 文件和 localhost 与 Docker Host 通信不惧怕 VPN 连接. 因为 VPN 会接管路由表,所以用 IP 来连接 Docker Host 的话,VPN 连上后将可能无法访问 Docker Host)
  2. 能与 IDE 进行集成开发与调试  (IntelliJ IDEA 能与 Docker Desktop, Docker Machine, TCP socket 和 SSH 上的 Docker Host 集成调试,但无法与 Daemonless 的 Podman 集成)
  3. DOCKER_HOST 能是远程机器 (由于 Podman 设计为 Daemonless,也就没有 Docker Host, 无法进行远程构建)

本文力图更深入的理解 Docker 的架构来解释最后选择的来由,清楚了原理后可以自主创建一个 Docker Host,连 docker-machine 也可以不用。比如创建一个 AWS EC2 实例作为 Docker Host, 然后在本地执行 docker 命令进行镜像的构建与容器的运行,这时候镜像构建过程与容器执行的环境是在 EC2 上,再也不用先把本地的文件上传(scp 或 rsync) 到 EC2 上,然后 ssh  到 EC2 去执行 docker 命令了。

一个小插曲:本人曾经随手在  ~/Downloads 目录下建立一个只有 FROM busybox 一行的 Dockerfile 文件,然后运行 docker build ...  命令,结果每次都提示磁盘空间不足,本机磁盘还非常宽裕,Docker Machine 也分配了 20 G 内存,怎么会不够了呢?登入到 Docker Machine  后 df 确实没空间了。四处找原因,原来是 docker build ... 一执行,不管 3721 首先把当前目录下的所有文件全部拷贝到 Docker Machine 中去,~/Downloads 目录中下了几十个 G 的内容,所以把 Docker Machine 给挤暴掉。解决办法就是要把  Dockerfile 放到一个没有无用文件的独立目录中去,这也是为什么 Dockerfile 中的 COPY 命令只能从当前目录中拷贝文件的原因。

理解 Docker 的 Client/Server 架构

DOCKER 的架构是 Docker Client 通过 REST API 与 Docker Host(dockerd 进程) 进行通信,我们所执行的  docker 命令只与 Docker Host 进行交互的。在 Linux 下 Docker Host 可以是系统自身,而对于 Mac OS X 或  Windows 需要有一个中间的 Linux 操作系统,比如 Docker Desktop  实现的虚拟机,或者自己创建一个 Linux 虚拟机,装上 Docker 就能作为 Docker Host。

Docker Client 与  Docker Host 之间是 REST API,它们可以通过以下三种方式交互

  1. 本地 Socket 文件(/var/run/docker.sock)
  2. https (通常用 2376 端口)
  3. http (通常用 2376 端口)  

Docker Desktop 所创建的虚拟机(Docker Host) 默认与 Docker Client 是通过 /var/run/docker.sock 文件交互的,准确的说是 docker 命令在没有 DOCKER_HOST 环境变量时会尝试与 /var/run/docker.sock 通信。这就是为什么 Docker Daemon 没启动时会提示

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

这里插入一张 Docker Client 与 Docker Host 通信的架构图

Docker Client 就是我们输入 docker 命令的地方,Docker Host 是具体执行 docker 命令的地方,如构建/上传/下载镜像,运行容器等。Docker Desktop 为我们所做的事情基本上就是暗地里启动了一个虚拟机,并在宿主机上的 /var/run/docker.sock 文件上监听 REST API 请求。对了还有鲜有人问津的 Swarm 和可选择启用的 Kubernetes。

Docker Desktop 的基本功能:

  1. 构建时把 Dockerfile 所在目录的全部内容传到 Docker Machine 完成构建
  2. docker run -p 9002:80 映射时完成的是 host:9002:docker-desktop:9002:container:80
  3. docker run -v ~/.aws:/.aws 卷映射完成从 host:~/.aws -> 经过 docker-desktop -> container:/.aws
  4. 其他.....

Docker Desktop 为我们实现以上 #2 和 #3 两个操作带来了便利。

那么我们怎么能感知到那个虚拟机的存在呢?在 Docker Desktop 的 Resources 和 Docker Engine 配置就是为该虚拟机分配资源与定制如何启动 dockerd 的

 

默认还挺大方,6 个 CPU,2G 内存

Docker Desktop 启动后,可以看到两个 sock 文件

$ ls -l /var/run/docker*
lrwxr-xr-x 1 root daemon 69 Oct 20 19:07 /var/run/docker-cli.sock -> /Users/yanbin/Library/Containers/com.docker.docker/Data/docker-cli.sock
lrwxr-xr-x 1 root daemon 35 Oct 20 19:07 /var/run/docker.sock -> /Users/yanbin/.docker/run/docker.sock

后面的 docker 命令就是与这里的 /var/run/docker.sock 文件通信的,比如可以 telnet 一下它

$ telnet /var/run/docker.sock
Trying /var/run/docker.sock...
Connected to (null).
Escape character is '^]'.
GET /version HTTP/1.0

HTTP/1.0 200 OK
Api-Version: 1.41
Content-Type: application/json
Date: Thu, 21 Oct 2021 00:17:59 GMT
Docker-Experimental: false
Ostype: linux
Server: Docker/20.10.8 (linux)

{"Platform":{"Name":"Docker Engine - Community"},"Components":[{"Name":"Engine","Version":"20.10.8","Details":{"ApiVersion":"1.41","Arch":"amd64","BuildTime":"2021-07-30T19:52:31.000000000+00:00","Experimental":"false","GitCommit":"75249d8","GoVersion":"go1.16.6","KernelVersion":"5.10.47-linuxkit","MinAPIVersion":"1.12","Os":"linux"}},{"Name":"containerd","Version":"1.4.9","Details":{"GitCommit":"e25210fe30a0a703442421b0f60afac609f950a3"}},{"Name":"runc","Version":"1.0.1","Details":{"GitCommit":"v1.0.1-0-g4144b63"}},{"Name":"docker-init","Version":"0.19.0","Details":{"GitCommit":"de40ad0"}}],"Version":"20.10.8","ApiVersion":"1.41","MinAPIVersion":"1.12","GitCommit":"75249d8","GoVersion":"go1.16.6","Os":"linux","Arch":"amd64","KernelVersion":"5.10.47-linuxkit","BuildTime":"2021-07-30T19:52:31.000000000+00:00"}
Connection closed by foreign host.

或者用 curl 命令

curl --unix-socket /var/run/docker.sock http:/localhost/version

会得到相同的版本信息

这里是当前 Docker Engine 1.41 的 REST API 文档 Docker Engine API [v1.41]。如果也想开发一个像  Portainer 那样的 Docker 管理工具就需要了解 Docker Engine 的  API。

还可以进到该虚拟机里去看看,SSH 是不行了,但我们能用个  docker 容器,使用宿主机的 Namespace 进入查看到的就是宿主机(虚拟机)本身的信息

上面大概就是 Docker Desktop 背后那个虚拟机主要的信息。

用  docker context 命令也能看到该 desktop-linux 虚拟机

见识过 Docker Desktop 虚拟机的那些信息后,在我们撇开 Docker Desktop 后完全可以自己创建一个 Docker Host,即启动 dockerd 进程时怎么监听 REST API 请求,还有客户端如何连接 Docker Host 的问题。

Docker 与 Podman 的区别

Podman 是 RedHat 发展可作为  docker  别名使用的工具,它被冠以几个属性,Dockerless, Daemonless 和 Rootless。在非 Linux 平台下运行 Linux 容器总是需要一个 Linux 虚拟机,Podman 用 podman machine init 初始化一个虚拟机,并且 driver 可以有多种选择,默认为 Qemu。宿主机上执行 podman run 肯定要让那个 Linux 虚拟机来执行,说明宿主机与虚拟机之间还是要通信的,那为什么叫做 Daemonless 呢?原因是在 podman machine 中不需要 dockerd 这个 daemon 进程.

Docker 的 client 不做多少事,构建镜像等工作都由 Docker Daemon(即 dockerd) 来完成, 而 podman 不一样,构建镜像等由 podman 自己完成,当然运行容器还是要那个  podman machine , 所以在这个虚拟机中也有 docker 命令。用 podman pull busy 拉下来的镜像在 podman machine ssh 后用 docker images 也能看到。

其实对 Podman 还是有些不是很清楚的,同样在构建镜像前,也会把 Dockerfile 所在目录的所有文件打包传到 podman machine 中去,比如会打包到 /sysroot/ostree/deploy/fedora-coreos/var/tmp/libpod_builder756951953/tarBall 文件中,文件大的话同样无法构建镜像,而且拷贝文件时不显示任何信息,docker build 还会显示拷贝文件大小的进度。

由于 Podman 不用通过网络与 podman machine 进行交互,所以也不用担心 VPN 连接后无法使用的情况,但是目前似乎流行度不够,还没受到 IntelliJ IDEA 的青睐,尚无法与 IntelliJ IDEA 集成。

再就是不能像 Docker 那样,启动一个 AWS EC2 上的 Docker daemon, 本地执行 docker build, docker push 等命令,实际构建在 EC2 发生的,上传镜像也是从 EC2 到远端的 Registry,而不是直接从本地上传的,特别是上传镜像到 AWS 的 ECR 中会快很多。 

使用 Docker Machine 替代 Docker Desktop

Docker Machine 是早已被 Docker 放弃的工具,见 https://docs.docker.com/machine/,同时被遗弃的工具还有 Docker Toolbox 和 Kitematic。所以 Docker Machine 的代码 https://github.com/docker/machine 3 年多未被更新。Docker Machine  用的是最早的 boot2docker 镜像,到目前为止它仍然能工作,等到它无法使用时估计要亲自折腾 Vagrant, 自建一个 Docker Daemon 的 Box, 然后由 Docker client 通过端口重定向连接进来。

趁着 Docker Machine 仍然能满足当前需求,还是直接用 docker-machine  来管理虚拟机方便,Docker Machine 的基本用法在前一篇 摆脱 Docker Desktop 即将到来的收费 一文方案三中有讲解。当前 docker-machine  版本为 0.16.1, 看来只会定格在这里了,安所使用的 Boot2Docker 版本是 v19.03.12(Docker Engine, 还不算太老), DockerMachine + Boot2Docker 可以说是 Docker Desktop 的前生。

这里会有一些与前篇重复内容,注:docker-machine 的虚拟机的用户名和密码是 docker/tcuser

Docker Machine 的基本用法

$ brew install docker-machine
$ docker-machine create --driver virtualbox default
$ eval $(docker-machine env)
$ docker ps

注:如果用 docker-machine  创建虚拟机或启动时出现类似下面的错误

Setting Docker configuration on the remote daemon...

This machine has been allocated an IP address, but Docker Machine could not
reach it successfully.

SSH for the machine should still work, but connecting to exposed ports, such as
the Docker daemon port (usually <ip>:2376), may not work properly.

You may need to add the route manually, or use another related workaround.

This could be due to a VPN, proxy, or host file configuration issue.

You also might want to clear any VirtualBox host only interfaces you are not using.
Checking connection to Docker...
Error creating machine: Error checking the host: Error checking and/or regenerating the certs: There was an error validating certificates for host "192.168.99.100:2376": dial tcp 192.168.99.100:2376: i/o timeout
You can attempt to regenerate them using 'docker-machine regenerate-certs [name]'.
Be advised that this will trigger a Docker daemon restart which might stop running containers.

这时可尝试重 vboxnetX 网卡,如 sudo ifconfig vboxnet1 down && sudo ifconfig vboxnet1 up, 或者把 vboxnet0 外的设备全删除,用 VBoxManage hostonlyif remove vboxnet1, 或者在 VirtualBox/File/Host Network Manager 里处理。

从 docker-machine env 的输出

export DOCKER_TLS_VERIFY="1"
export DOCKER_HOST="tcp://192.168.99.100:2376"
export DOCKER_CERT_PATH="/Users/yanbin/.docker/machine/machines/default"
export DOCKER_MACHINE_NAME="default"
# Run this command to configure your shell:
# eval $(docker-machine env)

运行 eval $(docker-machine evn) 生成的环境变量默认是要通过 HTTPS 端口号 2376 加上证书来访问 Docker Daemon 的 REST API 的,而不是通过 socket 文件。这就可能在机器连接上 VPN 后由于路由的原因不能访问 192.168.99.100。为解决在 VPN 后也能正常访问 docker-machine 有以下几个思路

  1. 映射可写的共享目录,让 docker-machine 启动 dockerd 进程时在创建  docker.sock 文件到宿主机目录下,比如写到默认的 /var/run/docker.sock 文件,或用 DOCKER_HOST=unix:///User/yanbin/docker.sock 环境变量,看能否执行 docker ps 命令
  2. 通过 VirtualBox 的虚拟机 default 的端口映射,或 ssh 隧道, 由本地的 2376 端口号映射到 192.168.99.100:2376, 然后配置 DOCKER_HOST=tcp://localhost:2376 来访问
  3. 思路与上面一样,但必须禁止掉证书验证,端口映射后设置 DOCKER_HOST=tcp://localhost:2376 来访问
  4. 仍然要进行端口映射,localhost:2375:192.168.99.100:2375,但在 docker-machine 中启动 HTTP 端口号,然后配置 DOKCER_HOT=tcp://localhost:2375 来访问

总之,只要是通过 socket 文件或 localhost 就不会被 VPN 的路由设置带偏。

以下是前面几个思路的尝试

思路1, 写 sock 文件到宿主机的方式

下面那样创建虚拟机

$ docker-machine create -d virtualbox --virtualbox-share-folder /Users/yanbin:yanbin --engine-opt host=unix:///yanbin/docker.sock default
default 虚拟机能创建,目录共享也设置成功,但无法启动 dockerd 进程。docker-machine ssh 登陆后用命令
sudo dockerd -H unix:///yanbin/docker.sock
INFO[2021-10-21T03:40:12.505891167Z] Starting up
failed to load listeners: can't create unix socket /yanbin/docker.sock: listen unix /yanbin/docker.sock: bind: operation not permitted

也无法启动 dockerd。本来期特通过设置  DOCKER_HOST=unix:///Users/yanbin/docker.sock 来让 docker 工作

后来还尝试过用 Vagrant 虚拟机,启动 dockerd,在宿主机上也能看到虚拟机中生成的 docker.sock 文件,但仍然无法使用 docker.

此路不通

思路2,端口映射来访问 tcp://localhost:2376

默认方式 docker-machine create -d virtualbox default 创建虚拟机,然后映射端口 localhost:2376:192.168.99.100:2376

$ docker-machine stop
$ VBoxManage modifyvm default --natpf1 "docker,tcp,,2376,,2376"
$ docker-machine start
$ eval $(docker-machine env)
$ export DOCKER_HOST=tcp://127.0.0.1:2376

试下 docker ps 命令

$ docker ps
error during connect: Get "https://127.0.0.1:2376/v1.24/containers/json": x509: certificate is valid for 192.168.99.100, not 127.0.0.1

证书不是给 127.0.0.1 用的,用  ssh -L 127.0.0.1:2376:$(docker-machine ip):2376 yanbin@localhost 映射端口后的错误是一样的。用 docker-machine regenerate-certs 也无法重新给 127.0.0.1 生成证书,或者经过其他更复杂的证书配置能让它工作,不想去尝试了。

基本上此路也不通,但是慢慢变得明朗

思路3,在上面的基础上禁掉证书验证

Docker 客户端在请求 Docker Host  可以用 --tlsverify=false 参数忽略掉证书验证。像普通方式一样创建虚拟机

$ docker-machine create -d virtualbox default
$ docker-machine stop
$ VBoxManage modifyvm default --natpf1 "docker,tcp,,2376,,2376"
$ export DOCKER_HOST=tcp://127.0.0.1:2376
$ docker ps
Error response from daemon: Client sent an HTTP request to an HTTPS server.
$ docker --tlsverify=false ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES

不管在用 docker-machine 创建虚拟机时也不要求 --engine-opt tlsverify=false--engine-opt tls=false 参数,只要客户端 docker 命令带上 --tlsverify=fasle 就能跳过证书验证

这里有个主意就是给 docker --tlsverify=false 一个为 docker 的别名,可以把下行写在 shell 的初始文件中,如 ~/.zshrc 文件

$ alias docker="docker --tlsverify=false"

以后用 docker 这个别名命令就能总是忽略掉证书验证了。

注:在客户端通过 export DOCKER_TLS_VERFIY=1export DOCKER_TLS_VERIFY= 能控制是用 HTTP 还是 HTTPS 访问,但 docker 命令跳过证书验证一定要用 docker --tlsverify=false 参数

思路4,用 HTTP 访问 Docker Host, 无需证书

在 docker-machine 创建虚拟机时加上 --engine-env DOCKER_TLS=no 参数

$ docker-machine create -d virtualbox --engine-env DOCKER_TLS=no default
$ docker-machine stop
$ VBoxManage modifyvm default --natpf1 "docker,tcp,,2376,,2376"
$ docker-machine start
$ export DOCKER_HOST=tcp://127.0.0.1:2376
$ docker ps

这样 docker 就能工作了,只需要 DOCKER_HOST 环境变量,因为有了 DOCKER_TLS=no 命令压根就没有生成证书,其他的与证书相关的环境变量也是多余的。虽然用的是 2376 端口号,但实际上是通过 HTTP 进行通信的。

上面在用 docker-machine create 创建虚拟机时会出现类似下面的错误信息

Checking connection to Docker...
Error creating machine: Error checking the host: Error checking and/or regenerating the certs: There was an error validating certificates for host "192.168.99.100:2376": tls: oversized record received with length 20527
You can attempt to regenerate them using 'docker-machine regenerate-certs [name]'.
Be advised that this will trigger a Docker daemon restart which might stop running containers.

不用担心,而且执行 docker-machine env 也出不来环境变量的设置,也无妨

如果想用 2375 端口号看起来更正规一些,可用

$ docker-machine create -d virtualbox --engine-env DOKCER_TLS=no --engine-opt host=tcp://0.0.0.0:2375 default

则会同时在 2375 和  2376 上监听  HTTP 请求,用  DOCKER_HOST=tcp://127.0.0.1:2375 还是 DOCKER_HOST=tcp://127.0.0.1:2376 都是一样的。

最后

docker-machine 除了支持 virtualbox 虚拟机类型外,还支持 hyperkit, xhyve, 甚至 amazonec2, 所以也能通过  docker-machine create --driver amazonec2 来创建 EC2 实例作为 Docker Host,这样就能本地执行 docker 命令,实际做事情都在 EC2 上。更为完整的 driver 支持列表可见 https://github.com/docker/machine/tree/master/drivers

可惜像 docker-machine 这么不错的项目却停止了更新,以后如果需要使用到更新版本的 Docker Engine 时只有仿照着 docker-machine 来制作自己的 Vagrant Box,或者更新 Boot2Docker。

由于 Mac 下 docker 命令也是来自于 Docker Desktop, 如果不安装 Docker Desktop 想要 docker 命令的话请尝试 brew install docker-toolbox 来安装。

docker-machine 默认的虚拟机内存为 1G, CPU 为 1 个,本人的目前的做法是

使用 docker-machine 时 -v 卷映射正常

执行 docker run -v /Users/yanbin:/yanbin busybox 可以映射 Mac OS 的  /Users/yanbin 到容器的 /yanbin 中去

总结 docker-machine + docker 命令能实现的

  1. [X] 连 VPN 后正常使用 docker, 使用 DOCKER_HOST=tcp://127.0.0.1:2376 解决
  2. [X] 端口映射,可解决,给 docker-machine 虚拟机添加一段预定的端口映射来使用
  3. [X] 卷映射正常,可映射 Mac OS X 下的目录到 Linux 容器

在使用  docker-machine 时,如果在登陆 AWS ECR 时出现类似下面的错误

aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789088.dkr.ecr.us-east-1.amazonaws.com
Error saving credentials: error storing credentials - err: exec: "docker-credential-desktop": executable file not found in $PATH, out:
请查一下在 ~/.docker/config.json 文件中是否有 credsStore,有的话把它改为 credStore

再最后,如果是用 docker-machine 的话,每次重启机器后记得 docker-machine start 启动一下虚拟机,并且把 DOCKER_HOST=tcp://127.0.0.1:2376 加到 shell 的配置文件中去。

Rancher Machine 再现,看来 Docker Machine  后继有机,看 https://docs.ionos.com/docker-machine-driver/rancher/rancher-machine,还很相似的用法,有空再看看。

$ rancher-machine create --driver ionoscloud test-machine
$ eval $(rancher-machine env test-machine)

或 Docker Machine 用 Boot2Docker 的替代品

$ docker-machine create -d virtualbox  --virtualbox-boot2docker-url https://releases.rancher.com/os/latest/rancheros.iso default

说到 Rancher, 感觉会引出一个大海怪,著名的 K3s 就来自于 Rancher。

2020-10-28


问题来了,在用 docker run -p 8888:8888 启动容器时,端口映射变得复杂了,映射的端口是 docker-machine 的 8888 到容器的 8888,在本机(Mac 或 Windows) 上并未启动 8888 端口,有个解决办法是一个 pf 脚本,或 VirtualBox 预先准备几个从本机到 docker-machine 的端口。或可尝试 --net=host 启动容器。对于 docker 容器中运行 jupyterlab 启动时必须加上 --ip 0.0.0.0。

链接:

  1. Get started with docker

本文链接 https://yanbin.blog/docker-desktop-replacement-docker-machine/, 来自 隔叶黄莺 Yanbin Blog

[版权声明] Creative Commons License 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
Subscribe
Notify of
guest

1 Comment
Inline Feedbacks
View all comments
trackback

[…] 理解 Docker Client/Server 架构, 找寻 Docker Desktop 替代品 中的老办法,揭一揭 Docker […]