# Docker In Docker 容器嵌套

Docker in Docker 自定义开源项目: <https://gitee.com/cncfstack/docker-in-docker/tree/master/dind>

![docker-in-docker-logo](../assets/images/docker-in-docker-logo.drawio.png)

容器相比于 VM 虚拟机是属于弱隔离,在某些场景下需要增强容器的隔离或扩展应用场景来解决新的问题,如下场景在云原生实践过程中也经常会遇见,本文收集并分析了目前主流的容器嵌套方法,可根据当前团队的现状来综合评估后选择。

- 使用容器来提供传统的操作系统运行环境。VMLC(类虚拟机容器) 是 VM Like Container 的缩写,其设计目标是为开发者在容器中提供类似虚拟机的环境,包括:systemd服务管理能力,sshd远程登录能力,docker/k8s嵌套能力等。
- 在容器中嵌套运行容器,提供沙盒环境。
- 在容器/POD 中运行 Kubernetes 集群。
- 在流水线执行时任务本身是封装在容器中,但是流水线的执行过程可能会包含镜像的拉取、构建、上传、运行等任务场景。
- 在 Jenkins 中,使用 Docker 容器作为构建的 Slave 节点。
- 在短期使用环境运行容器时,例如学习容器的基本使用,可以快速创建和回收环境,并减少资源的消耗。
- 在产品快速体验或 POC 场景下,将容器服务运行在容器中进行打包,快速部署运行。

在 Docker 中运行 Docker 目前可提供三类方法

- 方法1:共享宿主机 dockerd 服务
- 方法2:独立隔离的容器运行环境
- 方法3:使用 Sysbox 运行时

## 方法1:共享宿主机 dockerd 服务

dockerd 服务在启动时会提供基于 socket 的连接方式,docker client 客户端可以选择使用 Socket 文件或 TCP/IP 的方式连接 Socket 来访问 dockerd 进行使用和管理功能,默认会使用 `/var/run/docker.sock` 的 socket 文件接口方式连接。

当在宿主机上运行容器 *Container0* 后,需要在该容器内继续运行 docker 命令来使用容器功能,可以参考如下步骤:
- 准备 *Container0* 的容器镜像,该镜像提前安装好 docker 客户端命令;或者也可以直接使用 docker 官方提供的镜像 `docker:latest`
- 基于上述镜像来运行 *Container0* 的容器,该步骤需要配置宿主机 dockerd 连接方式。
- 在 *Container0* 中使用 docker 客户端命令进行容器的使用和管理

容器的嵌套和管理示意图:

![docker-in-docker-share](../assets/images/docker-in-docker-share.drawio.png)

- 1、在宿主机上创建出 *Container0* 后,可以通过 `docker exec` 进入到该容器
- 2、在 *Container0* 容器内继续创建/访问新的容器 *Container3* ,可以再次使用 `docker exec` 进入到 *Container3* 容器中
- 3、通过两次的 `docker exec` 进入到嵌套的内容容器
- 4、在共享 dockerd 方法下,*Container3* 虽然是在 *Container0* 中创建和使用管理,但实际上还是运行在宿主机上的 dockerd 系统中
- 5、通过 socket 或 TCP/IP 的方式将宿主机的 dockerd 挂载到 *Container0* 容器中。

该方法的容器嵌套命令参考

```
sudo docker run \
    -v /var/run/docker.sock:/var/run/docker.sock \
    --name docker0 \
    -ti docker
```

执行输出内容:

```
# sudo docker run \
>     -v /var/run/docker.sock:/var/run/docker.sock \
>     -ti docker

/ # docker ps |grep docker0
b326055722fb   docker                                                                "docker-entrypoint.s…"   29 seconds ago   Up 28 seconds                                                                                                                                                  docker0
/ #
```

这里可以看到进入到容器内后,使用 docker 命令还可以看到当前容器本身。​也使用 `docker version` 可以看到客户端和服务端是两个不同版本的 docker服务。


实例中使用的 socket 文件的方式连接,如果运行的容器和 dockerd 不在同一台机器,或者本地文件没有权限挂载,也可以通过指定容器的 IP 地址和端口的方式连接到 dockerd ,客户端连接参数 `-H, --host list          Daemon socket(s) to connect to`。

## 方法2: 独立隔离的容器运行环境

一般情况下操作系统在运行时会启动很多系统管理的服务,如 systemd;但容器启动时只有一个业务服务进程,即使安装了一些软件也无法像在操作系统中那样正常启动。

如下,是在 centos7 的基础镜像中安装了 docker 服务,在没有进行特殊改造的情况下,是无法来启动 docker.service 服务的。

```
[root@9952f799dab1 /]# systemctl start docker
Failed to get D-Bus connection: Operation not permitted
```

本篇文章不会深入的介绍如何构建这样的镜像,因为社区已经提供了相对完整的解决方案。这里主要提供
- 基于 Docker 提供的 `docker:dind`
- 基于 minikbue 提供的 `kicbase:v0.0.32`

这种方式提供的容器中的容器运行环境和宿主机上的 dockerd 是相互独立的

![docker-in-docker-dind](../assets/images/docker-in-docker-dind.drawio.png)

- 1、在宿主机上创建出 *Container0* 后,可以通过 `docker exec` 进入到该容器
- 2、在 *Container0* 容器内继续创建/访问新的容器 *Container3* ,可以再次使用 `docker exec` 进入到 *Container3* 容器中
- 3、通过两次的 `docker exec` 进入到嵌套的内容容器


### 基于 Docker 提供的 `docker:dind`

该镜像的目标是提供 docker 运行环境,因此启动的容器中主要功能就是 docker 服务,其他 systemd 的相关服务是无法正常运行的。

```
docker run -itd \
    --name dind-test \
    --privileged  docker:dind
```

运行示例:

```
# docker run -itd \
>     --name dind-test \
>     --privileged  docker:dind
4e2b702710f742f34b59683542c64724bb36502daa23115c537d25223c4ba138

# docker exec -it dind-test sh

/ # docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

### 基于 minikbue 提供的 `kicbase`

kicbase 提供了完整的隔离环境,也提供 systemd 的系统服务的管理,和 VM 虚拟机使用体验很相似,也可以安装启动其他 systed 服务。

minikube 社区使用 kicbase 就用来提供类似虚拟化 VM 的驱动,使用该驱动来运行 Kubernetes 集群的。

该容器启动时默认
```
docker run -itd \
  --name virtualvm1 \
  --privileged \
  -p 20022:22 \
  registry.cn-hangzhou.aliyuncs.com/google_containers/kicbase:v0.0.32
```

启动实例

```
​[minikube@localhost ~]$ docker run -itd \
>   --name virtualvm1 \
>   --privileged \
>   -p 20022:22 \
>   registry.cn-hangzhou.aliyuncs.com/google_containers/kicbase:v0.0.32
a06cc6ee28ebcc3eb5acfe7ac3f0fe171316693d67e68438e3c161efb043f98e​
​​
[minikube@localhost ~]$ docker exec -it virtualvm1 bash
​
root@a06cc6ee28eb:/# docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
​
root@a06cc6ee28eb:/# ps auxf
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         350  0.0  0.0   4248  3404 pts/1    Ss   04:26   0:00 bash
root         359  0.0  0.0   5896  2960 pts/1    R+   04:26   0:00  \_ ps auxf
root           1  0.5  0.0  21252 10168 ?        Ss   04:26   0:00 /sbin/init
root         201  0.1  0.0  27472  8420 ?        S<s  04:26   0:00 /lib/systemd/systemd-journald
message+     212  0.0  0.0   7000  3640 ?        Ss   04:26   0:00 /usr/bin/dbus-daemon --system --address=systemd: --nofork --nopidfile --systemd-activation
root         215  0.4  0.1 2021320 55556 ?       Ssl  04:26   0:00 /usr/bin/containerd
root         224  0.0  0.0  12176  7008 ?        Ss   04:26   0:00 sshd: /usr/sbin/sshd -D [listener] 0 of 10-100 startups
root         238  0.5  0.2 1752512 76360 ?       Ssl  04:26   0:00 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
```

kicbase 的容器默认会启动 init、systemd、dbus、docker、sshd 等服务,在容器内的使用体验就是一个 Linux 操作系统环境。

使用这两种方式运行容器嵌套都需要提供给 `--privileged` 参数来使用特权权限,该权限在不安全的网络环境中提供服务时具有一定的风险,如若考虑安全可以使用 docker rootless 的方式运行


## 方法3: 使用 Sysbox 运行时

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

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

在使用前需要先安装 sysbox 运行时环境。请参阅此页面以获取有关安装sysbox运行时的最新官方说明[1]。

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

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

```
docker run -itd \
  --runtime=sysbox-runc \
  --name sysbox-dind docker:dind
```

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

```
# docker exec -it sysbox-dind /bin/sh

/ # docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES
```

## 总结

本文提供了三种从不同思路提供了容器嵌套的解决方案。

使用共享宿主机 dockerd 的方式是能够快速理解和使用的一种方式,不需要进行特殊的调整和改造,在使用上可以解决一些容器嵌套的需求,但是没有提供较好的隔离。

使用独立隔离的容器运行环境其实是目前相对较好的选择,他可以提供相对独立的隔离环境,特别是 minikbue 提供的 kicbase,基本实现了基于容器的 VM 虚拟机实现;但是这种方式需要 root 特权,在网络安全要求较高的情况下需要进行特殊的处理。

使用 Sysbox Runtime 运行时的方案解决了上面两种独立和安全的问题,但 Sysbox 目前并非主流的容器运行时,并没有大众广泛接受。


## 参考
- <https://hub.docker.com/_/docker>
- <https://devopscube.com/run-docker-in-docker/>
- https://github.com/nestybox/sysbox
- [sysbox运行时安装](https://github.com/nestybox/sysbox#installing-sysbox)
- [使用 Docker 作为 Jenkins Build 的 Slave](https://devopscube.com/docker-containers-as-build-slaves-jenkins/)