Как запустить докер внутри докера?
Функция запуска 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.
Как запустить 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
Run Docker Container In Container: на что обратить внимание
Приведенные выше способы build a docker container in container являются некими экспериментами, и, конечно, хорошо, что Докер позволяет их делать. Но перед выполнением docker запуск контейнера в контейнере, обратите внимание на нюансы, которые стоит учесть.
- Проверьте, что use docker in docker ー единственный возможный способ решения вашей задачи. Сделайте proof of concept и тестирование, прежде чем переходить на метод контейнер-в-контейнере.
- Перед запуском контейнера в privileged mode убедитесь, что на это действие есть все разрешения по сетевой безопасности.
- При работе с нодами в Kubernetes есть нюансы.
- Если выбрали метод Sysbox, убедитесь, что на это есть апрув команды архитекторов и сетевой безопасности.
В качестве резюме этой можно сказать следующее: используйте опцию докер-в-докере, только если она действительно вам необходима. Проанализируйте, какой результат вы хотите получить в итоге, и если он возможен только с Docker-in-Docker, примените один из перечисленных в статье способов.
Важный момент: для использования этого подхода необходимо убедиться, что у вас есть на то разрешения от команды архитекторов и/или инженеров по безопасности. Подобные решения регламентируются как на корпоративном уровне, так и на уровне отдельных команд. Помните, что любой способ, не являющийся дефолтной фичей проекта, несет в себе риски для безопасности.