突然间研究这个来的缘由是正在从 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
1 2 |
FROM amazonlinux:2023 RUN 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ docker context ls --format="table {{.Name}}\t{{.DockerEndpoint}}" NAME DOCKER ENDPOINT default unix:///var/run/docker.sock desktop-linux unix:///Users/yanbin/.docker/run/docker.sock $ docker ps --format "table {{.ID}}" CONTAINER ID 84ba4d462094 4ad0a137b9ef $ export DOCKER_HOST=tcp://localhost:2375 $ docker context ls --format="table {{.Name}}\t{{.DockerEndpoint}}" NAME DOCKER ENDPOINT default tcp://localhost:2375 desktop-linux unix:///Users/yanbin/.docker/run/docker.sock $ docker ps --format "table {{.ID}}" CONTAINER 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 映像。
1 |
docker run -itd --runtime=sysbox-runc --name sysbox-dind docker:dind |
容器启动后,就可以登录到容器中
1 2 3 4 |
# docker exec -it sysbox-dind /bin/sh / # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
不需要 --privileged 权限
使用 DooD 容器
所谓的 DooD, 是在启动容器时把当前宿主机 Docker 命令连接的 Unix 套接字文件映射到容器中,然后无论是在容器内外执行的 Docker 命令都会连接到宿主机的 Docker Daemon 上,所以它们看到或操作的是一样内容。
当前机器是一台 Mac, 安装的是 Docker Desktop, 用 docker context ls 命令查看
1 2 3 4 |
$ docker context ls NAME TYPE DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR default moby Current DOCKER_HOST based configuration unix:///var/run/docker.sock desktop-linux * moby Docker Desktop unix:///Users/yanbin/.docker/run/docker.sock |
下面用命令切换 default 为默认 Context
1 2 3 4 5 6 7 |
$ docker context use default default Current context is now "default" $ docker context ls NAME TYPE DESCRIPTION DOCKER ENDPOINT KUBERNETES ENDPOINT ORCHESTRATOR default * moby Current DOCKER_HOST based configuration unix:///var/run/docker.sock desktop-linux moby Docker Desktop unix:///Users/yanbin/.docker/run/docker.sock |
接下来启动一个容器时把 /var/run/docker.sock
映射到容器内部
1 2 3 4 5 6 7 8 9 10 |
docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker:latest sh / # docker context ls NAME DESCRIPTION DOCKER ENDPOINT ERROR default * Current DOCKER_HOST based configuration unix:///var/run/docker.sock Warning: DOCKER_HOST environment variable overrides the active context. To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable. / # uname -a Linux b9d88eb40b40 6.4.16-linuxkit #1 SMP PREEMPT_DYNAMIC Fri Nov 10 14:51:57 UTC 2023 x86_64 Linux / # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b9d88eb40b40 docker:latest "dockerd-entrypoint.…" 13 seconds ago Up 12 seconds 2375-2376/tcp pensive_bell |
再回到容器外(宿主机),执行 docker ps
1 2 3 4 5 |
$ uname -a Darwin 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 $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES b9d88eb40b40 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
apiVersion: apps/v1 kind: Deployment metadata: name: dind spec: replicas: 1 selector: matchLabels: app: dind template: metadata: labels: app: dind spec: containers: - name: dockerd image: 'docker:dind' env: - name: DOCKER_TLS_CERTDIR value: "" securityContext: privileged: true - name: docker-cli image: 'docker:latest' env: - name: DOCKER_HOST value: 127.0.0.1 command: ["sleep"] args: ["infinity"] |
这样会在一个 Pod 中启动两个容器,它们共享同一个网络命名空间(-net=container),所以 docker-cli 可用 127.0.0.1 访问
部署
$ kubectl apply -f dind.yaml
部署后查看 Pod 名称
1 2 3 4 5 6 7 |
$ kubectl get pods NAME READY STATUS RESTARTS AGE dind-6455ffdbdd-p6m4c 2/2 Running 0 15m $ kubectl get pod dind-6455ffdbdd-p6m4c -o jsonpath='{.spec.containers[*]}' | jq ".name" "dockerd" "docker-cli" |
Pod 中运行了两个容器,分别是 dockerd
和 docker-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
容器
1 2 3 4 5 6 7 |
kubectl exec -it dind-6455ffdbdd-p6m4c -c docker-cli -- sh / # docker context ls NAME DESCRIPTION DOCKER ENDPOINT ERROR default * Current DOCKER_HOST based configuration tcp://127.0.0.1:2375 Warning: DOCKER_HOST environment variable overrides the active context. To use a context, either set the global --context flag, or unset DOCKER_HOST environment variable. / # docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES |
Kubernetes 一个 Deployment 只能部署一个 Pod, 要在同一个 yaml 文件中部署多个 Pod 的话可用 ---
把多份 Deployment 分隔开。
DooD
Kubernetes 的 DooD 也是要把宿主机 Docker 的 /var/run/docker.sock 通过在部署文件中配置映射到容器中去
dood.yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
apiVersion: apps/v1 kind: Deployment metadata: name: dood spec: replicas: 1 selector: matchLabels: app: dood template: metadata: labels: app: dood spec: containers: - image: docker:latest name: docker-cli securityContext: privileged: false command: ["sleep"] args: ["infinity"] volumeMounts: - mountPath: /var/run/docker.sock name: volume-docker volumes: - hostPath: path: /var/run/docker.sock type: "" 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
$ kubectl get pods NAME READY STATUS RESTARTS AGE dood-644fcf4d68-ltn9k 1/1 Running 0 77s $ kubectl exec -it dood-644fcf4d68-ltn9k -c docker-cli -- sh / # docker images --format "{{.Repository}}:{{.Tag}}" docker:dind docker:latest registry.k8s.io/kube-apiserver:v1.28.3 registry.k8s.io/kube-controller-manager:v1.28.3 registry.k8s.io/kube-scheduler:v1.28.3 registry.k8s.io/kube-proxy:v1.28.3 registry.k8s.io/etcd:3.5.9-0 registry.k8s.io/coredns/coredns:v1.10.1 registry.k8s.io/pause:3.9 gcr.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 2 3 4 5 6 7 8 9 10 11 |
/ # docker images REPOSITORY TAG IMAGE ID CREATED SIZE docker latest aa93deb4ad1b 11 days ago 330MB gcr.io/k8s-minikube/kicbase v0.0.42 dbc648475405 4 weeks ago 1.2GB registry.k8s.io/kube-apiserver v1.28.2 cdcab12b2dd1 2 months ago 126MB registry.k8s.io/kube-controller-manager v1.28.2 55f13c92defb 2 months ago 122MB registry.k8s.io/kube-proxy v1.28.2 c120fed2beb8 2 months ago 73.1MB registry.k8s.io/kube-scheduler v1.28.2 7a5d9d67a13f 2 months ago 60.1MB registry.k8s.io/etcd 3.5.9-0 73deb9a3f702 6 months ago 294MB registry.k8s.io/coredns/coredns v1.10.1 ead0a4a53df8 10 months ago 53.6MB registry.k8s.io/pause 3.9 e6f181688397 14 months ago 744kB |
但 docker ps
略有不同,在容器内显示了更多的容器,而在外部估计是 Docker Desktop 的 Kubernetes 有意隐藏了与 Kubernetes 众多相关的一些容器。
链接:
- 如何在 Docker 中使用 Docker
- Docker in Docker (DinD)
- Docker In Docker
- Docker In Docker 容器嵌套
- 如何在Docker容器中运行Docker 「3种方法」
- 在Docker(容器的系统中)中运行Docker
本文链接 https://yanbin.blog/docker-container-as-daemon/, 来自 隔叶黄莺 Yanbin Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。
哈哈,你文章的参考链接第3个 mafeifan 的编程技术分享 | mafeifan 的编程技术分享
我是博主,能否加个友链?
已加,没看到你友链显示在哪个页面
已加 mafeifan 的编程技术分享 | mafeifan 的编程技术分享
非常感谢