Кэширование слоёв в Dockerfile: как ускорить сборку образа в 2026 году | AdminWiki

Кэширование слоёв в Dockerfile: как ускорить сборку образа в 2026 году

26 сентября 2026 9 мин. чтения

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

Главный рычаг ускорения - порядок инструкций. Сначала идут редко меняющиеся вещи: базовый образ, системные пакеты, манифесты зависимостей. Затем установка зависимостей. И только в конце копируется код приложения, который меняется на каждом коммите. Приём, который даёт самый заметный эффект на практике: COPY package.json package-lock.json ./ перед RUN npm ci.

Ниже разбираем механику кэша, показываем готовые фрагменты Dockerfile для Node.js, Python и Go, команды проверки и чек-лист для локальной сборки и CI.

Как инструкции Dockerfile формируют слои и кэш

RUN, COPY и ADD создают слои: каждый хранит изменения файловой системы относительно предыдущего. ENV, WORKDIR, LABEL, CMD и ENTRYPOINT меняют только метаданные конфигурации и весят почти ноль, но в цепочку кэша входят наравне с остальными инструкциями. Базовый разбор сборки и структуры образа мы давали в материале сборка Docker-образа пошагово: Dockerfile, кэширование, multi-stage build.

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

Слой - результат выполнения ровно одной инструкции. При повторной сборке билдер сначала проверяет, закэширован ли базовый образ, а затем сравнивает каждую последующую инструкцию с кэшированными слоями: если ни один кэшированный слой не совпадает с инструкцией точно, кэш инвалидируется. Для инструкций, где метаданные файлов не проверяются, для поиска совпадения используется сама строка команды. Для COPY и ADD ключ кэша выводится из контрольной суммы метаданных копируемых файлов (без времени модификации), и кэш инвалидируется, если метаданные любого из файлов изменились. Аргументы сборки (build arguments) также приводят к инвалидации кэша, а вот содержимое build secrets частью кэша не является, и его изменение кэш не сбрасывает.

Совпало - берётся готовый слой, сборка шагает к следующей строке. Не совпало - инструкция выполняется заново, создаётся новый слой, и все последующие слои тоже пересобираются: после инвалидации кэша все последующие команды Dockerfile генерируют новые образы, и кэш больше не используется.

BuildKit активирован по умолчанию в Docker Desktop и Docker Engine версии 23.0 и выше; для более ранних версий его включают переменной DOCKER_BUILDKIT=1 при сборке. BuildKit анализирует зависимости между инструкциями Dockerfile и выполняет независимые стадии параллельно, а в его основе лежит LLB - контентно-адресуемый граф зависимостей для сборки. Результат сборки BuildKit автоматически кэширует во внутреннем кэше и дополнительно поддерживает экспорт кэша во внешнее хранилище для импорта в последующих сборках. Правило инвалидации последующих слоёв это не отменяет: изменилась инструкция, зависимые от неё шаги выполняются заново.

Кэш опирается на базовый образ, а не на строку тега как таковую: билдер проверяет, закэширован ли базовый образ, и при его обновлении слой FROM и весь последующий кэш становятся недействительными. Чтобы принудительно проверить и скачать более новую версию базового образа, даже если версия уже закэширована локально, используют флаг docker build --pull. Предсказуемость даёт фиксация digest: об этом подробно в статье как собирать и использовать Docker-образы для воспроизводимых сред.

Официальная документация Docker описывает механику инвалидации кэша в разделе Build cache invalidation, а поведение BuildKit и его кэша - в разделе Cache storage backends.

Почему изменение одной строки инвалидирует последующие слои

Возьмём Dockerfile, где код копируется до установки пакетов:

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN npm ci
CMD ["node", "server.js"]

Правка одного символа в src/server.js меняет контрольную сумму COPY . . . Слой COPY пересобирается, а вместе с ним и RUN npm ci: зависимости не менялись, но Docker об этом уже не знает, потому что родительский слой другой. Каждый коммит заново скачивает и распаковывает весь node_modules, а в CI это повторяется на каждом запуске пайплайна.

Тот же Dockerfile с правильным порядком:

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

Теперь правки кода бьют только по слоям после RUN npm ci, а установка зависимостей берётся из кэша. Поменялся манифест зависимостей - пересобирается всё, начиная с его копирования, и это ожидаемая плата.

Что изменилосьCOPY package.json и package-lock.jsonRUN npm ciCOPY . .
Файл в src/кэшкэшпересборка
package.json или package-lock.jsonпересборкапересборкапересборка
Строка CMD в конце файлакэшкэшкэш
Базовый образ (обновление за тегом)пересборкапересборкапересборка

Чем ниже изменяемая строка в Dockerfile, тем дешевле правка. Именно поэтому CMD и USER ставят в конец, а COPY манифестов - как можно раньше.

Файл .dockerignore решает вторую часть задачи. Без него COPY . . тянет в контекст .git, node_modules, coverage и локальный .env. Контекст больше, сборка дольше, а хеш слоя меняется от любого служебного файла. Минимальный набор для исключения: .git, node_modules, .env, *.pem, coverage, dist, .DS_Store.

Правильный порядок инструкций в Dockerfile

Принцип один: сверху вниз по частоте изменений. Вверху то, что меняется раз в месяцы, ниже - раз в недели, в самом низу - то, что меняется каждый коммит. Рабочий порядок выглядит так:

  1. FROM с зафиксированным digest базового образа.
  2. Системные пакеты: apt-get или apk add.
  3. Манифесты зависимостей: package.json, requirements.txt, go.mod и go.sum, composer.json и composer.lock.
  4. Установка зависимостей: npm ci, pip install -r, go mod download, composer install.
  5. Код приложения: COPY . .
  6. Сборка артефактов: npm run build, go build.
  7. USER, ENTRYPOINT, CMD.

Каждая группа - отдельный слой, тогда кэш переиспользуется максимально широко.

Приём COPY package.json перед установкой пакетов

Для Node.js рабочий фрагмент:

FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
USER node
CMD ["node", "server.js"]

Манифест зависимостей меняется редко, поэтому слой RUN npm ci живёт в кэше неделями и переиспользуется на каждой правке кода. Команда npm ci может устанавливать пакеты только при наличии существующего package-lock.json или npm-shrinkwrap.json с lockfileVersion >= 1; без него она завершается ошибкой с требованием сначала выполнить установку через npm@5 или новее. Это делает установку воспроизводимой и привязанной к зафиксированному lock-файлу.

Для Python порядок тот же, меняются имена файлов и флаги:

FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "app.py"]

Флаг --no-cache-dir не даёт кэшу пакетов pip остаться в образе. Для Go отдельно скачиваются модули, а сборка идёт в стадии build:

FROM golang:1.24 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app ./cmd/app

Для PHP-проектов на Composer кэшируемый слой даёт COPY composer.json composer.lock ./ с последующим composer install --no-dev --no-scripts, а COPY . . идёт после. Приём универсален: важно разделить редко меняющийся манифест и часто меняющийся код.

Разделение слоёв для системных пакетов и зависимостей

Установку системных пакетов оформляйте отдельным RUN до копирования манифестов:

RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates curl \
&& rm -rf /var/lib/apt/lists/*

Три детали здесь связаны с кэшем напрямую. update и install идут в одной инструкции, иначе устаревший индекс пакетов из кэша приведёт к ошибкам загрузки или к установке старых версий. rm -rf /var/lib/apt/lists/* стоит в том же слое, иначе списки пакетов навсегда останутся в образе. Отдельный RUN не смешивается с зависимостями приложения.

Объединять в один RUN установку системных пакетов и npm ci тоже не стоит: правка package.json потянет за собой переустановку всего набора пакетов ОС. Слои разной частоты изменений держите раздельно.

Как ускорить сборку в CI с помощью кэша

Локально кэш живёт в хранилище образов и в build cache, поэтому вторая сборка почти мгновенная. В CI раннер обычно чистый: кэша нет, и Docker собирает образ с нуля на каждом запуске. Порядок инструкций сокращает работу внутри образа, но повторную установку зависимостей он не убирает, пока кэш не вынесен за пределы раннера.

BuildKit экспортирует и импортирует кэш через бэкенды: registry, local, inline, gha и s3. В отличие от локального кэша BuildKit, который всегда включён, все эти бэкенды нужно явно экспортировать и явно импортировать. Бэкенд registry переносится между разными CI и не привязывает вас к вендору.

DOCKER_BUILDKIT=1 docker buildx build --progress=plain \
-t registry.example.com/team/app:ci \
--cache-from type=registry,ref=registry.example.com/team/app:buildcache \
--cache-to type=registry,ref=registry.example.com/team/app:buildcache \
.

Для self-hosted раннеров с постоянным диском дешевле локальный бэкенд: --cache-to type=local,dest=/var/cache/buildkit. Кэш растёт, поэтому чистите его через docker buildx prune и сборку мусора в registry. Секреты в кэшируемые слои не передавайте: используйте RUN --mount=type=secret, а не ARG и COPY .env. Больше про кэш сборки и registry в разборе кэширование в Docker и Kubernetes 2026.

Список поддерживаемых бэкендов и синтаксис флагов --cache-from и --cache-to описаны в документации Docker Cache storage backends и в справочнике buildx build.

Команды проверки кэша и времени сборки

Убедиться, что перестановка инструкций дала эффект, помогают четыре команды.

КомандаЧто показывает
docker build --progress=plain -t app:local .пошаговый лог сборки, у взятых из кэша слоёв стоит пометка CACHED
docker history app:localсписок слоёв с командами и размером, видно, какой слой раздут
docker buildx duсколько места занимает build cache
time docker build -t app:local .полное время сборки для сравнения до и после правок

Сравнивайте две сборки подряд на одном и том же коде: первая прогревает кэш, вторая показывает чистое время попадания. Ориентируйтесь на строку RUN npm ci в выводе: пометка CACHED после правки файла в src/ означает, что порядок инструкций настроен верно. Разбор слоёв и поиск «лишнего веса» через docker history и dive мы разбирали в статье архитектура Docker-образов: слои и анализ с docker inspect и dive.

Чек-лист для ускорения сборок в 2026 году

Список рассчитан на применение за один проход. Безопасные шаблоны Dockerfile с non-root пользователем и фиксацией версий собраны в подборке готовых Dockerfile для Python, Node.js и Go.

  1. Вынесите манифесты зависимостей в отдельный COPY перед установкой: package.json и package-lock.json, requirements.txt, go.mod и go.sum.
  2. Установку зависимостей держите отдельным RUN: npm ci, pip install --no-cache-dir -r, go mod download, composer install.
  3. Системные пакеты ставьте одним RUN до манифестов, с apt-get update, install и rm -rf /var/lib/apt/lists/* в одной инструкции.
  4. Код приложения копируйте последним: COPY . . после установки зависимостей.
  5. Добавьте .dockerignore с .git, node_modules, .env, *.pem, coverage, dist.
  6. Включите BuildKit и настройте вынос кэша в CI: registry или local бэкенд.
  7. Проверяйте результат через docker build --progress=plain и docker history, не забывайте сверять пометки CACHED.
  8. Не копируйте секреты в образ. .env и ключи, попавшие в слой через COPY, останутся в истории образа, и удаление в следующей инструкции их не уберёт. Передавайте их через RUN --mount=type=secret.
  9. Запускайте контейнер не от root: USER app в конце Dockerfile.
  10. Фиксируйте digest базового образа и lock-файлы зависимостей, тогда кэш ведёт себя предсказуемо.

Начните с одного изменения: перенесите COPY манифестов выше RUN npm ci, соберите образ дважды с флагом --progress=plain и проверьте, что во второй сборке установка зависимостей помечена как CACHED, а время сборки сократилось.

Поделиться: