Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

Docker BuildKit registry cache в CI: быстрые сборки на любом раннере

Долгие Docker‑сборки в CI — классическая боль: свежие раннеры, мало диска и кэш каждый раз с нуля. Registry‑backed cache в BuildKit решает проблему: кэш хранится в реестре и доступен из любого раннера и ветки. В статье — практика и готовые YAML для GitHub Actions.
Docker BuildKit registry cache в CI: быстрые сборки на любом раннере

Если вы используете 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-образы приватные, доступ к реестру и артефактам кэша должен быть настроен соответствующим образом.

Схема BuildKit registry cache: импорт и экспорт кэша между раннером и реестром

Быстрый старт: 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.
FastFox VDS
Облачный VDS-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Лучшие практики 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

Фрагмент workflow GitHub Actions с настройками buildx и registry‑кэша

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 в реестр и добавляйте продвинутые фишки по мере развития пайплайна.

Поделиться статьей

Вам будет интересно

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...