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

1FROM amazonlinux:2023<br/>
2RUN dnf install -y docker procps-ng

构建出来的 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 命令

 1$ docker context ls --format="table {{.Name}}\t{{.DockerEndpoint}}"
 2NAME            DOCKER ENDPOINT
 3default         unix:///var/run/docker.sock
 4desktop-linux   unix:///Users/yanbin/.docker/run/docker.sock
 5$ docker ps --format "table {{.ID}}"
 6CONTAINER ID
 784ba4d462094
 84ad0a137b9ef
 9$ export DOCKER_HOST=tcp://localhost:2375
10$ docker context ls --format="table {{.Name}}\t{{.DockerEndpoint}}"
11NAME            DOCKER ENDPOINT
12default         tcp://localhost:2375
13desktop-linux   unix:///Users/yanbin/.docker/run/docker.sock
14$ docker ps --format "table {{.ID}}"
15CONTAINER ID

 可见在外部可通过 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 映像。

1docker run -itd --runtime=sysbox-runc --name sysbox-dind docker:dind

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

1# docker exec -it sysbox-dind /bin/sh
2/ # docker ps
3CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

不需要 --privileged 权限

使用 DooD 容器

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

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

1$ docker context ls
2NAME                TYPE                DESCRIPTION                               DOCKER ENDPOINT                              KUBERNETES ENDPOINT   ORCHESTRATOR
3default             moby                Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
4desktop-linux *     moby                Docker Desktop                            unix:///Users/yanbin/.docker/run/docker.sock

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

1$ docker context use default
2default
3Current context is now "default"
4$ docker context ls
5NAME                TYPE                DESCRIPTION                               DOCKER ENDPOINT                              KUBERNETES ENDPOINT   ORCHESTRATOR
6default *           moby                Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
7desktop-linux       moby                Docker Desktop                            unix:///Users/yanbin/.docker/run/docker.sock

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

 1docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker:latest sh
 2/ # docker context ls
 3NAME        DESCRIPTION                               DOCKER ENDPOINT               ERROR
 4default *   Current DOCKER_HOST based configuration   unix:///var/run/docker.sock
 5Warning: DOCKER_HOST environment variable overrides the active context. To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable.
 6/ # uname -a
 7Linux b9d88eb40b40 6.4.16-linuxkit #1 SMP PREEMPT_DYNAMIC Fri Nov 10 14:51:57 UTC 2023 x86_64 Linux
 8/ # docker ps
 9CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS           NAMES
10b9d88eb40b40   docker:latest   "dockerd-entrypoint.…"   13 seconds ago   Up 12 seconds   2375-2376/tcp   pensive_bell

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

1$ uname -a
2Darwin US-C02GG1BAMD6P 22.6.0 Darwin Kernel Version 22.6.0: Fri Sep 15 13:39:52 PDT 2023; root:xnu-8796.141.3.700.8~1/RELEASE_X86_64 x86_64
3$ docker ps
4CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS           NAMES
5b9d88eb40b40   docker:latest   "dockerd-entrypoint.…"   27 seconds ago   Up 26 seconds   2375-2376/tcp   pensive_bell

因此,无论是在容器的内外,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

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: dind
 5spec:
 6  replicas: 1
 7  selector:
 8    matchLabels:
 9      app: dind
10  template:
11    metadata:
12      labels:
13        app: dind
14    spec:
15      containers:
16        - name: dockerd
17          image: 'docker:dind'
18          env:
19            - name: DOCKER_TLS_CERTDIR
20              value: ""
21          securityContext:
22            privileged: true
23        - name: docker-cli
24          image: 'docker:latest'
25          env:
26          - name: DOCKER_HOST
27            value: 127.0.0.1
28          command: ["sleep"]
29          args: ["infinity"]

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

部署

$ kubectl apply -f dind.yaml

部署后查看 Pod 名称

1$ kubectl get pods
2NAME                    READY   STATUS    RESTARTS   AGE
3dind-6455ffdbdd-p6m4c   2/2     Running   0          15m
4$ kubectl get pod dind-6455ffdbdd-p6m4c -o jsonpath='{.spec.containers[*]}'  | jq ".name"
5
6"dockerd"
7"docker-cli"

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 容器

1kubectl exec -it dind-6455ffdbdd-p6m4c -c docker-cli -- sh
2/ # docker context ls
3NAME        DESCRIPTION                               DOCKER ENDPOINT        ERROR
4default *   Current DOCKER_HOST based configuration   tcp://127.0.0.1:2375
5Warning: DOCKER_HOST environment variable overrides the active context. To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable.
6/ # docker ps
7CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

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

DooD

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

dood.yaml

 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: dood
 5spec:
 6  replicas: 1
 7  selector:
 8    matchLabels:
 9      app: dood
10  template:
11    metadata:
12      labels:
13        app: dood
14    spec:
15      containers:
16        - image: docker:latest
17          name: docker-cli
18          securityContext:
19            privileged: false
20          command: ["sleep"]
21          args: ["infinity"]
22          volumeMounts:
23          - mountPath: /var/run/docker.sock
24            name: volume-docker
25      volumes:
26        - hostPath:
27            path: /var/run/docker.sock
28            type: ""
29          name: volume-docker

部署

$ 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

进到容器

 1$ kubectl get pods
 2NAME                    READY   STATUS    RESTARTS   AGE
 3dood-644fcf4d68-ltn9k   1/1     Running   0          77s
 4$ kubectl exec -it dood-644fcf4d68-ltn9k -c docker-cli -- sh
 5/ # docker images --format "{{.Repository}}:{{.Tag}}"
 6docker:dind
 7docker:latest
 8registry.k8s.io/kube-apiserver:v1.28.3
 9registry.k8s.io/kube-controller-manager:v1.28.3
10registry.k8s.io/kube-scheduler:v1.28.3
11registry.k8s.io/kube-proxy:v1.28.3
12registry.k8s.io/etcd:3.5.9-0
13registry.k8s.io/coredns/coredns:v1.10.1
14registry.k8s.io/pause:3.9
15gcr.io/k8s-minikube/storage-provisioner:v5

在容器内看到的 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 看到的列表是一致的

 1/ # docker images
 2REPOSITORY                                TAG       IMAGE ID       CREATED         SIZE
 3docker                                    latest    aa93deb4ad1b   11 days ago     330MB
 4gcr.io/k8s-minikube/kicbase               v0.0.42   dbc648475405   4 weeks ago     1.2GB
 5registry.k8s.io/kube-apiserver            v1.28.2   cdcab12b2dd1   2 months ago    126MB
 6registry.k8s.io/kube-controller-manager   v1.28.2   55f13c92defb   2 months ago    122MB
 7registry.k8s.io/kube-proxy                v1.28.2   c120fed2beb8   2 months ago    73.1MB
 8registry.k8s.io/kube-scheduler            v1.28.2   7a5d9d67a13f   2 months ago    60.1MB
 9registry.k8s.io/etcd                      3.5.9-0   73deb9a3f702   6 months ago    294MB
10registry.k8s.io/coredns/coredns           v1.10.1   ead0a4a53df8   10 months ago   53.6MB
11registry.k8s.io/pause                     3.9       e6f181688397   14 months ago   744kB

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's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。