10. Docker в CI/CD
Цель темы: эффективно использовать Docker в пайплайнах: понимать разницу между Docker-in-Docker (DinD) и Docker-outside-of-Docker (DOoD), включать BuildKit и кэш сборки в CI, при необходимости собирать multi-arch образы и сканировать образы в пайплайне; разбирать типовые кейсы — почему сборка в CI медленная и почему build падает только в CI.
Определения терминов
Docker-in-Docker (DinD)
DinD — запуск демона Docker внутри контейнера. Образ docker:dind; демон работает в том же контейнере или в отдельном сервисе, а job-контейнер подключается к нему по TCP или через socket. Сборка образов выполняется «внутри» изолированного окружения; кэш слоёв живёт внутри этого контейнера и по умолчанию не сохраняется между запусками job (если не монтировать volume для данных демона). Подходит для изолированных сборок без доступа к демону хоста.
Docker-outside-of-Docker (DOoD)
DOoD — клиент Docker (CLI) в контейнере или на агенте подключается к демону на хосте (монтирование /var/run/docker.sock). Сборка выполняется на хосте; кэш слоёв сохраняется между job'ами на одном и том же агенте. Риск: job получает полный доступ к демону и может запускать любые контейнеры. Часто используется на выделенных build-агентах с доверенными пайплайнами.
BuildKit
BuildKit — движок сборки образов (по умолчанию в современных Docker). Параллелизует этапы, кэширует слои эффективнее, поддерживает расширенные возможности (секреты в сборке, SSH forward, cache mount). В CI включают переменной DOCKER_BUILDKIT=1; для кэша между job'ами используют --cache-from и --cache-to (registry или локальный volume).
Кэш в CI
Кэш сборки — повторное использование слоёв от предыдущих сборок, чтобы не перекачивать базовые образы и не перевыполнять неизменённые шаги (RUN, COPY). В CI каждый job часто запускается «с нуля»: нет локального кэша. Варианты: передавать кэш через registry (--cache-from, --cache-to), использовать общий volume с данными BuildKit на агенте (DOoD), или принимать холодную сборку и оптимизировать Dockerfile (редко меняющиеся слои выше).
Multi-arch в CI
Multi-arch — сборка одного тега образа для нескольких платформ (например, linux/amd64 и linux/arm64). В CI используют docker buildx build --platform linux/amd64,linux/arm64 с кэшем и push в registry. Требует BuildKit/buildx; на одном хосте возможна эмуляция (QEMU) для неподдерживаемой архитектуры (медленнее).
Сканирование в пайплайне
Security scanning — шаг после сборки образа: проверка на известные CVE (Trivy, Docker Scout, встроенное в registry). При критических уязвимостях пайплайн может падать или блокировать push в production. Подробнее в разделе «Безопасность Docker».
Docker-in-Docker vs Docker-outside-of-Docker
| Критерий | DinD | DOoD (socket) |
|---|---|---|
| Где выполняется сборка | В контейнере (демон в контейнере) | На хосте (демон хоста) |
| Кэш между job'ами | По умолчанию нет (контейнер уничтожается) | Да, если один и тот же агент |
| Изоляция | Высокая (отдельный демон) | Низкая (доступ к демону хоста) |
| Безопасность | Нет доступа к хосту по Docker | Полный доступ к демону = риск для хоста |
| Настройка | Запуск dind, подключение по TCP или socket | Монтирование docker.sock |
Когда DinD: изолированные окружения (например, GitLab CI с сервисом docker:dind), когда нельзя давать доступ к демону хоста. Кэш в DinD сохраняют через volume для /var/lib/docker или через registry cache (--cache-to type=registry).
Когда DOoD: выделенные build-агенты, где скорость и кэш важнее; агент доверенный, пайплайн не выполняет произвольный код из MR.
BuildKit
Включение и использование в CI:
export DOCKER_BUILDKIT=1
docker build -t myimage:tag .
Или в Dockerfile для cache mount (кэш пакетов между сборками):
# syntax=docker/dockerfile:1
RUN --mount=type=cache,target=/var/cache/apt \
apt-get update && apt-get install -y ...
RUN --mount=type=cache,target=/root/.npm \
npm ci
В CI кэш mount эффективен, если один и тот же runner/volume переиспользуется (иначе кэш пустой каждый раз). Чаще в CI используют registry cache: сохранять кэш в registry и подтягивать в следующей сборке.
Кэш в CI: registry cache
Сборка с записью кэша в registry и подтягиванием при следующем запуске:
docker buildx build \
--cache-from type=registry,ref=myregistry.com/myapp:buildcache \
--cache-to type=registry,ref=myregistry.com/myapp:buildcache,mode=max \
-t myregistry.com/myapp:tag \
--push .
mode=max — сохранять все промежуточные слои (не только финальный образ). Первая сборка заполняет кэш; последующие подхватывают его через --cache-from. Требуется BuildKit и buildx.
В GitLab CI, GitHub Actions и др. часто используют встроенную переменную для образа (например, образ по ветке или по коммиту) как ref для кэша, чтобы разные ветки не затирали кэш друг друга или использовали общий тег buildcache.
Практика
Если пайплайн каждый раз тянет большой базовый образ и гоняет одни и те же RUN — включите BuildKit и registry cache (или общий volume на агенте при DOoD). Плюс оптимизируйте порядок инструкций в Dockerfile.
Multi-arch сборка в CI
docker buildx create --use
docker buildx build \
--platform linux/amd64,linux/arm64 \
--cache-from type=registry,ref=myregistry.com/myapp:buildcache \
--cache-to type=registry,ref=myregistry.com/myapp:buildcache,mode=max \
-t myregistry.com/myapp:v1.0 \
--push .
На хосте с одной архитектурой для другой платформы BuildKit использует QEMU (если установлен); сборка для arm64 на amd64 будет медленнее. В некоторых CI предусмотрены native arm-раннеры.
Сканирование в пайплайне
После сборки образа добавить шаг:
docker build -t myapp:$TAG .
trivy image --exit-code 1 --severity HIGH,CRITICAL myapp:$TAG
# или
docker scout cves myapp:$TAG
При обнаружении уязвимостей заданной тяжести пайплайн завершается с ошибкой. Секреты из образа тоже можно проверять (trivy, другие инструменты).
Типовые кейсы
Почему CI-билд медленный?
Возможные причины:
- Нет кэша — каждый job с чистого агента; все слои собираются заново, базовый образ качается каждый раз. Решение: registry cache или общий кэш на агенте (DOoD), оптимизация Dockerfile (редко меняющиеся шаги выше).
- Большой контекст —
docker buildотправляет весь контекст (дерево файлов) демону; без.dockerignoreтуда попадают node_modules, .git, артефакты. Решение: актуальный.dockerignore. - Тяжёлые RUN — установка пакетов без кэша (apt, npm) каждый раз заново. Решение: кэш в registry или cache mount; ставить зависимости до копирования кода.
- Медленная сеть — pull базового образа и push результата. Решение: registry ближе к агентам, образы с минимальной базой (alpine и т.д.).
- Один агент перегружен — несколько job'ов собирают образы на одной машине. Решение: больше агентов или очередь.
Почему Docker build падает только в CI?
Возможные причины:
- Другая архитектура — сборка на локальной машине (например, arm) успешна, в CI (amd64) — падает из-за бинарников или платформо-зависимых шагов. Решение: явно указывать платформу в CI или собирать через buildx для нужной платформы.
- Нет памяти/диска — в CI лимиты по памяти и месту на диске; сборка или загрузка слоёв падает с OOM или «no space left». Решение: увеличить ресурсы раннера, уменьшить параллелизм сборки, очищать старые образы.
- Сетевые ограничения — в CI может быть блокировка части URL (apt, npm, pip). Решение: зеркала, прокси, проверка доступности из пайплайна.
- Права и пути — скрипты в контексте без прав на выполнение или пути с пробелами/кириллицей ведут себя по-разному. Решение: проверять права (chmod в Dockerfile при необходимости), избегать «сложных» путей.
- Кэш даёт другой результат — локально закэшированный слой скрывает ошибку; в CI кэш пустой и слой пересчитывается, ошибка проявляется. Решение: исправить ошибку в Dockerfile или в скриптах; при отладке временно отключить кэш (
--no-cache) в CI и локально.
Production
В пайплайне зафиксировать DOCKER_BUILDKIT=1, использовать один и тот же ref для registry cache (например, по имени образа и ветки). Добавить шаг сканирования после сборки. При жалобах на медленный билд — замерить время по шагам (pull, build, push) и расширить кэш или оптимизировать Dockerfile.
Паттерны использования
| Паттерн | Описание |
|---|---|
| BuildKit + registry cache | Включить BuildKit и сохранять/подтягивать кэш через registry для ускорения. |
| .dockerignore | Уменьшать контекст сборки; не тянуть в демон лишние файлы. |
| Оптимизация порядка в Dockerfile | Редко меняющиеся слои выше; кэш дольше живёт. |
| Сканирование после сборки | Блокировать или помечать образы с критическими CVE. |
| Один образ — один тег в registry | Тегировать по коммиту или версии; не полагаться на локальный кэш для «последнего» образа. |
Антипаттерны
| Антипаттерн | Почему плохо | Что делать |
|---|---|---|
| Игнорировать кэш в CI | Каждая сборка с нуля — долго и нагрузка на сеть. | Включить registry cache или общий кэш на агенте. |
| Монтировать docker.sock в недоверенный job | Доступ к демону хоста из пайплайна. | Использовать DinD или выделенных агентов с контролем пайплайнов. |
| Сборка без BuildKit | Медленнее и меньше возможностей по кэшу. | В CI всегда DOCKER_BUILDKIT=1. |
| Тяжёлый контекст без .dockerignore | Долгая отправка контекста и риск лишних слоёв. | Ведение актуального .dockerignore. |
Примеры из production
GitLab CI: DinD и registry cache
Сервис docker:dind; переменные DOCKER_TLS_CERTDIR, DOCKER_HOST. В job задают DOCKER_BUILDKIT=1 и передают --cache-from/--cache-to на registry проекта. Кэш переживает завершение job'а.
GitHub Actions: DOoD и кэш
Часто используют хостовой Docker (runner уже с установленным Docker). Кэш слоёв сохраняется между job'ами одного workflow за счёт actions/cache (кэш каталога BuildKit или сохранение/восстановление образа cache). Либо кэш в registry: push образа с тегом cache и pull в следующем job.
Падение только в CI из-за архитектуры
Локально разработчик на M1 (arm64); в CI — amd64. В Dockerfile копировали бинарник, собранный под arm. В CI копирование проходит, но при запуске контейнера бинарник не подходит под платформу. Решение: в CI собирать бинарник внутри Docker (multi-stage) под целевой платформой или собирать образ только в CI для нужной платформы.