Москва: проверенные б/у ноутбуки с гарантией и проверкой перед оплатой
+7 906 722-94-45

Главная → Статьи → BuildKit и Kaniko: сборка образов в CI без демона

Сборка образов в CI: BuildKit, Kaniko и кэш слоёв

Как собирать Docker-образы в пайплайнах: BuildKit и cache mounts, Kaniko без привилегий, кэширование слоёв в реестре и ускорение сборок в разы.

Прямой ответ

Сборка образа в CI упирается в три вопроса: где собирать (демон/привилегии), что кэшировать (слои/зависимости) и как не ждать 15 минут; BuildKit — движок сборки по умолчанию в современном Docker (параллельные стадии, пропуск неиспользуемых стадий, секреты без попадания в слои, cache mounts) — включается DOCKER_BUILDKIT=1 (или docker buildx — multistage-контекст — стадии параллелятся — сборка «frontend+backend+миграции» — три ветки сразу); ключевые фичи для CI: cache mounts (RUN --mount=type=cache,target=/root/.cache/pip pip install ... — кэш pip/apt живёт между сборками, не в слоях образа — «зависимости ставятся 4 минуты каждый build» — 10 секунд с кэшем — CI-кэш; кэш монтируется, не копируется в образ — слои чистые); секреты (RUN --mount=type=secret,id=npm_token npm install — токен не печётся в слой — «приватный индекс pip с токеном в ARG» — токен в истории образа — supply chain-дыра — секрет-маунты закрывают); регистровый кэш слоёв (--cache-from type=registry,ref=registry/app:buildcache --cache-to type=registry,mode=max — кэш сборки живёт в реестре — любой раннер/ветка переиспользует — реестр — CI-раннеры эфемерны — локальный кэш бесполезен — реестр — общее хранилище — MinIO-S3-бэкенд тоже работает); Kaniko — альтернатива для сред без docker-демона/привилегий (собирает образ внутри контейнера без Docker-in-Docker — не нужен privileged-режим — rootless-контекст — подман-альтернатива; k8s-джобы/GitLab-раннеры в контейнерах — Kaniko-контейнер собирает и пушит — «docker.sock в CI» — привилегированная дыра — Kaniko её убирает); выбор движка: GitHub Actions + buildx-action — BuildKit (дефолт-путь — кэш ghactions/registry), k8s-джобы без привилегий — Kaniko (или buildah/podman rootless), локальная разработка — BuildKit нативен; порядок слоёв (база, без которой кэш не работает): изменяющееся — вниз (COPY требований → install → COPY кода — код меняется, зависимости реже — multistage-дисциплина — COPY . . до pip install — кэш слоёв мёртв всегда — ошибка №1); .dockerignore обязателен (контекст сборки — node_modules/.git в контекст — минуты на отправку + инвалидация кэша — контекст-гигиена).

Практика: пайплайн сборки за 90 секунд

GitHub Actions + buildx (каркас): setup-buildx-action → login (реестр — секреты) → build-push-action с cache-from: type=gha + cache-to: type=gha,mode=max (GHA-кэш — 10 ГБ на репо — слои/маунты — CI-кэш; для k8s-раннеров — registry-кэш в свой реестр); цифры трансформации (реальный профиль Python-сервиса): было — 9 минут (pull базы 40 с, pip install 4 мин — без кэша, push 2 мин, контекст 900 МБ с .git); стало — 85 секунд: --mount=type=cache на pip (зависимости — 25 с), .dockerignore (контекст 900→40 МБ — 5 с), registry-кэш слоёв (базовые слои не пересобираются — hit rate 90% на PR-сборках), multistage (builder-стадия с компиляцией — в итог не попадает — итог 120 МБ против 900); Kaniko-вариант (k8s-джоба): pod с kaniko-executor, Dockerfile+контекст — из git-репо (--context=git://...) или PVC, аргументы --cache=true --cache-repo=registry/app/cache --destination=registry/app:$SHA — push без демона; подводные камни Kaniko: файловая система раннера должна позволять (overlayfs-нюансы — volume для /kaniko/.docker — docker-тома — config.json с креденшелами — секретом), кэш репо чистить (растёт бесконечно — registry-гигиена — TTL-политика); аттестация сборки: после build — подпись+SBOM (cosign-практика — «собрали — сразу подписали» — один пайплайн — supply chain); матричные сборки (multi-arch amd64/arm64 — buildx --platform — QEMU-эмуляция arm медленная — native-раннеры при возможности — матрица-GHA — параллельно по платформам).

Практический сценарий: CI-сборка с 11 минут до 2 без изменения Dockerfile'а по существу

Контекст: монорепо, 6 сервисов, CI — GitHub Actions, сборка образа сервиса — 11 минут (девопсы «оптимизировали» год назад — «норма»); диагностика недели: (1) хронометраж джобы (CI-логика — шаги): checkout 20 с, buildx setup 10 с, docker build 8:40, push 1:30; внутри build (BuildKit-лог --progress=plain): pull python:3.11-fat 50 с, pip install 3:40 (400 пакетов — каждый раз), npm ci во frontend-стадии 2:10, COPY кода + пересборка всего после — 2:00; (2) находки: кэша нет вовсе (ни gha, ни registry — каждый build с нуля), Dockerfile — COPY . . перед pip install (любое изменение кода — полная переустановка зависимостей — порядок слоёв), контекст — 1.2 ГБ (.git 800 МБ — .dockerignore отсутствует), база — python:3.11 (fat — 900 МБ); (3) фиксы (Dockerfile — минимальные правки): reorder (COPY pyproject/uv.lock → install → COPY кода), --mount=type=cache на pip/npm, FROM python:3.11-slim (trivy-бонус — минус 200 CVE), .dockerignore (.git/node_modules/тесты/доки); (4) CI: buildx + cache-from/to type=gha,mode=max + реестр-кэш для k8s-джоб; (5) результат: холодная сборка (первая/после изменения зависимостей) — 4:20, тёплая (типичный PR — только код) — 1:50 (зависимости из кэша, слои кода — 40 МБ), push итогового образа 120 МБ — 25 с (реестр); (6) попутно: Kaniko-ветка для k8s-джоб миграционных (alembic-образы — тот же Dockerfile — без docker.sock — привилегии убраны — харднинг-CI); (7) закрепление: шаблон Dockerfile в документации (reorder+cache mounts — «новый сервис стартует быстрым»), CI-метрика длительности сборки в Grafana (регрессия >3 мин — разбор — «сборка снова распухла» — видно — SLO-культура для DX); вывод: 80% выигрыша — кэш зависимостей + порядок слоёв + .dockerignore — три вещи, которые «все знают», но в легаси-репо их нет — техдолг — аудит CI раз в квартал окупается часами команды ежедневно.

FAQ

Docker-in-Docker против Kaniko/buildx — почему «docker.sock в CI» — плохо? Монтирование docker.sock хоста в CI-контейнер = root на хосте раннера (сокет — полный контроль демона — любой код джобы (включая зависимости PR из форка!) — эскалация — OWASP-CI — «сборка образов» как вектор компрометации раннера); DinD (privileged-контейнер с демоном внутри) — безопаснее для хоста, но privileged-режим сам по себе — риск + медленнее (вложенный демон); buildx (BuildKit rootless-режим) и Kaniko — сборка без демона и привилегий (пользовательские namespace'ы — cgroups/ns-механика) — современный дефолт; исключения: нужды legacy-инструментов, требующих сокет (тесты с testcontainers в той же джобе — отдельный сервис-контейнер — testcontainers-контекст — не смешивать со сборкой); правило 2026: docker.sock в CI-джобе — red flag на ревью (чеклист).

Как работает кэш слоёв и почему он «не срабатывает»? Механика: каждый слой — хэш (инструкция + контекст входа) — совпал хэш — слой взят из кэша (секунды); инвалидация — цепочкой (изменился слой N — слои N+1.. пересобираются всегда — «поменяли COPY кода — pip install выше не затронут, но всё ниже — да» — multistage); типичные «кэш не работает»: COPY . . рано (любое изменение кода — переустановка зависимостей — reorder — зависимые файлы (pyproject/lock) — отдельным слоем выше), изменчивые инструкции вверху (ARG BUILD_DATE/apt-get update без фикса версий — детерминизм), отсутствие кэш-бэкенда в CI (эфемерные раннеры — локальный кэш не живёт — cache-from/to gha/registry — обязательны — CI-кэш), кэш-репо разросся/протух (registry-кэш без GC — pull кэша дольше сборки — registry-гигиена); диагностика: --progress=plain — CACHED-метки по стадиям — видно, где обрыв (профилирование-культура — «не угадывай, посмотри»).

cache mounts против обычных слоёв — в чём разница и когда что? Слой-кэш: результат инструкции (установленные пакеты) — часть образа/кэша слоёв — инвалидируется изменением входа (requirements изменился — весь install-слой заново — даже если 399 пакетов те же); cache mount: каталог кэша тулзы (~/.cache/pip) монтируется в RUN, не попадая в образ — переживает изменение требований (399 пакетов из кэша pip, 1 новый — download — секунды против минут — зависимости); комбинация — норма: слой-кэш (install-слой целиком при неизменных lock-файлах) + cache mount (частичное изменение — добор из кэша); нюансы: кэш-маунты BuildKit-специфичны (Kaniko поддерживает частично — --cache-copy-layers/registry-кэш — Kaniko-маунты — проверять версию — инструмент-выбор по контексту), кэш мусорится (TTL/--cache-ttl в CI — кэш-гигиена), кэш не для секретов (монтируемое переиспользуется между сборками — токен в кэше — утечка между джобами — секреты — только type=secret).

Нужен ли отдельный «CI-образ» для сборки или собирать из Dockerfile приложения? Собирать — Dockerfile приложения (один артефакт — прод и CI идентичны — «в CI собиралось иначе» — класс расхождений — 12-factor — билд-репро — supply chain — аттестация сборки описывает один путь); CI-специфика — вокруг, не внутри (кэш-бэкенды/секреты/подписи — аргументы пайплайна — Dockerfile чист — конфигурация); targets для разных артефактов (buildx --target builder — CI-стадия тестов использует builder-стадию с dev-зависимостями, прод-артефакт — runtime-стадия — один Dockerfile, два потребителя — multistage-структура); «тяжёлый CI-образ с тулзами» — отдельная история (раннеры/тулчейн — не про образ приложения — GHA-матрица); проверка идентичности: digest собранного в CI = digest задеплоенного (cosign-верификация — «деплоим то, что собрали и подписали» — политика кластера — Kyverno).

Источники: документация Docker BuildKit/cache backends, Kaniko — ссылки в шапке.