Если вы используете Docker в CI/CD и видите, как каждый новый билд на свежем раннере скачивает все зависимости и пересобирает мир, значит пора включать BuildKit и вынести кэш в реестр контейнеров. Такой подход называется registry-backed cache: BuildKit умеет экспортировать и импортировать кэш в OCI-совместимый реестр, и благодаря этому кэш становится общим для всех раннеров, веток, даже для локальной разработки (если нужна).
Зачем именно registry-backed cache
BuildKit поддерживает несколько бэкендов кэша: локальный диск (type=local), встроенный в образ (type=inline), кэш поставщика CI (например, type=gha), и реестр контейнеров (type=registry). В корпоративном и командном сценарии чаще всего выигрывает именно реестр:
- Кэш доступен всем раннерам без привязки к их диску и времени жизни VM.
- Кэш может переживать пересоздание инфраструктуры CI.
- Кэшом могут пользоваться разные пайплайны и репозитории (если нужно и если есть доступ к реестру).
- Не раздуваете итоговые образы, как при
type=inline, и не зависите от конкретного провайдера CI.
Для площадок с «холодными» ephemeral-раннерами это зачастую самый быстрый и предсказуемый способ резко сократить время сборки.
Как это работает
BuildKit при экспорте кэша в реестр публикует специальные артефакты (OCI-объекты), содержащие метаданные о шагах сборки и ссылки на уже существующие слои. При последующих билдах BuildKit импортирует эти метаданные и быстро находит совпадающие шаги, не пересобирая их. Это даёт «CACHED» для подавляющей части слоёв, если Dockerfile составлен корректно.
Важно: registry-cache хранит метаданные сборки и использует уже имеющиеся слои. Если ваш образ или его base-образы приватные, доступ к реестру и артефактам кэша должен быть настроен соответствующим образом.

Быстрый старт: CLI
Ниже — минимальный пример, как собрать и сразу выгрузить кэш в реестр. Предполагаем, что Buildx уже включен и вы залогинились в свой реестр.
# 1) Включаем BuildKit и plain-лог
export DOCKER_BUILDKIT=1
export BUILDKIT_PROGRESS=plain
# 2) Создаём builder c драйвером docker-container
docker buildx create --use --name ci-builder
# 3) Логин в реестр (замените на ваш реестр и пользователя)
docker login REGISTRY_EXAMPLE
# 4) Сборка с импортом/экспортом кэша в один и тот же ref
docker buildx build --cache-from type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache --cache-to type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache,mode=max -t REGISTRY_EXAMPLE/ORG/APP:commit-SHA --push .
Первый прогон будет «холодным». Начиная со второго, при неизменном Dockerfile структура слоёв переиспользуется, а скорость сборки заметно растёт.
GitHub Actions: готовый workflow
В CI особенно удобно использовать action для Buildx. Пример для многоплатформенной сборки с кэшем в реестре:
name: docker-build
on:
push:
branches: [ main ]
pull_request:
env:
REGISTRY: REGISTRY_EXAMPLE
IMAGE_NAME: ORG/APP
CACHE_REF_MAIN: REGISTRY_EXAMPLE/ORG/APP:buildcache-main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push with registry cache
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: |
type=registry,ref=${{ env.CACHE_REF_MAIN }}
cache-to: |
type=registry,ref=${{ env.CACHE_REF_MAIN }},mode=max
provenance: false
sbom: false
Пояснения:
cache-toиcache-fromуказывают один и тот жеref, чтобы на каждом прогоне кэш актуализировался.mode=maxрасширяет объём экспортируемого кэша (глубокие графы), что помогает последующим билдам.- Отключение
provenanceиsbomускоряет сборку, если эти артефакты не нужны в CI.
Лучшие практики Dockerfile под BuildKit
1) Максимум детерминизма
Чем более детерминированны слои, тем стабильнее попадание в кэш. Не используйте легко меняющиеся значения в аргументах и LABEL на ранних слоях (например, текущую дату). Перенесите такие метаданные ближе к концу.
2) Правильный порядок COPY
Классическая оптимизация для Node.js/Go/PHP/Composer и т.д. — сначала копировать lock-файлы и манифесты зависимостей, устанавливать их, и лишь затем копировать остальное. Например, для Node.js:
# syntax=docker/dockerfile:1.7
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci
FROM node:20-alpine AS build
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
FROM node:20-alpine AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
CMD ["node","dist/index.js"]
При неизменных package.json/package-lock.json шаг npm ci будет стабильно кешироваться.
3) Кэшируемые монтирования
BuildKit умеет давать временные кэши для инструментов сборки и пакетных менеджеров. Это не тот же кэш, что registry-backed, но вместе они дают максимальный эффект. См. также подробный разбор кэшируемых монтирований BuildKit.
# apt
RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt/lists apt-get update && apt-get install -y --no-install-recommends build-essential && rm -rf /var/lib/apt/lists/*
# Go
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build go build -v ./...
# Composer
RUN --mount=type=cache,target=/composer/cache composer install --no-dev --prefer-dist --no-interaction
Такие кэши живут только в рамках конкретного билдера/хоста, но сильно ускоряют «горячие» шаги и уменьшают трафик.
4) Секреты — через --mount=type=secret
Чтобы не «прожечь» секреты в слоях и кэше, используйте секреты как монтирования:
# Dockerfile
RUN --mount=type=secret,id=npm_token bash -lc 'echo "//registry.npmjs.org/:_authToken=$(cat /run/secrets/npm_token)" >> ~/.npmrc && npm ci'
А в CI передавайте секрет параметром action:
with:
secrets: |
npm_token=${{ secrets.NPM_TOKEN }}
Секрет не попадёт в кэш и не испортит его переиспользуемость.
Варианты и нюансы конфигурации кэша
Несколько источников кэша
Вы можете указать несколько cache-from, например общий кэш main-ветки и кэш текущей ветки. BuildKit возьмёт всё, что найдёт.
cache-from: |
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-main
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-${{ github.ref_name }}
cache-to: |
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-${{ github.ref_name }},mode=max
Так разработчики в фиче сразу получают бенефит общего кэша из main, а затем наращивают свой веточный.
Многоплатформенная сборка
Registry-cache работает и для multi-arch. Рекомендуется один общий ref, если у вас единый пайплайн:
platforms: linux/amd64,linux/arm64
cache-from: type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache
cache-to: type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache,mode=max
Если вы часто гоняете независимые билды по платформам, можно разделить на два ref, чтобы уменьшить гонки и перезаписи. Если нужен постоянный исполнитель, поднимите его на нашем облачном VDS — это даст стабильный локальный кэш и контроль над окружением раннера.
Стабильность и гонки
Одновременная запись в один и тот же ref разными джобами бывает чувствительной. Стратегии смягчения:
- Разделять кэш по веткам, как в примере выше.
- Использовать один общий read-only ref (только
cache-from) и писать в веточный ref. - Включать
ignore-error=trueдляcache-to, чтобы сбои публикации кэша не валили сборку:
cache-to: type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache,mode=max,ignore-error=true

Inline-кэш vs registry-кэш vs CI-кэш провайдера
- type=inline: кэш вшит в сам образ. Просто, но увеличивает размер и требует пуша образа перед использованием кэша. Удобно как доп. источник (
cache-from), но нежелательно как основной. - type=gha или аналогичный кэш провайдера: простая интеграция в пределах одной платформы CI, но кэш не разделяется с другой инфраструктурой и может иметь квоты/ограничения.
- type=registry: кэш живёт рядом с образами, общедоступен для ваших раннеров и разработчиков (при наличии прав), не раздувает сами образы и не привязан к конкретному CI.
Безопасность
- Не публикуйте кэш в публичный реестр, если собираете приватный код. Кэш содержит метаданные сборки.
- Соблюдайте принцип наименьших прав для аккаунта, который пушит/читает кэш.
- Секреты всегда передавайте через
--mount=type=secretи не записывайте их в файлы, попадающие в слои. - Пинируйте базовые образы по тегам или digest для предсказуемости кэша. При использовании «скользящих» тегов кэш будет чаще инвалидироваться.
Поддержка и очистка registry-кэша
У BuildKit для type=registry нет штатного параметра TTL. Очистка решается политиками реестра, ручным удалением ненужных тегов кэша, ротацией ref (например, раз в неделю менять суффикс) или задачами обслуживания.
Распространённые подходы:
- Ротация: хранить 2–3 последних ref кэша и периодически удалять старые.
- Разделять кэш по веткам и удалять ref для удалённых веток.
- Настроить политики в реестре: удаление старых тегов, неиспользуемых артефактов.
Диагностика и проверка попаданий в кэш
Включайте «plain» прогресс, чтобы видеть CACHED на шагах:
export BUILDKIT_PROGRESS=plain
Признаки здорового кэша: базовый образ скачивается один раз, установка зависимостей не выполняется повторно, большие слои помечаются как CACHED.
Типичные причины промахов по кэшу
- Слишком ранний COPY всего контекста. Сначала копируйте только манифесты зависимостей и устанавливайте их, затем — остальное.
- Непинированные зависимости. Скользящие версии ломают детерминизм.
- Случайно меняющиеся LABEL/ARG. Не подставляйте дату/время до «тяжёлых» шагов.
- Злоупотребление скачиванием из сети. Кэшируйте менеджеры пакетов через
--mount=type=cache. - Частая смена базового образа. При смене базового слоя все последующие слои потребуется пересобрать.
Продвинутые сценарии
Разделение кэша по типам сборок
Иногда полезно иметь разные ref для PR и для main. PR будут быстрее за счёт общего main, но писать они будут в собственный ref, не перетирая основной кэш:
cache-from: |
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-main
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-pr
cache-to: type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-pr,mode=max
Сборка без публикации образа
Иногда в PR вы не хотите пушить образ, а кэш — хотите. Это нормально: у docker/build-push-action можно оставить push: false, но при этом экспортировать кэш в реестр:
with:
push: false
cache-to: type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache-pr,mode=max
Сочетание локального и registry-кэша
Если у вас есть выделенные постоянные раннеры, добавьте локальный кэш как первый источник, а registry — как второй. Это даст минимальное время на «горячих» раннерах и стабильную производительность на «холодных»:
cache-from: |
type=local,src=/var/lib/docker/buildkit-cache
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache
cache-to: |
type=local,dest=/var/lib/docker/buildkit-cache
type=registry,ref=REGISTRY_EXAMPLE/ORG/APP:buildcache,mode=max
Локальные пути подберите под вашу инфраструктуру и убедитесь, что они переживают рестарт раннера.
Практический чек-лист
- Включен BuildKit и Buildx. Вы используете builder с драйвером
docker-container. - Dockerfile оптимизирован: правильный порядок
COPY, кэшируемые монтирования для зависимостей, секреты через--mount=type=secret. - Выбрана политика ref: общий для main, веточные для PR, либо единый ref для всего.
- В CI настроен логин в реестр и права на чтение/запись кэша.
- Есть план очистки кэша: ротация tag/ref, политики в реестре.
FAQ
Нужно ли пушить образ, чтобы заработал registry-кэш?
Нет. type=registry публикует именно кэш-артефакты. Образ можно не пушить (например, в PR), но кэш экспортировать — да.
Почему с type=inline кэш всё равно медленнее?
Потому что inline-кэш живёт внутри образа. Чтобы им воспользоваться, образ нужно уже иметь в реестре. Registry-кэш независим и доступен заранее. Плюс образы не раздуваются метаданными кэша.
Можно ли использовать один и тот же кэш для разных репозиториев?
Технически — да, если Dockerfile и контексты близки. Но это риск совпадений и загрязнения. Практичнее разделять ref по проектам и веткам, а общим делать только базовый слой или образы-«базисы», если это осознанная стратегия.
Итоги
Registry-backed cache в BuildKit — один из лучших способов ускорить Docker-сборки в CI с эфемерными раннерами: общедоступный кэш для всех джоб, никаких привязок к дискам и провайдерам CI, гибкая политика изоляции по веткам и проектам. При грамотном Dockerfile и дисциплине зависимостей время сборки сокращается в разы, а инфраструктура становится проще и предсказуемее. Начните с базового cache-from/cache-to в реестр и добавляйте продвинутые фишки по мере развития пайплайна.


