Неизменившиеся слои контейнерного образа проходят по быстрому пути, изменённый слой собирается заново
DevOps

Кеш сборки Docker: как ускорить build и не получить старый результат

Разбираемся, когда Docker переиспользует слои, как расположить COPY и RUN, настроить .dockerignore и cache mounts, а также проверить чистую сборку.

Содержание

Кеш сборки — это не архив готового контейнера, а возможность повторно использовать результат инструкций Dockerfile. Он ускоряет работу, пока входы конкретного шага не изменились. Если один слой пришлось собрать заново, пересобираются и все следующие за ним слои.

Как сохранить полезный кеш

Расположите дорогие и редко меняющиеся шаги раньше часто меняющегося кода. Сначала копируйте manifest и lock-файл, устанавливайте зависимости, затем добавляйте исходники. Исключите мусор из build context через .dockerignore, а кеш пакетного менеджера подключайте BuildKit mount. Периодически выполняйте контрольную сборку с --pull --no-cache, но не превращайте её в режим каждой разработки.

Как Docker решает, использовать ли слой

Для COPY и ADD builder учитывает содержимое и метаданные задействованных файлов. Для RUN важна сама инструкция и предшествующее состояние сборки: Docker не запускает команду, чтобы выяснить, изменился ли результат во внешнем репозитории. Поэтому неизменившаяся строка RUN apt-get update не означает, что индекс пакетов обязательно будет скачан заново.

Практическое следствие простое: шаг зависит только от того, что было доступно ему до выполнения. Чем раньше вы копируете весь репозиторий, тем раньше случайная правка README, теста или локального файла сбрасывает полезный кеш.

Сначала установите зависимости, затем копируйте код

Для приложения Node.js базовый порядок выглядит так. Фрагмент размещается в Dockerfile: сначала в образ попадают только два файла со списком зависимостей, и лишь после npm ci копируется остальной проект:

# syntax=docker/dockerfile:1
FROM node:24-alpine AS build
WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci

COPY . .
RUN npm test && npm run build

После правки файла в src/ builder повторит COPY . ., тест и сборку приложения, но сможет взять слой npm ci из кеша. При изменении package-lock.json зависимости установятся заново — это правильная инвалидация.

Тот же принцип действует для других экосистем:

  • Python: отдельно копировать requirements.txt, lock-файл Poetry или uv;
  • Go: сначала go.mod и go.sum, затем go mod download;
  • PHP: сначала composer.json и composer.lock;
  • Java: отделять файлы сборочной системы и описание зависимостей от исходников.

Не копируйте только manifest, забывая lock-файл. Тогда разрешение версий может меняться между сборками, хотя Dockerfile выглядит одинаково.

Исключите ненужные файлы из контекста сборки

.dockerignore сокращает набор файлов, который клиент отправляет builder. Это одновременно ускоряет передачу, уменьшает число случайных cache miss и снижает риск отправить секреты.

.git
.env
.env.*
node_modules
coverage
dist
*.log

Список нельзя механически копировать в любой проект. Например, если финальный dist собирается вне Docker и затем добавляется в образ, исключать его нельзя. Проверяйте, какие именно файлы используются COPY.

В выводе docker build --progress=plain . найдите строку transferring context. Неожиданно большой объём — повод проверить игнорирование локальных зависимостей, Git-истории, дампов и результатов предыдущей сборки.

Используйте cache mounts для пакетов

Даже когда слой установки инвалидирован, повторно скачивать каждый пакет необязательно. BuildKit mount сохраняет кеш пакетного менеджера отдельно от файлов итогового слоя:

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt,sharing=locked \
    apt-get update && apt-get install -y --no-install-recommends curl

Путь и режим зависят от менеджера пакетов. Cache mount нельзя считать частью образа: сборка должна оставаться корректной и с пустым кешем. Не храните в нём секреты и не используйте его вместо lock-файла.

Сравните две последовательные сборки

В каталоге с Dockerfile соберите один и тот же образ дважды с подробным выводом. Это диагностические команды: они заменяют только локальный тег example-app:cache-test и не затрагивают работающий контейнер:

docker build --progress=plain -t example-app:cache-test .
docker build --progress=plain -t example-app:cache-test .

Во второй сборке неизменившиеся шаги должны иметь отметку CACHED. Затем измените один файл приложения и повторите команду. Установка зависимостей должна остаться в кеше, а копирование кода и следующие шаги — выполниться заново.

Посмотреть распределение места можно так:

docker system df
docker buildx du

Вторая команда показывает кеш выбранного Buildx builder, если он доступен. Не сравнивайте только общее время: сеть, нагрузка диска и загрузка base image способны исказить единичный замер.

Когда нужна чистая сборка

Для проверки свежей основы и полного повторения инструкций выполните в каталоге проекта отдельную контрольную сборку. Новый тег example-app:clean-check не заменит тег работающего приложения:

docker build --pull --no-cache -t example-app:clean-check .

--pull проверяет базовый образ, --no-cache не переиспользует результаты шагов. Такая сборка полезна в периодической CI-проверке и при расследовании расхождения. Она не исправляет незакреплённые зависимости и не делает mutable tag воспроизводимым.

После проверки удалите только тестовый тег:

docker image rm example-app:clean-check

Не начинайте с docker system prune -a: команда затрагивает ресурсы всех проектов на Docker host. Если место действительно заканчивается, сначала изучите вывод docker system df, выясните владельца ресурсов и только затем удаляйте конкретные неиспользуемые объекты.

Частые причины странного кеша

  • COPY . . расположен до установки зависимостей;
  • в контекст попадают .git, логи, дампы или локальная сборка;
  • незакреплённая зависимость меняется во внешнем репозитории, а строка RUN остаётся прежней;
  • разные CI runners используют отдельные builders и не имеют общего external cache;
  • сборка зависит от времени, случайных данных или сетевого ресурса без checksum;
  • secret передан через ARG или COPY, хотя для него нужен secret mount.

Сначала сделайте Dockerfile воспроизводимым, затем оптимизируйте скорость. Кеш не должен маскировать неполное описание входов.

Базовую структуру образа разбирает материал о воспроизводимом Dockerfile, правила выбора версии — статья о тегах и digest, а временные учётные данные следует передавать по правилам хранения секретов.

Источники

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Какой порядок лучше сохраняет кеш зависимостей Node.js?
02Для чего нужен BuildKit cache mount?
03Что делает --pull при docker build?

Разбираем коротко

Частые вопросы

Почему Docker не переустанавливает зависимости после изменения исходного кода?

Если manifest и lock-файл скопированы и обработаны раньше исходников, их слой остаётся пригодным для кеша. Изменение кода инвалидирует только COPY исходников и последующие шаги.

Одинаковы ли параметры --no-cache и --pull?

Нет. --no-cache запрещает переиспользовать кеш инструкций, а --pull запрашивает свежую версию базового образа. Для полностью свежей контрольной сборки параметры можно сочетать.

Можно ли удалить весь кеш сборки без проверки?

Не стоит. Сначала посмотрите docker system df и используемый builder. Полная очистка может замедлить следующие сборки и удалить кеш других проектов на том же узле.