Как запустить докер внутри докера?

Функция запуска Docker-in-Docker изначально использовалась с целью развития самого Докера. Но, сталкиваясь с рядом сложностей в пайплайнах CI, многие стали применять ее для оптимизации работы Continuous Integration. Несколько лет назад запуск Докера в Докере выполняли только с -privileged flag. Сегодня ситуация изменилась в лучшую сторону и выбор решений Docker (как пользоваться и запускать контейнеры) стал гораздо обширнее.

В этой статье:

  • ответим на главный вопрос ー “Docker: how to run container in container”;
  • разберем сценарии, зачем вообще нужно делать rundocker-in-docker;
  • рассмотрим плюсы и минусы этого подхода;
  • приведем примеры.

Вся правда про Docker-in-Docker

Ранее типичный цикл билда в Докере выглядел так: делаем build, останавливаем Docker daemon, запускаем новый, тестируем и повторяем. С изобретением Docker-in-Docker весь этот долгий процесс упростился: делаем build и run одним шагом, повторяем. Выглядит неплохо, правда?

Но вопреки всем прелестям, у Docker-in-Docker есть свои минусы. Например, Linux Security Modules (AppArmor или SELinux). Если делать start docker container, то «внутренний» docker контейнер может запустить профайлы безопасности. В итоге получаем конфликт с «внешним» контейнером. Это было одной из наиболее частых проблем при реализации start a docker image с -privileged flag.

Еще один нюанс: линковка со storage drivers. При запуске run docker container in Docker «внешний» контейнер запускается сверху системы, а «внутренний» ー сверху copy-on-write. К примеру, нельзя запустить AuFS сверху этой же системы. При запуске BTRFS сверху BTRFS вначале эта схема может сработать. Но при добавлении сабвольюмов удаление parent subvolume выдаст ошибку.

И еще немного о сложностях, как использовать Докер в Докере, а именно ー о build cache. Частый вопрос «Как запустить образ докера на хосте и не делать пул во внутренний Docker?». Как вариант, можно применить /var/lib/docker от хоста к контейнеру (иногда даже к нескольким). Но изначально daemon докера был разработан с уникальным доступом к библиотеке, и ничего не должно было вмешиваться в этот путь. Docker трансформировался из dotCloud, и в те времена движок контейнера работал с множественными процессами by default, одновременно стучась к /var/lib/dotcloud. Сколько было проведено экспериментов разработчиками проекта! И удачным исходом рефакторинга движка стал Docker daemon, который объединил в себе всю операционку контейнера.

Тем не менее, если попробовать расшарить директорию /var/lib/docker между несколькими сущностями докера, нас ждет фейл. На самом деле, решение будет работать, но если вы решите затянуть докер образ из двух разных сущностей докера, весь мир разрушится… Каждый раз выполняя эту команду, кэш будет просто взрываться.

Docker: как работает Sysbox

После очередного ряда экспериментов разработчики проекта пришли к одному вполне удачному решению ー sysbox. Это container runtime проект с открытым исходным кодом (следующее поколение “runc”). ПО позволяет запускать system докер контейнеры, которые нуждались бы в privileged flag, но по факту работают без него. Sysbox дает возможность реализовать адекватную изоляцию докер контейнеров, а также контейнеров и их хоста.

Sysbox x Docker: create container-in-container 

В sysbox есть и другие возможности оптимизации запуска контейнера-в-контейнере, в частности, когда запускаем множество сущностей Докера. Их можно «засеять» общим набором Docker images. Это существенно сэкономит дисковое пространство (и время). А еще это оптимальный вариант при запуске нод Kubernetes в контейнерах. Подытожим: если ваш случай очень требует run Docker container-in-container, обратите внимание на sysbox.

Docker: create container Docker-in-Docker via socket

Мы уже проговорили, что изначально опция docker create container-in-container не была предусмотрена, а появилась в результате экспериментов по оптимизации процессов. Поэтому, предлагаем посмотреть на задачу детальнее: ваша цель ー знать как запустить Docker контейнер в контейнере? Или запускать Docker from CI, когда сама CI запущена в контейнере? На практике в большинстве случаев необходим именно второй вариант. Простыми словами, мы хотим, чтобы наша Continuous Integration система сама стартовала docker from container по типу Jenkins, например. Оптимальным решением будет затянуть Docker socket к контейнеру CI, сконнектив их посредством -v flag:
docker run -v /var/run/docker.sock:/var/run/docker.sock ...
Таким образом, контейнер сможет коннектиться с Docker socket и выполнять start docker container.

Docker in Docker dind

Метод Docker dind используется, когда мы хотим создать containers / images внутри контейнера. Фактически мы создаем дочерний контейнер внутри родительского. Если у вас другая задача, этот метод не сработает. Итак, разбираем как запустить Докер контейнер внутри контейнера, применив Docker dind. Для этого нужно выполнить команду run docker image ー образ будет запечатан вместе с необходимыми утилитами для запуска Докера внутри контейнера.

Log into docker container и запуск dockerfile

Обратите внимание: в этом решении контейнер должен работать в режиме privileged. Рассмотрим пошагово как запустить docker внутри контейнера.

Создаем контейнер, называем его, к примеру, dind-test с образом docker:dind image.

docker run --privileged -d --name dind-test docker:dind

Выполняем log into docker container, используя exec.

docker exec -it dind-test /bin/sh

Следующее действие: будучи внутри контейнера, запустите команду:

docker pull ubuntu

В списке всех docker images на хостовой виртуальной машине отобразится ubuntu image.

docker images

Теперь выполняем в тестовой директории docker run dockerfile

mkdir test && cd test
vi Dockerfile

Копируем содержимое dockerfile, чтобы проверить билд образа внутри контейнера:

FROM ubuntu:18.04
LABEL maintainer="Bibin Wilson <[email protected]>"
RUN apt-get update && \

apt-get -qy full-upgrade && \

apt-get install -qy curl && \

apt-get install -qy curl && \

curl -sSL https://get.docker.com/ | sh

Выполняем в docker run dockerfile:

docker build -t test-image.

Начиная с версии 18.09, при запуске run docker image dind, будет автоматически сгенерирован TLS сертификат в директории, определенной переменной DOCKER_TLS_CERTDIR. До версии 18.09 эта опция отсутствовала. 

Если Docker daemon активирован, выполняем starting docker daemon с командой –host=tcp://0.0.0.0:2376 –tlsverify …. Когда имеем ситуацию “docker daemon is not running”, запускаем его через –host=tcp://0.0.0.0:2375.

Теперь вы знаете еще один способ, как работает docker в docker. Далее перейдем к решению, которое предлагает Docker Hub ー как запустить контейнер Docker-in-Docker. 

Docker-in-Docker практические рекомендации_Banner 1

Как запустить dockerfile (Docker Hub Official Image)

Комьюнити, поддерживающее проект, не особо приветствует использование Docker-in-Docker, если на это нет веских обоснованных причин. Но учитывая, что практика распространенная, ребята из Docker Hub решили дать свои рекомендации, как запустить dockerfile методом контейнер-в-контейнере.

Start a docker image as Docker-in-Docker

Основываясь на решении разработчиков Docker Hub, разберем способ docker-in-docker start container from image (official image). 

Запускаем daemon: 

$ docker run --privileged --name some-docker -d \

--network some-network --network-alias docker \

-e DOCKER_TLS_CERTDIR=/certs \

-v some-docker-certs-ca:/certs/ca \

-v some-docker-certs-client:/certs/client \

docker:dind

Флаг –privileged обязателен в этом случае, но запускать Docker Ubuntu Container в контейнере с ним нужно аккуратно. Он открывает полный доступ к хостовому окружению. Соответственно, выполняя docker run container from image с privileged flag, не забывайте про безопасность.

Connect (log into) docker container 

Теперь переходим к подключению Докера от второго контейнера:

$ docker run --rm --network some-network \
-e DOCKER_TLS_CERTDIR=/certs \

-v some-docker-certs-client:/certs/client:ro \

docker:latest version

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

$ docker run -it --rm --network some-network \

-e DOCKER_TLS_CERTDIR=/certs \

-v some-docker-certs-client:/certs/client:ro \

docker:latest sh

/ # docker version

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

$ docker run --rm --network some-network \

-e DOCKER_TLS_CERTDIR=/certs \

-v some-docker-certs-client:/certs/client:ro \

docker:latest info

Containers: 0

 Running: 0

 Paused: 0

 Stopped: 0

Images: 0

Server Version: 18.09.8

Storage Driver: overlay2

 Backing Filesystem: extfs

 Supports d_type: true

 Native Overlay Diff: true

Logging Driver: json-file

Cgroup Driver: cgroupfs

Plugins:

 Volume: local

 Network: bridge host macvlan null overlay

 Log: awslogs fluentd gcplogs gelf journald json-file local logentries splunk syslog

Swarm: inactive

Runtimes: runc

Default Runtime: runc

Init Binary: docker-init

containerd version: 894b81a4b802e4eb2a91d1ce216b8817763c29fb

runc version: 425e105d5a03fabd737a126ad93d62a9eeede87f

init version: fec3683

Security Options:

 apparmor

 seccomp

Profile: default

Kernel Version: 4.19.0-5-amd64

Operating System: Alpine Linux v3.10 (containerized)

OSType: linux

Architecture: x86_64

CPUs: 12

Total Memory: 62.79GiB

Name: e174d61a4a12

ID: HJXG:3OT7:MGDL:Y2BL:WCYP:CKSP:CGAM:4BLH:NEI4:IURF:4COF:AH6N

Docker Root Dir: /var/lib/docker

Debug Mode (client): false

Debug Mode (server): false

Registry: https://index.docker.io/v1/

Labels:

Experimental: false

Insecure Registries:

 127.0.0.0/8

Live Restore Enabled: false

Product License: Community Engine

WARNING: bridge-nf-call-iptables is disabled

WARNING: bridge-nf-call-ip6tables is disabled

$ docker run --rm -v /var/run/docker.sock:/var/run/docker.sock docker:latest version

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.7

API version: 1.39 (minimum version 1.12)

Go version: go1.10.8

Git commit: 2d0083d

Built: Thu Jun 27 17:23:02 2019

OS/Arch: linux/amd64

Experimental: false
Docker-in-Docker практические рекомендации_Banner 4

Run Docker Container In Container: на что обратить внимание

Приведенные выше способы build a docker container in container являются некими экспериментами, и, конечно, хорошо, что Докер позволяет их делать. Но перед выполнением docker запуск контейнера в контейнере, обратите внимание на нюансы, которые стоит учесть.

  1. Проверьте, что use docker in docker ー единственный возможный способ решения вашей задачи. Сделайте proof of concept и тестирование, прежде чем переходить на метод контейнер-в-контейнере.
  2. Перед запуском контейнера в privileged mode убедитесь, что на это действие есть все разрешения по сетевой безопасности. 
  3. При работе с нодами в Kubernetes есть нюансы. 
  4. Если выбрали метод Sysbox, убедитесь, что на это есть апрув команды архитекторов и сетевой безопасности.

В качестве резюме этой можно сказать следующее: используйте опцию докер-в-докере, только если она действительно вам необходима. Проанализируйте, какой результат вы хотите получить в итоге, и если он возможен только с Docker-in-Docker, примените один из перечисленных в статье способов.

Важный момент: для использования этого подхода необходимо убедиться, что у вас есть на то разрешения от команды архитекторов и/или инженеров по безопасности. Подобные решения регламентируются как на корпоративном уровне, так и на уровне отдельных команд. Помните, что любой способ, не являющийся дефолтной фичей проекта, несет в себе риски для безопасности.

error: Контент защищен.