Docker 容器中使用 Docker - DinD 和 DooD

突然间研究这个来的缘由是正在从 Jenkins 往 Harness 的过度, 而完全用命令来构建 Docker 镜像变得不一样了。在 Jenkins 中 Agent 本身也是一个 Docker Daemon, 所以 Docker 命令执行无障碍,而 Harness 的所谓的 Agent 就是一个个的运行在 Kubernetes 中的 Docker Container (Pod) 了,这其中没有 Docker Daemon, 又不能连接到 Kubernetes 本身的 Docker Daemon。另外 CloudBees CI/CD 的运行环境与 Harness 类似,也是运行在 Kubernetes 中的 Pod。

因此可能要使用某个 Docker 容器来作为 Docker Daemon, 所以牵连出对此的研究,相应的方案有 Docker in Docker(DinD) 和  Docker outside of Docker(DooD)。

对容器中启动 Docker Daemon 的探索

在知晓 DinD 和 DooD 这两个概念本人还试图构建过一个 Docker 镜像,试图用一个 Docker 容器既作 Docker Daemon 又作为 Docker 客户端。在容器中 Docker 安装成功,但无法在容器中启动 Docker Daemon。比如用下面的 Dockerfile

构建出来的 Docker 镜像 my-docker, 然后启动容器, 再到容器中启动 Docker Daemon

$ docker run -it my-docker:latest sh
sh-5.2# systemctl start docker
System has not been booted with systemd as init system (PID 1). Can't operate.
Failed to connect to bus: Host is down
sh-5.2# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 03:32 pts/0 00:00:00 bash

在容器中无法启动 Docker Daemon,因为 systemd 不是 PID 为 1 的进程,PID 为 1 的进程是启动容器的 ENTRYPOINT 或 CMD 命令。但仍有两种方式可以使用容器中的 Docker Daemon, 那就是直接用 dockerd 命令启动, 但启动 Docker 容器的时候需要用 --privileged  参数

$ docker run -it --privileged my-docker:latest sh
sh-5.2# dockerd &
[1] 7
sh-5.2# INFO[2023-12-06T05:31:36.941163628Z] Starting up
......
INFO[2023-12-06T05:31:37.185826886Z] Loading containers: done.
INFO[2023-12-06T05:31:37.193146248Z] Docker daemon commit=5df983c graphdriver(s)=vfs version=20.10.25
INFO[2023-12-06T05:31:37.193347566Z] Daemon has completed initialization
INFO[2023-12-06T05:31:37.222365135Z] API listen on /var/run/docker.sock 

注:本文中用 $ 代表在容器外部执行命令,若提示符是 sh-5.2#/ # 则表示是在容器中执行命令。

然后在该容器中可以执行 docker 命令

sh-5.2# docker pull busybox
sh-5.2# docker images --format="{{.Repository}}"
busybox
sh-5.2# docker context ls --format="{{.DockerEndpoint}}"
unix:///var/run/docker.sock

可见在 Docker 容器中执行的 Docker 命令连接的 DOCKER_HOST 是本地的 /var/run/docker.sock。

那么我们是否能在外部连接容器内的 Docker Daemon 呢?下面尝试用卷映射的方式把容器内的 /var/run/docker.sock 写到容器外

$ mkdir -p var/run
$ docker run -it --privileged -v $(pwd)/var/run:/var/run my-docker:latest sh
bash-5.2# dockerd
INFO[2023-12-06T05:53:45.515410033Z] Starting up
failed to load listeners: can't create unix socket /var/run/docker.sock: chown /var/run/docker.sock: invalid argument

无法启动在 /var/run/docker.sock 文件上,不知道在哪里出了问题,或许仍然有解。不过可以再试试启动到 tcp://localhost:2375 上,启动容器的命令

$ docker run -it --privileged -p 2375:2375 my-docker:latest sh
bash-5.2# dockerd -H tcp://0.0.0.0:2375 &
[1] 7
bash-5.2# INFO[2023-12-06T06:01:34.313379814Z] Starting up
......
INFO[2023-12-06T06:01:50.615738590Z] Loading containers: done.
INFO[2023-12-06T06:01:50.626709327Z] Docker daemon commit=5df983c graphdriver(s)=vfs version=20.10.25
INFO[2023-12-06T06:01:50.626889526Z] Daemon has completed initialization
INFO[2023-12-06T06:01:50.663679142Z] API listen on [::]:2375
bash-5.2# docker ps
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?

但现在在容器中还不能直接用 docker 命令,因为它默认会连接 unix:///var/run/docker.sock,需要改为连接 tcp://localhost:2375,可用 docker context create 一个上下文再使用,或配置 DOCKER_HOST=tcp://localhost:2375 环境变量

bash-5.2# export DOCKER_HOST=tcp://localhost:2375
bash-5.2# docker ps --format "table {{.ID}}
CONTAINER ID

由于上面从外部映射了 2375 端口号到容器内部,所以可以从外部来执行 docker 命令

 可见在外部可通过 DOCKER_HOST=tcp://localhost:2375  切换连接到容器中的 Docker Daemon。由上面的 docker ps 命令的输出可知它们连接是两个完全隔离的 Docker Daemon。

以上无论是用 unix:///var/run/docker.sock 在容器中使用 docker 命令,还是用 tcp://0.0.0.0:2375 在容器内/外使用 docker 命令,实质上都是所谓的 Docker in Docker(DinD), 只是未直接使用官方的 docker:dind 镜像。

DinD 的方式总是需要使用 --privileged 参数,否则启动 dockerd 时报错

failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: iptables failed: iptables -t nat -N DOCKER: iptables v1.8.8 (nf_tables): Could not fetch rule set generation id: Permission denied (you must be root)

下面测试使用官方 docker:dind 镜像

启动 DinD 容器

$ docker run --privileged -e DOCKER_TLS_CERTDIR="" --name dockerd docker:dind
time="2023-12-06T06:44:54.128959986Z" level=info msg="Starting up"
......
time="2023-12-06T06:45:10.360177221Z" level=info msg="API listen on [::]:2375"
time="2023-12-06T06:45:10.360190342Z" level=info msg="API listen on /var/run/docker.sock"

同时在  tcp://0.0.0.0:2375 和  unix:///var/run/docker.sock 上监听。如果不加 DOCKER_TLS_CERTDIR 环境变量,容器中的 Docker Daemon 将会在 tcp://0.0.0.0:2376 和  unix:///var/run/docker.sock 上监听,注意 2376 端口上是 TLS 加密通信,需配置证书.

docker:dind 的 ENTRYPOINT 是 /usr/local/bin/dockerd-entrypoint.sh, 更灵活的控制如何在容器中启动 Docker Daemon 应阅读该脚本。

再启动一个容器来连接它(也可运行 docker:dind)

$ docker run -it --link dockerd:docker docker:latest sh
/ # echo $DOCKER_HOST
tcp://docker:2375
/ # docker ps --format "table {{.ID}}"
CONTAINER ID

--link dockerd:docker 命令是连接已有容器 dockerd, 并命作别名  docker, 这样在当前容器中可用 docker 访问到所链接的容器,因为在此容器中默认配置了 DOCKER_HOST=tcp://docker:2375 环境变量,所以 docker 命令将会使用前面的 DinD 容器作为  Docker Daemon。

Docker 命令与 Docker Daemon 是一种 C/S 的结构,当我们在执行 docker context ls 命令的时候可区分出当前 Docker 命令连接的是哪个 Docker Daemon(DOCKER_HOST)。

DinD 的另一种方式,使用 Sysbox 运行时

SysBox 是一种与 containerd、cri-o 类似的容器运行时。

与前两种容器嵌套方式不同的是,SysBox 从容器运行时的角度提供了新的解决方案,它可以在能够运行 systemd,docker,kubernetes 的容器内创建虚拟环境,而无需特权访问基础主机系统。

在使用前需要先安装 sysbox 运行时环境。sysbox 目前只支持 Linux 系统,安装方式点击链接 Installing Sysbox。在 Debian 系的 Linux 下基本安装过程炎

$ wget https://downloads.nestybox.com/sysbox/releases/v0.6.2/sysbox-ce_0.6.2-0.linux_amd64.deb
$ docker rm $(docker ps -aq) -f
$ sudo apt install jq
$ sudo apt install ./sysbox-ce_0.6.2-0.linux_amd64.deb

检查 sysbox 是否安装启动成功

$ sudo systemctl status sysbox -n20

一旦拥有sysbox运行时可用,您要做的就是使用 sysbox 运行时标志启动 docker 容器,如下所示。

在这里,我们使用的是官方 docker dind 映像。

容器启动后,就可以登录到容器中

不需要 --privileged 权限

使用 DooD 容器

所谓的 DooD, 是在启动容器时把当前宿主机 Docker 命令连接的 Unix 套接字文件映射到容器中,然后无论是在容器内外执行的 Docker 命令都会连接到宿主机的 Docker Daemon 上,所以它们看到或操作的是一样内容。

当前机器是一台 Mac, 安装的是 Docker Desktop, 用 docker context ls 命令查看

下面用命令切换 default 为默认 Context

接下来启动一个容器时把 /var/run/docker.sock 映射到容器内部

再回到容器外(宿主机),执行 docker ps

因此,无论是在容器的内外,Docker 命令最终都是连接到了宿主机的 /var/run/docker.sock 文件,即 DOCKER_HOST 是在宿主机上的 Docker Daemon,所以 docker ps 看到的容器 ID 都是一样的 b9d88eb40b40。

Docker Daemon 启动的实际上是一个 HTTP 服务器,Docker 命令便是一个 HTTP 客户端,所以与 docker pull busybox:latest 等价的 curl  命令是

$curl -XPOST --unix-socket /var/run/docker.sock http://localhost/images/create?fromImage=busybox&tag=latest

所谓的 DooD 好像没什么特别的,就是把现有 Docker Daemon 透传到容器中,如果 Docker Daemon 同时也监听在某个端口上,如 :2375 或 :2375, 也可以在容器内部访问该端口上的服务来使用 Docker 命令,比如 docker run --network HOST 与宿主机共享网络。

DooD 与容器本身功能没多大关系,也就启动的时候无须使用 --privileged 参数来增强权限。

Kubernetes 环境中使用 DinD 和 DooD

切换到 Kubernetes 环境,如果单单是运行一个  Docker 容器的话,没有太本质的区别,无非就是 docker run 换成了 docker apply xyz.yaml,相应的参数由命令行移至 yaml 文件中去。

DinD

dind.yaml

这样会在一个 Pod 中启动两个容器,它们共享同一个网络命名空间(-net=container),所以 docker-cli 可用  127.0.0.1 访问

部署

$ kubectl apply -f dind.yaml

部署后查看 Pod 名称

Pod 中运行了两个容器,分别是 dockerddocker-cli.

查看 dockerd 容器的日志

$ kubectl logs dind-6455ffdbdd-p6m4c -c dockerd | tail -n 3
time="2023-12-09T05:57:58.749429695Z" level=info msg="Daemon has completed initialization"
time="2023-12-09T05:57:58.790858356Z" level=info msg="API listen on /var/run/docker.sock"
time="2023-12-09T05:57:58.790875995Z" level=info msg="API listen on [::]:2375"

进到 docker-cli 容器

Kubernetes 一个 Deployment 只能部署一个 Pod, 要在同一个 yaml 文件中部署多个 Pod 的话可用 --- 把多份 Deployment 分隔开。 

DooD

Kubernetes 的 DooD 也是要把宿主机 Docker 的 /var/run/docker.sock 通过在部署文件中配置映射到容器中去

dood.yaml

部署

$ kubectl delete -f dind.yaml  # 把刚刚部署的容器销毁掉
$ kubectl apply -f dood.yaml

过会儿看到

$ kubectl get pods
NAME READY STATUS RESTARTS AGE
dood-644fcf4d68-ltn9k 1/1 Running 0 77s

进到容器

在容器内看到的 Image 都是 Kubernetes 生态环境的所使用镜像。

由于本机既安装了 Docker Desktop, 而 Kubernetes 环境是用的 Minikube, 所以在外部用 docker images 看到的镜像列表是不一样的,但用 kubectl 查看所启动容器用的 Image 是差不多一致的

$ kubectl get pods --all-namespaces -o=jsonpath='{range .items[*].spec.containers[*]}{.image}{"\n"}{end}' | sort -u
docker:latest
gcr.io/k8s-minikube/storage-provisioner:v5
registry.k8s.io/coredns/coredns:v1.10.1
registry.k8s.io/etcd:3.5.9-0
registry.k8s.io/kube-apiserver:v1.28.3
registry.k8s.io/kube-controller-manager:v1.28.3
registry.k8s.io/kube-proxy:v1.28.3
registry.k8s.io/kube-scheduler:v1.28.3

如果切换到使用 Docker Desktop 提供的 Kubernetes 的话,在容器里外用 docker images 看到的列表是一致的

docker ps  略有不同,在容器内显示了更多的容器,而在外部估计是 Docker Desktop 的 Kubernetes 有意隐藏了与 Kubernetes 众多相关的一些容器。 

链接:

  1. 如何在 Docker 中使用 Docker
  2. Docker in Docker (DinD)
  3. Docker In Docker
  4. Docker In Docker 容器嵌套
  5. 如何在Docker容器中运行Docker 「3种方法」
  6. 在Docker(容器的系统中)中运行Docker

本文链接 https://yanbin.blog/docker-container-as-daemon/, 来自 隔叶黄莺 Yanbin Blog

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

3 Comments
Inline Feedbacks
View all comments
mafeifan
3 months ago

哈哈,你文章的参考链接第3个 mafeifan 的编程技术分享 | mafeifan 的编程技术分享
我是博主,能否加个友链?

mafeifan
3 months ago
Reply to  Yanbin