Как я запускал Докер внутри Докера и что из этого получилось
Всем привет! В своей предыдущей статье, я обещал рассказать про запуск Докера в Докере и о практических аспектах применения этого занятия. Настало время выполнить свое обещание. Опытный девопс, пожалуй, возразит, что тем кому нужен Докер внутри Докера, просто пробрасывают сокет Докер демона из хоста внутрь контейнера и этого хватит в 99% случаев. Но не спешите кидать в меня печеньки, в речь пойдет о реальном запуске Докера внутри Докера. У этого решения много возможных областей применения и об одном из них эта статья, так что усаживайтесь поудобнее и выпрямите руки перед собой.
Начало
Все началось дождливым сентябрьским вечером, когда я чистил арендованную за $5 машинку на Digital Ocean, которая намертво повисла из-за того что Докер заполонил своими образами и контейнерами все 24 гигабайта доступного дискового пространства. Ирония была в том, все все эти образы и контейнеры были транзиентными и нужны были лишь для того чтобы тестировать работоспособность моего приложения каждый раз когда выходила новая версия какой-либо библиотеки или фреймворка. Я пробовал писать шелл-срипты и настраивать расписание крон для очистки мусора, но это не спасло: каждый раз все неминуемо заканчивалось тем, что дисковое пространство моего сервера оказывалось съеденым, а сервер подвисшим (в лучшем случае). В какой-то момент мне наткнулась статья про то как запускать Jenkins в контейнере и как он может создавать и удалять сборочные конвееры через проброшенный в него сокет докер демона. Идея мне приглянулась, но я решил пойти дальше и попробовать поэкспериментировать с непосредственным запуском Докера внутри Докера. Мне тогда казалось вполне логичным решением выкачивать докер образы и создавать контейнеры всех приложений которые мне нужны для тестирования внутри другого контейнера (давайте назовем его staging контейнер). Идея заключалась в том, чтобы запускать staging контейнер с флагом -rm, что автоматически удаляет весь контейнер со всем его содержимым при его остановке. Я покопался с докер образом от самого Докера (https://hub.docker.com/_/docker), но оно оказалось слишком громоздким и мне так и не удалось заставить его работать так как мне нужно и мне хотелось пройти весь путь самому.
Практика. Шишки
Я задался целью заставить контейнер работать так как мне было нужно и продолжал свои эксперименты, результатом которых стало несметное количество шишек. Итогом моего самоистязания стала следующий алгоритм:
Запускаем Докер контейнер в интерактивном режиме.
docker run --privileged -it docker:18.09.6
Обратите внимание на версию контейнера, шаг вправо или в лево и ваш ДинД превращается в тыкву. На самом деле, все ломается довольно часто с выходом новой версии.
Мы должны сразу попасть в шелл.Пробуем узнать, какие контейнеры запущены (Ответ: никакие), но давайте выполним команду все-равно:
docker ps
Вы будете немного удивлены, но оказывается Докер демон даже не запущен:
error during connect: Get http://docker:2375/v1.40/containers/json: dial tcp: lookup docker on 192.168.65.1:53: no such host
Давайте запустим его самостоятельно:
dockerd &
Еще одна неприятная неожиданность:
failed to start daemon: Error initializing network controller: error obtaining controller instance: failed to create NAT chain DOCKER: Iptables not found
Устанавливаем пакеты iptables и bash (в баше всяко работать приятнее чем в sh):
apk add --no-cache iptables bash
Запускаем bash. Наконец-то мы снова в привычном шелле
попробуем запустить Докер еще раз:
dockerd &
Мы должны увидеть длинную простыню логов заканчивающуюся:
INFO[2019-11-25T19:51:19.448080400Z] Daemon has completed initialization INFO[2019-11-25T19:51:19.474439300Z] API listen on /var/run/docker.sock
Нажимаем Enter. Мы снова в баше.
Понаблюдать за этим действом можно здесь (английский):
Начиная с этого момента мы можем пробовать запускать другие контейнеры внутри нашего Докер контейнера, но что если мы хотим поднять еще один Докер контейнер внутри нашего Докер контейнера или что-то пойдет не так и контейнер «вылетит»? Начинать все с начала.
Собственный DinD контейнер и новые эксперименты
Чтобы не повторять вышеописанные шаги снова и снова я создал собственный DinD контейнер:
https://github.com/alekslitvinenk/dind
Рабочее DinD решение дало мне возможность запускать Докер внутри Докера рекурсивно и проводить более смелые эксперименты.
Один такой (удачный) эксперимент с запуском MySQL и Nodejs я собираюсь сейчас описать.
Самые нетерпеливые могут посмотреть как это было здесь
Итак, начнем:
Запускаем DinD в интерактивном режиме. В данной версии DinD нам нужно вручную замапить все порты которые могут использовать наши дочерние контейнеры (я над этим уже работаю)
docker run --privileged -it \ -p 80:8080 \ -p 3306:3306 \ alekslitvinenk/dind
Мы попадаем в баш, откуда можем сразу приступать к запуску дочерних контейнеров.
Запускаем MySQL:
docker run --name mysql -e MYSQL_ROOT_PASSWORD=strongpassword -d -p 3306:3306 mysql
Подключаемся к базе данных так же как мы бы подключались к ней локально. Убеждаемся что все работает.
Запускаем второй контейнер:
docker run -d --rm -p 8080:8080 alekslitvinenk/hello-world-nodejs-server
Обратите внимание, что порт мапинг здесь будет именно 8080:8080, так как мы уже замапили порт 80 из хоста в родительский контейнер на порт 8080.
Идем на localhost в браузере, убеждаемся что сервер отвечает «Hello World!».
В моем случае эксперимент с вложенными Докер контейнерами оказался довольно положительным и я продолжу развивать проект и использовать его для стейджинга. Мне кажется, что это гораздо более легковесное решение чем тот же Kubernetes и Jenkins X. Но это мое субъективное мнение.
Я думаю, что для сегодняшней статьи — это все. В следующей статье я более подробно опишу эксперименты с рекурсивным запуском Докера в Докере и монтирование директорий вглубь вложенных контейнеров.
P.S. Если вы считаете данный проект полезным, то пожалуйста поставьте ему звездочку на ГитХабе, сделайте форк и расскажите друзьям.