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