BuildKit: современная сборка
Старый сборщик Docker выполнял Dockerfile строго сверху вниз, по одной инструкции за раз, и не умел ни прятать секреты, ни переиспользовать кэш пакетного менеджера. BuildKit меняет правила: он строит граф зависимостей, гонит независимые стадии параллельно и даёт безопасные mount-механизмы для секретов и кэша. В свежих версиях Docker он включён по умолчанию.
BuildKit — это современный бэкенд сборки образов Docker, который выполняет инструкции Dockerfile как граф зависимостей, параллелит независимые шаги и предоставляет mount-механизмы для секретов и кэша.
Зачем это нужно на практике? Классический сборщик линеен: он идёт по Dockerfile сверху вниз и не понимает, что две независимые стадии можно собрать одновременно. У него нет штатного способа передать токен приватного репозитория так, чтобы тот не остался в истории образа, — поэтому люди прокидывали секреты через ARG или COPY, и ключи утекали в слои. Нет у него и кэша пакетного менеджера: каждая сборка заново качала зависимости. BuildKit решает обе проблемы. Он быстрее за счёт параллелизма и умного кэширования по контенту и безопаснее за счёт того, что секреты и кэш существуют только во время выполнения команды и не превращаются в слои образа.
Как включить
В новых версиях Docker (Docker Desktop и свежий Docker Engine) BuildKit уже работает по умолчанию, и делать ничего не нужно. На более старых установках его включают переменной окружения DOCKER_BUILDKIT=1 перед командой сборки. Отдельный интерфейс docker buildx — это расширенный клиент поверх BuildKit; именно он нужен для мультиплатформенных сборок и отдельных билдеров.
# Явно включить BuildKit для одной сборки (на старых версиях)
DOCKER_BUILDKIT=1 docker build -t myapp .
# Проверить, что buildx доступен
docker buildx version
Чтобы заработали mount-фичи (секреты и кэш), Dockerfile должен начинаться со строки активации синтаксиса. Это самая первая строка файла, до любого FROM:
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
Без этой строки парсер использует старый набор инструкций, и любой --mount будет восприниматься как ошибка синтаксиса.
Build args
ARG объявляет переменную времени сборки. Её значение можно подменить флагом --build-arg, но в самом образе после сборки её уже нет. ENV, наоборот, задаёт переменную окружения, которая остаётся в финальном образе и доступна процессу в работающем контейнере. У ARG есть область видимости: переменная, объявленная до первого FROM, видна только в строках до FROM (например, чтобы подставить тег базового образа), а чтобы использовать её внутри стадии, ARG нужно объявить заново уже после FROM.
# syntax=docker/dockerfile:1
ARG NODE_VERSION=20
FROM node:${NODE_VERSION}-alpine
# Чтобы переменная была видна внутри стадии, объявляем её снова
ARG APP_ENV=production
ENV APP_ENV=${APP_ENV}
docker build --build-arg NODE_VERSION=18 --build-arg APP_ENV=staging -t myapp .
Важное предупреждение: --build-arg не предназначен для секретов. Значения build-аргументов сохраняются в метаданных образа и видны через docker history. Передавать так пароль или токен — значит опубликовать его вместе с образом.
Secret mounts
Для секретов BuildKit даёт отдельный механизм: --mount=type=secret. Секрет подключается к команде RUN как файл на время её выполнения и исчезает сразу после. Он не попадает ни в один слой образа — в отличие от COPY, который физически копирует файл в слой, и от ARG, который оседает в истории.
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
# Файл .npmrc с токеном доступен только во время этого RUN
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc \
npm ci
docker build --secret id=npmrc,src=.npmrc -t myapp .
Внутри сборки секрет лежит по указанному пути (target), npm читает токен, ставит зависимости — и после завершения RUN файла больше нет. В готовом образе токена не найти даже через docker history.
Cache mounts
--mount=type=cache подключает к команде RUN постоянный кэш-каталог, который переживает сборки, но не становится слоем образа. Это идеально для каталогов пакетных менеджеров: повторная сборка не качает зависимости заново, а образ при этом не раздувается, потому что кэш живёт отдельно от слоёв.
# syntax=docker/dockerfile:1
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
Тот же приём для других экосистем — меняется только целевой каталог:
# Python / pip
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
# Go — кэш загруженных модулей
RUN --mount=type=cache,target=/go/pkg/mod \
go build ./...
Кэш-маунт не попадает в финальный образ: он хранится в билдере и переиспользуется между запусками сборки, поэтому размер образа от него не растёт.
Параллельные стадии и buildx
В multi-stage Dockerfile стадии, не зависящие друг от друга, BuildKit собирает одновременно. Если у вас есть стадия сборки фронтенда и стадия сборки бэкенда, и финальный образ копирует артефакты из обеих, эти две стадии пойдут параллельно — без всякой ручной настройки.
Для сборки под несколько архитектур используется docker buildx с флагом --platform. Одна команда соберёт образ сразу под amd64 и arm64:
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest --push .
Флаг --push здесь нужен потому, что мультиплатформенный результат — это манифест из нескольких образов, который удобнее сразу отправить в реестр.
Как это работает под капотом
BuildKit не выполняет Dockerfile построчно. Он сначала разбирает его в DAG — направленный ациклический граф инструкций, где рёбра — это зависимости (например, RUN зависит от предшествующего COPY). Узлы, между которыми нет связей, выполняются параллельно. Кэш считается по контенту: если входные данные шага не изменились, BuildKit берёт готовый результат вместо повторного выполнения. А mount-механизмы (secret и cache) существуют исключительно в момент выполнения конкретного RUN и принципиально не становятся слоями — поэтому ни секрет, ни кэш не оказываются в итоговом образе.
Частые ошибки
- Забыли строку
# syntax=docker/dockerfile:1в начале файла — и любой--mountпадает с ошибкой синтаксиса. - Кладут секрет через
ARGилиENVвместо--mount=type=secret— токен утекает вdocker historyи остаётся в образе. - Ждут параллелизма от стадий, которые на самом деле зависят одна от другой по цепочке — граф их выстроит последовательно, ускорения не будет.
- Путают build-time секрет (нужен только во время сборки, через
--secret) с runtime-секретом (нужен работающему контейнеру, передаётся через переменные окружения или docker secret уже при запуске).
Итоги
- BuildKit — современный движок сборки Docker; в свежих версиях включён по умолчанию, на старых — через
DOCKER_BUILDKIT=1. - Mount-фичи требуют строки
# syntax=docker/dockerfile:1в самом начале Dockerfile. ARGи--build-arg— для значений времени сборки, но не для секретов: они видны вdocker history.--mount=type=secretпередаёт токены и ключи безопасно — секрет живёт только во времяRUNи не попадает в слои.--mount=type=cacheускоряет повторные сборки, кэшируя каталоги npm, pip и go, и при этом не раздувает образ.- Независимые стадии multi-stage идут параллельно, а
docker buildx --platformсобирает образ сразу под несколько архитектур.