Как сократить время сборки образов Docker в GitLab CI
Современный цикл разработки программного обеспечения зачастую подразумевает, что ваши приложения регулярно упаковываются в контейнеры. Эта задача может занимать много времени, чем может значительно замедлять ваше тестирование или развертывание. Проблема становится особенно очевидной в контексте процесса непрерывной интеграции и развертывания, когда образы пересобираются при каждом изменении в коде
В этой статье мы обсудим различные способы ускорения сборки образов Docker в конвейере непрерывной интеграции путем реализации различных стратегий.
Локальная упаковка приложения
В качестве примера мы возьмем достаточно простое приложение Python Flask:
from flask import Flask app = Flask(__name__) @app.route('/') def hello_world(): return 'Hello, World!'
Написание Dockerfile
Давайте напишем соответствующий Dockerfile:
FROM python:3.7-alpine as builder # установка зависимостей, необходимых для сборки пакетов python RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip # настройка venv и загрузка или сборка зависимостей ENV VENV="/venv" ENV PATH="$/bin:$" COPY requirements.txt . RUN python -m venv $ \ && pip install --no-cache-dir -r requirements.txt FROM python:3.7-alpine # настройки venv с зависимостями с этапа компоновки ENV VENV="/venv" ENV PATH="$/bin:$PATH" COPY --from=builder $ $ # копирование файлов приложения WORKDIR /app COPY app . # запуск приложения EXPOSE 5000 ENV FLASK_APP="hello.py" CMD [ "flask", "run", "--host=0.0.0.0" ]
Здесь вы можете наблюдать классический многоступенчатый процесс сборки:
- Мы начинаем с легкого базового образа, в который мы устанавливаем инструменты сборки и загружаем или компилируем зависимости в виртуальную среду Python.
- На втором этапе мы копируем виртуальную среду с нашими зависимостями в целевой образ и, наконец, добавляем файлы приложения.
Почему этот процесс выполняется в два этапа? Во-первых, потому что вы получаете секьюрный процесс сборки, поскольку он выполняется в контейнере без вмешательства среды хоста. Во-вторых, вы получаете облегченный финальный образ без всех библиотек сборки, а только с тем, что требуется для запуска приложения.
Запуск и тестирование образа.
Убедитесь, что все работает должным образом:
docker build -t hello . docker run -d --rm -p 5000:5000 hello curl localhost:5000 Hello, World!
Если вы запустите команду docker build во второй раз:
docker build -t hello . . Step 2/15 : RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip ---> Using cache ---> 24d044c28dce .
Как видите, вторая сборка происходит намного быстрее, так как слои кэшируются в вашей локальной службе Docker и используются повторно, если в них не вносили изменений.
Отправка образа
Давайте опубликуем наш образ во внешнем реестре и посмотрим, что произойдет:
docker tag hello my-registry/hello:1.0 docker push my-registry/hello:1.0 The push refers to repository [my-registry/hello] 8388d558f57d: Pushed 77a59788172c: Pushed 673c6888b7ef: Pushed fdb8581dab88: Pushed 6360407af3e7: Pushed 68aa0de28940: Pushed f04cc38c0ac2: Pushed ace0eda3e3be: Pushed latest: digest: sha256:d815c1694083ffa8cc379f5a52ea69e435290c9d1ae629969e82d705b7f5ea95 size: 1994
Обратите внимание, как каждый из промежуточных слоев идентифицируется хэшем. Мы можем насчитать 8 слоев, потому что у нас есть ровно 8 docker команд в Dockerfile поверх нашей последней инструкции FROM.
Важно понимать, что слои из нашего базового образа компоновщика не отправляются в удаленный реестр Docker, когда мы пушим наш образ, пушатся только слои из последнего этапа. Однако промежуточные слои по-прежнему кэшируются в локальном демоне Docker и их можно повторно использовать для вашей следующей локальной команды сборки.
С локальной сборкой все достаточно просто, давайте теперь посмотрим, как это будет работать в CI среде.
Сборка образа Docker в контексте CI конвейера
В реальных условиях сборка и отправка образов Docker не всегда выполняется локально, как мы только что это делали. Обычно она выполняется внутри платформы непрерывной интеграции и развертывания. Перед развертыванием приложения вам нужно собирать и пушить свой образ при каждом изменении в коде. Естественно время сборки имеет решающее значение, так как вам нужен максимально быстрый цикл обратной связи.
Тестовая CI среда
Мы реализуем CI среду, используя:
- GitLab.com CI
- Kubernetes Executor для хостинга GitLab Runner
Последний пункт важен, потому что наши CI задачи будут выполняться в контейнерной среде. Учитывая это, каждая задача создается в виде пода Kubernetes. Каждое современное CI решение использует контейнерные задачи, и при создании Docker контейнеров все они сталкиваются с одной и той же проблемой: вам нужно заставить Docker команды работать внутри Docker контейнера.
Чтобы все прошло гладко, у вас есть два пути:
- Забиндить /var/run/docker.sock , который слушает демон Docker, сделав демон хоста доступным для нашего контейнера задач.
- Использовать дополнительный контейнер, запускающий «Docker in Docker» (также известный как dind) вместе с вашей задачей. Dind — это особый вариант Docker, работающий с привилегиями и настроенный для работы внутри самого Docker ?
Для нашего примера мы будем использовать второй вариант.
Реализация GitLab конвейера
В GitLab конвейере обычно вы создаете служебные контейнеры, такие как DinD, с помощью ключевого слова service.
В приведенном ниже фрагменте конвейера и задача docker-build, и служебный dind контейнер будут выполняться в одном и том же поде Kubernetes. Когда в сценарии задачи используется docker, он отправляет команды вспомогательному dind контейнеру благодаря переменной среды DOCKER_HOST .
stages: - build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" # адрес localhost используется как контейнером задачи, так и dind контейнером (поскольку они используют один и тот же под) # Таким образом, при выполнении команд Docker эта конфигурация делает службу dind нашим демоном Docker DOCKER_HOST: "tcp://localhost:2375" services: - docker:stable-dind docker-build: image: docker:stable stage: build script: - docker build -t hello . - docker tag my-registry/hello:$ - docker push my-registry/hello:$
Запуск конвейера
Этот конвейер должен работать нормально. Запустив его один раз и проверив вывод задачи, мы получим:
docker build -t hello . Step 1/15 : FROM python:3.7-alpine as builder . Step 2/15 : RUN apk update && apk add --no-cache make gcc && pip install --upgrade pip ---> Running in ca50f59a21f8 fetch http://dl-cdn.alpinelinux.org/alpine/v3.12/main/x86_64/APKINDEX.tar.gz .
Поскольку мы впервые создаем наш контейнер, каждый слой создается путем выполнения команд. Общее время выполнения задачи составляет около 1 минуты.
Если вы запустите конвейер во второй раз, ничего не меняя, вы должны увидеть то же самое: каждый слой перестраивается! Когда мы запускали наши команды сборки локально, кэшированные слои использовались повторно. А здесь — нет. Для такого простого образа это на самом деле не имеет значения, но в реальной жизни, где для создания некоторых образов могут потребоваться десятки минут, это может стать настоящей проблемой.
Почему так происходит? Просто потому, что в этом случае dind — это временный контейнер, который создается вместе с задачей и умирает после ее завершения, поэтому все кэшированные данные теряются. К сожалению, вы не можете легко сохранить данные между двумя запусками конвейера.
Как же нам получить выгоду от кэширования и по-прежнему использовать Dind-контейнер?
Использование кэша Docker вместе с Docker in Docker
Первое решение: Pull/Push танцы
Первое решение довольно простое: мы будем использовать наш удаленный реестр (тот, в который мы пушим) в качестве удаленного кэша для наших слоев.
Если быть точнее:
- Мы начинаем с извлечения (pull) самого актуального образа (т. е. последнего) из удаленного реестра, который будет использоваться в качестве кэша для последующей docker команды сборки.
- Затем мы создаем образ, используя извлеченный образ в качестве кэша (аргумент —cache-from), если он доступен. Мы помечаем эту новую сборку в качестве последней и коммитим SHA.
- Наконец, мы помещаем оба образа с тегами в удаленный реестр, чтобы их также можно было использовать в качестве кэша для последующих сборок.
stages: - build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" DOCKER_HOST: "tcp://localhost:2375" services: - docker:stable-dind docker-build: image: docker:stable stage: build script: - docker pull my-registry/hello:latest || true - docker build --cache-from my-registry/hello:latest -t hello:latest . - docker tag hello:latest my-registry/hello:$ - docker tag hello:latest my-registry/hello:latest - docker push my-registry/hello:$ - docker push my-registry/hello:latest
Если вы запустите этот новый конвейер два раза, разница от использования кэша все равно будет неудовлетворительной.
Все слои из базового образа компоновщика пересобираются. Только первые 2 слоя (8 и 9) заключительного этапа используют кэш, но последующие слои перестраиваются.
Как мы видели ранее, при локальной отправке нашего образа слои базового образа компоновщика не помещаются в удаленный реестр и фактически теряются. Следовательно, когда мы извлекаем последние образы, их там нет, и их нужно строить заново.
Затем, когда наш финальный образ собран (шаги с 8 по 15), первые два слоя присутствуют в образе, который мы извлекли и использовали в качестве кэша. Но на шаге 10 мы получаем зависимости образа компоновщика, которые изменились, поэтому все последующие шаги также строятся заново.
Подводя итог, можно сказать, что использование кэша в значительной степени ограничено: только 2 шага из 15 используют кэш! Чтобы исправить это, нам нужно отправлять образ промежуточного компоновщика в удаленный реестр, чтобы сохранить его слои:
stages: - build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" DOCKER_HOST: "tcp://localhost:2375" services: - docker:stable-dind docker-build: image: docker:stable stage: build script: - docker pull my-registry/hello-builder:latest || true - docker pull my-registry/hello:latest || true - docker build --cache-from my-registry/hello-builder:latest --target builder -t hello-builder:latest . - docker build --cache-from my-registry/hello:latest --cache-from my-registry/hello-builder:latest -t hello:latest . - docker tag hello-builder:latest my-registry/hello-builder:latest - docker tag hello:latest my-registry/hello:$ - docker tag hello:latest my-registry/hello:latest - docker push my-registry/hello-builder:latest - docker push my-registry/hello:$ - docker push my-registry/hello:latest
Мы создаем промежуточный этап нашего сборщика в качестве образ докера, используя опцию target. После этого мы пушим его в удаленный реестр, в конечном итоге извлекая его в качестве кэша для создания нашего финального образа. При запуске конвейера наше время сократилось до 15 секунд!
Как видите, сборка постепенно усложняется. Если вы уже начинаете путаться, тогда представьте образ с 3 или 4 промежуточными стадиями! Но это метод работает. Другой недостаток заключается в том, что вам придется каждый раз загружать и выгружать все эти слои, что может быть довольно дорогостоящим с точки зрения затрат на хранение и передачу.
Второе решение: внешняя служба dind
Нам нужно запустить службу dind для выполнения нашей сборки docker. В нашем предыдущем решении dind был встроен в каждую задачу и разделял ее жизненный цикл, что делало невозможным создание надлежащего кэша.
Почему бы не сделать Dind гражданином первого класса, создав службу Dind в нашем кластере Kubernetes? Она будет работать с подключенным PersistentVolume для обработки кэшированных данных, и каждая задача может отправлять свои docker команды в эту общую службу.
Создать такую службу в Kubernetes достаточно просто:
apiVersion: v1 kind: PersistentVolumeClaim metadata: labels: app: docker-dind name: dind spec: accessModes: - ReadWriteOnce resources: requests: storage: 500Gi --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: docker-dind name: dind spec: replicas: 1 selector: matchLabels: app: docker-dind template: metadata: labels: app: docker-dind spec: containers: - image: docker:19.03-dind name: docker-dind env: - name: DOCKER_HOST value: tcp://0.0.0.0:2375 - name: DOCKER_TLS_CERTDIR value: "" volumeMounts: - name: dind-data mountPath: /var/lib/docker/ ports: - name: daemon-port containerPort: 2375 protocol: TCP securityContext: privileged: true # Требуется для работы dind контейнера. volumes: - name: dind-data persistentVolumeClaim: claimName: dind --- apiVersion: v1 kind: Service metadata: labels: app: docker-dind name: dind spec: ports: - port: 2375 protocol: TCP targetPort: 2375 selector: app: docker-dind
Затем мы немного изменим наш исходный GitLab конвейер, чтобы он указывал на эту новую внешнюю службу, и удалим встроенные dind службы:
stages: - build - test - deploy variables: # отключаем проверку Docker TLS DOCKER_TLS_CERTDIR: "" # здесь имя хоста dind разрешается как dind служба Kubernetes с помощью kube dns DOCKER_HOST: "tcp://dind:2375" docker-build: image: docker:stable stage: build script: - docker build -t hello . - docker tag hello:latest my-registry/hello: - docker push my-registry/hello:
Если вы запустите конвейер дважды, второй раз сборка должна занять 10 секунд, что даже лучше, чем в нашем предыдущем решении. Для «большого» образа, для построения которого требуется около 10 минут, эта стратегия также сокращает время построения до нескольких секунд, если слои не изменялись.
И последний вариант: использование Kaniko
Последним вариантом может быть использование Kaniko. С его помощью вы можете создавать образы Docker без использования демона Docker, делая все, что мы сейчас делали, без особых проблем.
Однако обратите внимание, что при этом вы не можете использовать расширенные BuildKit опции, такие как, например, внедрение секретов при создании образа. По этой причине я сконцентрировался на другом решении.
Заключение
Поскольку при разработке программного обеспечения контейнеры широко используются практически повсюду, их эффективное создание является ключевым моментом в конвейере релиза. Как мы видели, проблема может стать довольно сложной, и каждое решение имеет свои компромиссы. Предлагаемые здесь решения проиллюстрированы с использованием GitLab, но стоит отметить, что они будут работать и в других контейнерных CI средах.
А прямо сейчас приглашаем ва ознакомиться с программой супер-интенсива «CI/CD или Непрерывная поставка с Docker и Kubernetes», а таже записаться на день открытых дверей.
Как запустить докер внутри докера?
Функция запуска 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
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).
$ 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 внутри Docker – CloudSavvy IT
Запуск Docker внутри Docker позволяет создавать образы и запускать контейнеры в уже контейнерной среде. Есть два возможных подхода для достижения этого в зависимости от того, хотите ли вы запустить дочерние или родственные контейнеры.
Доступ к Docker изнутри контейнера Docker чаще всего желателен в контексте систем CI и CD. Обычно агенты, которые запускают ваш конвейер, размещаются внутри контейнера Docker. В конечном итоге вы будете использовать стратегию Docker-in-Docker, если на одном из этапов конвейера затем будет создан образ или он будет взаимодействовать с контейнерами.
Образ Docker-in-Docker
Программы для Windows, мобильные приложения, игры — ВСЁ БЕСПЛАТНО, в нашем закрытом телеграмм канале — Подписывайтесь:)
Docker предоставляется в виде автономного образа через докер: dind тег на Docker Hub. Запуск этого образа даст вам работающую установку демона Docker внутри вашего нового контейнера. Он будет работать независимо от демона вашего хоста, который запускает контейнер dind, поэтому docker ps внутри контейнера будет давать разные результаты для docker ps на вашем хосте.
docker run -d –privileged –name docker -e DOCKER_TLS_CERTDIR = / certs -v docker-certs-ca: / certs / ca -v docker-certs-client: / certs / client docker: dind
При таком использовании Docker-in-Docker есть одна большая оговорка: вам нужно использовать привилегированный режим. Это ограничение применяется даже если вы используете бескорневые контейнеры. Привилегированный режим активируется флагом –privileged в команде, показанной выше.
Использование привилегированного режима дает контейнеру полный доступ к вашей хост-системе. Это необходимо в сценарии Docker-in-Docker, чтобы ваш внутренний Docker мог создавать новые контейнеры. Однако в некоторых средах это может быть неприемлемым риском для безопасности.
Есть другие проблемы с dind тоже. В некоторых системах могут возникать конфликты с модулями безопасности Linux (LSM), такими как AppArmor и SELinux. Это происходит, когда внутренний Docker применяет политики LSM, которые внешний демон не может предвидеть.
Другая проблема касается файловых систем контейнеров. Внешний демон будет работать поверх обычной файловой системы вашего хоста, такой как ext4. Однако все его контейнеры, включая внутренний демон Docker, будут находиться в файловой системе копирования при записи (CoW). Это может создать несовместимость, если внутренний демон настроен на использование драйвера хранилища, который нельзя использовать поверх существующей файловой системы CoW.
Установка Docker Socket вашего хоста вместо
С проблемами, связанными с dind, лучше всего справляться, полностью избегая его использования. Во многих сценариях вы можете добиться желаемого эффекта, установив сокет Docker вашего хоста в обычный контейнер докеров:
docker run -d –name docker -v /var/run/docker.sock:/var/run/docker.sock docker: latest
Интерфейс командной строки Docker внутри образа докера взаимодействует с сокетом демона Docker, который он находит в /var/run/docker.sock. Подключение сокета вашего хоста к этому пути означает, что команды докеров, выполняемые внутри контейнера, будут выполняться против вашего существующего демона Docker.
Это означает, что контейнеры, созданные внутренним Docker, будут находиться в вашей хост-системе вместе с самим контейнером Docker. Все контейнеры будут существовать как братья и сестры, даже если будет казаться, что вложенный Docker является дочерним по отношению к родителю. Запуск docker ps даст одинаковые результаты, независимо от того, запущен ли он на хосте или внутри вашего контейнера.
Этот метод устраняет проблемы реализации dind. Это также устраняет необходимость использования привилегированного режима, хотя установка сокета Docker сама по себе является потенциальной проблемой безопасности. Все, что имеет доступ к сокету, может отправлять инструкции демону Docker, предоставляя возможность запускать контейнеры на вашем хосте, извлекать изображения или удалять данные.
Когда использовать каждый подход
Docker-in-Docker через dind исторически широко использовался в средах CI. Это означает, что «внутренние» контейнеры имеют уровень изоляции от хоста. Один контейнер CI runner поддерживает каждый конвейерный контейнер, не загрязняя демон Docker хоста.
Хотя это часто работает, это чревато побочными эффектами, а не предполагаемым вариантом использования dind. Он был добавлен для облегчения разработки самого Docker, а не для обеспечения поддержки конечным пользователям вложенных установок Docker.
В соответствии с Джером Петаццони, создатель реализации dind, использование подхода на основе сокетов должно быть вашим предпочтительным решением. Bind монтирование сокета демона вашего хоста безопаснее, гибче и так же полнофункционально, как запуск контейнера dind.
Если ваш вариант использования означает, что вам абсолютно необходим dind, есть более безопасный способ его развернуть. Современный Sysbox проект – это выделенная среда выполнения контейнера, которая может вкладывать другие среды выполнения без использования привилегированного режима. Контейнеры Sysbox становятся похожими на виртуальные машины, поэтому они могут поддерживать программное обеспечение, которое обычно запускается без операционной системы на физической или виртуальной машине. Сюда входят Docker и Kubernetes без какой-либо специальной настройки.
Заключение
Запуск Docker в Docker – довольно распространенное требование. Скорее всего, вы увидите это при настройке серверов CI, которым необходимо поддерживать сборку образов контейнера из созданных пользователем конвейеров.
Использование docker: dind дает вам независимый демон Docker, работающий внутри своего собственного контейнера. Он эффективно создает дочерние контейнеры, которые напрямую не видны с хоста. Хотя кажется, что он предлагает сильную изоляцию, на самом деле dind таит в себе множество краевых проблем и проблем безопасности. Это связано с взаимодействием с операционной системой Docker.
Установка сокета Docker вашего хоста в контейнер, который включает двоичный файл докера, является более простой и предсказуемой альтернативой. Это позволяет вложенному процессу Docker запускать контейнеры, которые становятся его братьями и сестрами. При использовании подхода на основе сокетов никаких дополнительных настроек не требуется.
Программы для Windows, мобильные приложения, игры — ВСЁ БЕСПЛАТНО, в нашем закрытом телеграмм канале — Подписывайтесь:)
Как и зачем запускать Docker внутри Docker

Мануал
Автор cryptoparty На чтение 5 мин Опубликовано 14.12.2021
Запуск Docker внутри Docker позволяет создавать образы и запускать контейнеры в уже контейнеризированной среде.
Существует два возможных подхода для достижения этой цели в зависимости от того, хотите ли вы запустить дочерние или родственные контейнеры.
Доступ к Docker изнутри контейнера Docker чаще всего желателен в контексте систем CI и CD.
Обычно агенты, запускающие ваш пайплайн, размещаются внутри контейнера Docker.
Вы можете использовать стратегию Docker-in-Docker, если один из этапов вашего пайплайна будет создавать образ или взаимодействовать с контейнерами.
Образ Docker-in-Docker
Docker предоставляется в виде самодостаточного образа через тег docker:dind на Docker Hub.
Запустив этот образ, вы получите функционирующую установку демона Docker внутри вашего нового контейнера.
Он будет работать независимо от демона вашего хоста, который запускает контейнер dind, поэтому docker ps внутри контейнера даст результаты, отличные от docker ps на вашем хосте.
docker run -d --privileged --name docker \ -e DOCKER_TLS_CERTDIR=/certs \ -v docker-certs-ca:/certs/ca \ -v docker-certs-client:/certs/client \ docker:dind
Использование Docker-in-Docker таким образом связано с одним большим предостережением: необходимо использовать привилегированный режим.
Это ограничение действует, даже если вы используете контейнеры без рутов.
Привилегированный режим активируется флагом –privileged в команде, показанной выше.
Использование привилегированного режима дает контейнеру полный доступ к вашей хост-системе.
Это необходимо в сценарии Docker-in-Docker, чтобы ваш внутренний Docker мог создавать новые контейнеры.
Однако в некоторых средах это может быть неприемлемым риском для безопасности.
Существуют и другие проблемы с dind.
В некоторых системах могут возникать конфликты с модулями безопасности Linux Security Modules (LSM), такими как AppArmor и SELinux.
Это происходит, когда внутренний Docker применяет политики LSM, которые внешний демон не может предугадать.
Еще одна проблема связана с файловыми системами контейнеров.
Внешний демон будет работать поверх обычной файловой системы вашего хоста, например ext4.
- Как проверить и восстановить файловую систему EXT4 на Linux
- ️ Что такое Ext2, Ext3 и Ext4 и как создавать и конвертировать файловые системы Linux
Однако все его контейнеры, включая внутренний демон Docker, будут располагаться на файловой системе с копированием при записи (CoW).
Это может привести к несовместимости, если внутренний демон настроен на использование драйвера хранения, который не может быть использован поверх существующей файловой системы CoW.
Вместо этого монтируйте сокет Docker на хосте
Проблемы, связанные с dind, лучше всего решать, избегая его использования вообще.
Во многих сценариях вы можете добиться желаемого эффекта, монтируя Docker-сокет хоста в обычный docker-контейнер:
docker run -d --name docker -v /var/run/docker.sock:/var/run/docker.sock \ docker:latest
Docker CLI внутри образа docker взаимодействует с сокетом демона Docker, который он находит по адресу /var/run/docker.sock.
Подключение сокета вашего хоста к этому пути означает, что команды docker, запущенные внутри контейнера, будут выполняться под управлением существующего демона Docker.
Это означает, что контейнеры, созданные внутренним Docker, будут находиться на вашей хост-системе вместе с самим контейнером Docker.
Все контейнеры будут существовать как родные братья и сестры, даже если кажется, что вложенный Docker является дочерним по отношению к родительскому.
Запуск docker ps даст одинаковые результаты, независимо от того, выполняется ли он на хосте или внутри вашего контейнера.
Эта техника митигирует проблемы реализации dind.
Она также устраняет необходимость использования привилегированного режима, хотя монтирование сокета Docker само по себе является потенциальной проблемой безопасности.
Все, что имеет доступ к сокету, может посылать инструкции демону Docker, обеспечивая возможность запуска контейнеров на хосте, извлечения образов или удаления данных.
Когда использовать каждый подход
Docker-in-Docker через dind исторически широко используется в средах CI.
Это означает, что “внутренние” контейнеры имеют уровень изоляции от хоста.
Один контейнер CI runner поддерживает все контейнеры пайплайна без загрязнения Docker-демона хоста.
Хотя это часто работает, это чревато побочными эффектами и не является целью использования dind.
Он был добавлен для облегчения разработки самого Docker, а не для обеспечения поддержки конечного пользователя для вложенных установок Docker.
По словам Жерома Петаццони, создателя реализации dind, предпочтительным решением должно быть использование подхода на основе сокетов.
Bind монтирование сокета демона вашего хоста является более безопасным, более гибким и столь же функциональным, как и запуск контейнера dind.
Если для вашего случая использования вам абсолютно необходим dind, есть более безопасный способ его развертывания.
Современный проект Sysbox – это специализированная контейнерная среда выполнения, которая может встраивать другие среды выполнения без использования привилегированного режима.
Контейнеры Sysbox становятся VM-подобными, поэтому они могут поддерживать программное обеспечение, которое обычно запускается “на пустом месте” на физической или виртуальной машине.
Включает Docker и Kubernetes без какой-либо специальной конфигурации.
Заключение
Запуск Docker внутри Docker – относительно распространенное требование.
Скорее всего, вы столкнетесь с ней при настройке CI-серверов, которым необходимо поддерживать сборку образов контейнеров из созданных пользователем конвейеров.
Использование docker:dind дает вам независимого демона Docker, работающего внутри собственного контейнера.
Он эффективно создает дочерние контейнеры, которые не видны непосредственно с хоста.
Хотя кажется, что dind обеспечивает сильную изоляцию, на самом деле он таит в себе множество проблем, связанных с крайними случаями и безопасностью.
Это связано с взаимодействием Docker с операционной системой.
Монтирование сокета Docker вашего хоста в контейнер, включающий бинарный файл docker, является более простой и предсказуемой альтернативой.
Это позволяет вложенному процессу Docker запускать контейнеры, которые становятся его родными братьями и сестрами.
При использовании подхода на основе сокета никаких дополнительных настроек не требуется.
Пожалуйста, не спамьте и никого не оскорбляйте. Это поле для комментариев, а не спамбокс. Рекламные ссылки не индексируются!
Добавить комментарий Отменить ответ

Поддержать нас
- Аудит ИБ (49)
- Вакансии (12)
- Закрытие уязвимостей (110)
- Книги (27)
- Мануал (2 385)
- Медиа (66)
- Мероприятия (39)
- Мошенники (23)
- Обзоры (835)
- Обход запретов (34)
- Опросы (3)
- Скрипты (122)
- Статьи (366)
- Философия (133)
- Юмор (19)
Наш Telegram

Социальные сети
Поделиться
Anything in here will be replaced on browsers that support the canvas element
- Как проверить IPv4-адреса в скрипте 25.12.2023
Проверка IP-адресов – распространенная задача в сетевом и системном администрировании. В этом уроке мы узнаем, как проверить IPv4-адреса с помощью скрипта оболочки. Это особенно полезно в ситуациях, когда нужно убедиться, что пользовательский ввод или данные из другого источника имеют правильный формат IPv4. IPv4 против IPv6: В чем разница между IPv4 и IPv6 Понимание формата адресов […]
Deep Packet Inspection (DPI) – это передовая техника сетевой фильтрации. Если традиционные методы мониторинга и фильтрации сети позволяют лишь поверхностно изучить заголовки пакетов, то DPI проникает глубже, тщательно анализируя фактическое содержание данных в пакетах. Такая детальная проверка позволяет получить полное представление о потоке данных, что дает возможность определить не только тип или категорию данных, но […]
Обратный инжиниринг, термин, часто ассоциируемый с технологическими инновациями и решением проблем, включает в себя сложный процесс раскрытия дизайна, структуры или функциональности продукта, системы или части технологии, чтобы понять их внутреннюю работу. Эта многогранная дисциплина играет ключевую роль в различных отраслях промышленности, способствуя инновациям, обеспечивая совместимость и способствуя продвижению вперед. Сегодня обратный инжиниринг услуги выполняют одни из лучших […]
Компания “Автозайм”: надежное залоговое кредитование в СПб и по всей России “Автозайм” представляет собой современный автоломбард, который оперирует в различных городах России, включая Санкт-Петербург – https://spb.carzaem.ru/autolombard. Компания специализируется на предоставлении кредитов под залог автомобилей, предлагая клиентам удобные и прозрачные условия. Основные преимущества Быстрый и простой процесс. Процедура получения займа в “Автозайм” максимально упрощена. Клиенты могут подать […]
Мы рассмотрим подписание коммитов и тегов ключом GPG, а также отправку и получение открытых ключей GPG на сервер ключей для проверки. Шпаргалка Неподписанный коммит: Подписанный коммит: Если ваши адреса электронной почты git и gpg-ключа отличаются, это приведет к неудаче, пока вы не настроите свой git signingkey Неподписанный тег: Подписанный тег: Переопределение параметров конфигурации автоподписания: Импорт […]