Docker - DinD, DooD
Docker in Docker, Docker out of Docker
인터넷의 많은 글에서 DinD와 DooD의 개념을 혼용해서 사용하고 있습니다.
이 글에서는 DinD, DooD의 정확한 개념과 경우에 따라 어떤것을 선택해야 하는지에 대해 다룹니다.
우선 DinD와 DooD의 차이점을 제대로 이해하기 위해서는 Docker의 architecture가 어떻게 구성되어 있는지 알아야합니다.
(출처:https://docs.docker.com/get-started/overview/)
-
Docker Daemon (dockerd)
docker daemon은 Docker API를 처리하고 image, container, network, volume등 과 같은 docker object를 관리합니다. 대게 유저들은 dockerd에 직접 접근하여 사용할 일은 없습니다.
-
Docker Client (docker)
docker client는 user가
docker <command>
를 통해 docker를 사용하는 주요 서비스입니다. docker client는 사용자로부터 전달받은 명령어를 docker daemon (dockerd) 에게 전달합니다.
DinD와 DooD의 차이점은 Docker Daemon 을 어떻게 사용하냐에 따라 나뉜다고 말할 수 있습니다.
DinD (Docker in Docker)
들어가기 앞서 본론부터 말하자면, DinD를 쓰려고 하는 목적이 단순히 "자신의 서비스가 Docker container 안에서 실행되고 있고, 그 서비스에서 다른 Dockerized 된 container를 실행시키는것" 이라면 DinD 대신 DooD를 사용하는것이 맞습니다.
DinD 사용이 필요한 경우의 예시를 들자면, CI(Continuous Integration) 과정에서 빌드한 Docker Image의 Docker version 호환성 체크의 경우가 있을 수 있습니다. (Ex: Jenkins 등의 CI 서비스가 Host Docker 20.x.x 에서 실행되고있고, 이 CI에서 빌드한 image를 Docker 18.x.x에 대해 테스트해야할 때)
이런 특수한 경우가 아니라면, DinD는 후술할 여러가지 단점들 때문에 사용을 지양하는것이 좋습니다.
Concept
Docker in Docker는 말 그대로 Docker container 안에서 또 다른 Docker container를 실행시키는 것입니다.
위의 그림에서 Docker (Host) 에서 생성된 container 1, 2, 3은 서로 sibling 관계가 됩니다. 반면 Container1 에서 DinD로 생성된 container 4,5는 Host, container 2,3과는 전혀 연관성을 갖지 않고 대신 container1과 child 관계만 가지게 됩니다.
중요한 포인트는 Docker (Host)에 의해 생성된 DinD container를 생성하는 Container1 은 Host의 Docker 환경과 별개의 Docker client와 Docker daemon를 가진다는 것입니다. (container 1에서 docker version
명령어 실행 시 Host와 다른 버젼의 Docker Engine, Client 버젼이 표시됨.)
-
Result
# from container 3 Client: Docker Engine - Community Version: 18.09.8 API version: 1.39 Go version: go1.10.8 Git commit: 0dd43dd87f Built: Wed Jul 17 17:38:58 2019 OS/Arch: linux/amd64 Experimental: false Server: Docker Engine - Community Engine: Version: 18.09.8 API version: 1.39 (minimum version 1.12) Go version: go1.10.8 Git commit: 0dd43dd87f Built: Wed Jul 17 17:48:49 2019 OS/Arch: linux/amd64 Experimental: false ==================== # from Host Client: Docker Engine - Community Version: 20.10.14 API version: 1.41 Go version: go1.16.15 Git commit: a224086 Built: Thu Mar 24 01:47:57 2022 OS/Arch: linux/amd64 Context: default Experimental: true Server: Docker Engine - Community Engine: Version: 20.10.14 API version: 1.41 (minimum version 1.12) Go version: go1.16.15 Git commit: 87a90dc Built: Thu Mar 24 01:45:46 2022 OS/Arch: linux/amd64 Experimental: false containerd: Version: 1.5.11 GitCommit: 3df54a852345ae127d1fa3092b95168e4a88e2f8 nvidia: Version: 1.0.3 GitCommit: v1.0.3-0-gf46b6ba docker-init: Version: 0.19.0 GitCommit: de40ad0
(Container3: Engine, Client = 18.09.8, Host: Engine, Client = 20.10.14 )
이러한 특성으로인해 발생하는 문제점들과 이를 해결하기 위한 방법들이 DinD를 지양해야 하는 이유가 됩니다.
- Storage Mount
Host와 다른 docker daemon을 사용하고 있는 container1 에서는 Host의 dockerd에 configured 된 registry에 있는 image들, cache 등등을 사용할 수 없습니다. 이는 Host에 모든 image와 cache가 있음에도 불구하고, image를 새로 pull하고 build해야 하는 불편함이 생깁니다.
이를 해결할 방법으로 Host machine의 /var/lib/docker
폴더를 container1에 bind mount 하는 방법이 있지만, 이는 정상적으로 작동하는 것 처럼 보여도, single daemon에 의해 접근되는것을 전제로 동작하기 때문에 data corruption이 일어날 수 있다고 합니다.
(But try to do something more involved (pull the same image from two different instances…) and watch the world burn. 라고 하네요..)
그 밖에도 Outer docker (container1)는 normal file system (EXT4, BTRFS, ..) 위에서 작동하지만 Container1 안의 Inner docker(container4)는 copy-on-write file system (AUFS, BTRFS, ...) 위에서 작동하기 때문에 nested bind mount 등의 작업을 할 때 예상치 못한 문제가 발생할 수 있습니다.
- Security
DinD로 실행된 container는 LSM (Linux Security Module)은 AppArmor, SELinux 등과 같은 보안 프로필을 만드는데, 이 작업은 Outer docker와 충돌을 일으킵니다. 이러한 문제를 해결하기 위해서 DinD를 실행하려면 Outer Docker를 --previleged
옵션을 사용하여 run 해야합니다.
하지만 위의 옵션은 Docker container가 Host machine의 거의 동일한 액세스를 가지게 하기 때문에 보안에 매우 취약해집니다.
Code
DinD는 docker에서 공식적으로 제공하는 docker image를 통해 쉽게 사용할 수 있습니다.
자세한 방법은 아래 글과 동영상을 참고
https://hub.docker.com/_/docker
https://asciinema.org/a/378669
DooD (Docker out of Docker)
Concept
Docker out of Docker는 DinD와 달리 자신과 동일한 (sibling) 관계의 Docker container를 생성합니다. DooD로 생성된 Container 3은 Container1의 resource에 의존성을 갖지 않고, Host Docker에 종속됩니다.
이와 같은 결과는 Container1에서 Host machine의 /var/run/docker.sock
을 bind-mount 하고있기 때문입니다.
즉 Container1의 docker client가 사용자의 커맨드를 전달하는 대상이 Container1의 docker daemon이 아니라, Host Docker의 daemon이 됩니다.
DooD container를 생성하는 Container1 에서 docker version
명령어를 쳐보면
enerzai@f1e8d4fe8c6b:/app$ docker version
Client: Docker Engine - Community
Version: 20.10.14
API version: 1.40
Go version: go1.16.15
Git commit: a224086
Built: Thu Mar 24 01:48:21 2022
OS/Arch: linux/amd64
Context: default
Experimental: true
Server: Docker Engine - Community
Engine:
Version: 19.03.5
API version: 1.40 (minimum version 1.12)
Go version: go1.12.12
Git commit: 633a0ea838
Built: Wed Nov 13 07:28:22 2019
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.2.10
GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339
runc:
Version: 1.0.0-rc8+dev
GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
docker-init:
Version: 0.18.0
GitCommit: fec3683
위와 같이 Client만 자신의 것을 사용하고 Docker Engine(daemon) 은 Host Docker의 것인걸 확인할 수 있습니다.
따라서 DooD로 새로 만들어지는 Container는 Host docker에서 생성되는것과 동일하게 되고, 그렇기 때문에 앞서 설명한 DinD의 문제점들로부터 자유로워질 수 있습니다.
Code
DooD를 구현하는 방법은 위에 설명되어있듯이 docker image를 run 할 때 Host docker의 socket을 bind-mount하기만 하면 끝입니다.
$ docker run -it -v /var/run/docker.sock:/run/docker.sock <image>
하지만
막상 DooD를 생성하기 위해 Container 안에서 docker image를 run하면 permission 에러가 발생할 수 있습니다.
Dockerfile에서 user를 바꾸지 않았다면 기본적으로 root로 실행되는데 permission 에러가 왜 발생하는지 의문일 수 있지만,
이는 Host와 Container 시스템의 Group ID 의 차이때문에 발생하는 것입니다.
실제로 Host machine에서 docker.sock의 정보를 보면 아래와 같습니다.
srw-rw---- 1 root docker 0 Apr 18 17:58 docker.sock=
이 파일을 그대로 bind-mount 한 Container에서 docker.sock의 정보를 보면
srw-rw---- 1 root 999 0 Apr 18 17:58 docker.sock=
인것을 볼 수 있습니다.
따라서 DooD를 정상적으로 작동시키려면 Host와 Container의 docker gid 를 동일하게 맞춰주어야 합니다.
해당 과정은 image Dockerfile에 아래의 빌드과정을 추가하여 해결할 수 있습니다.
- Install docker on image (if not installed)
#Dockerfile
# setup docker
RUN apt-get update && apt-get install \
ca-certificates \
curl \
gnupg \
lsb-release -y
# Docker GPG key
RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
# set up stable version
RUN echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
$(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null
# Install docker
RUN apt-get update && apt-get install docker-ce docker-ce-cli containerd.io -y
- Set docker group id
docker group의 gid를 build arg를 통해서 받은 후 groupmod
명령어를 통해 gid를 바꿔줍니다.
#Dockerifle
ARG D_GROUP
RUN groupadd docker
RUN groupmod -g ${D_GROUP} docker
- Build
아래의 명령어를 통해 Host에서 docker group의 gid를 추출할 수 있습니다. 이를 build arg로 넘겨주어 image를 build할 때 사용합니다.
$ --build-arg D_GROUP=$(getent group docker | cut -d: -f3)
999 #Output
docker build --build-arg D_GROUP=$(getent group docker | cut -d: -f3) -t pyojuncode/ranix-model .
빌드 후 container에 접속하여 /var/run/docker.sock이 root:docker 로 되어있는지 확인합니다.
- Test and enjoy!
$ docker run -it -v /var/run/docker.sock:/var/run/docker.sock pyojuncode/ranix-model /bin/bash
# In docker
root@df052ef792d6:~# docker run busybox
# In local
# Check docker container is running
junpyo@beryl$ docker ps
Ref
https://docs.docker.com/get-started/overview/
https://hub.docker.com/_/docker/
https://sreeninet.wordpress.com/2016/12/23/docker-in-docker-and-play-with-docker/
https://techflare.blog/permission-problems-in-bind-mount-in-docker-volume/