Если вы собираете PHP/Node.js образы каждый день, то львиная доля времени уходит на загрузку артефактов и запись их во временные каталоги менеджеров пакетов. Docker BuildKit с cache mounts решает именно эту проблему: повторно использует кэш директорий Composer/NPM/Yarn/PNPM между сборками, резко снижает I/O и ускоряет конвейер.
Что такое cache mounts в BuildKit
BuildKit добавил в Dockerfile директивы RUN --mount=..., позволяющие монтировать в шаг сборки разные источники: кэш, секреты, бинды. Тип type=cache создаёт персистентное хранилище на стороне билдера и подключает его в контейнер сборки в указанную папку. Данные кэша переживают шаги и сборки, пока вы их явно не очистите.
В отличие от слоёв образа, cache mount:
- не попадает в итоговый образ (нет риска раздуть размер артефактами);
- не зависит от порядка команд в Dockerfile, кэшируется отдельно от layer cache;
- может быть одним и тем же хранилищем для множества проектов (через общий
id=...).
Слои Docker отвечают за неизменяемые снапшоты ФС. Cache mounts — это локальные, персистентные для билдера директории, которые BuildKit монтирует на время шага
RUN.
Общие параметры cache mount
Чаще всего используются такие параметры:
target— путь в контейнере сборки, куда монтируется кэш.id— имя кэша. Одинаковыйidпозволяет переиспользовать один кэш между разными проектами/контекстами.sharing— стратегия совместного доступа:shared(по умолчанию),locked(рекомендуется для менеджеров пакетов),private.uid/gid/mode— права на директорию кэша внутри контейнера сборки (полезно при пользователе без root).ro— монтировать только для чтения (редко для кэша пакетов, т.к. он должен пополняться).
Подготовка: включаем BuildKit и синтаксис Dockerfile
В современных версиях Docker BuildKit включён по умолчанию. Если нужно задать явно, используйте переменную окружения при сборке:
DOCKER_BUILDKIT=1 docker build -t app:dev .
И добавьте в Dockerfile синтаксис:
# syntax=docker/dockerfile:1.4
Эта строка включает поддержку RUN --mount и других расширений.

Composer: правильные каталоги кэша и пример Dockerfile
Composer хранит кэш в $COMPOSER_CACHE_DIR. По умолчанию это внутри $COMPOSER_HOME/cache. Чтобы поведение было предсказуемым, явно зададим переменные и замонтируем cache mount именно туда.
# syntax=docker/dockerfile:1.4
FROM php:8.3-cli-bookworm AS builder
ENV COMPOSER_HOME=/tmp/composer
ENV COMPOSER_CACHE_DIR=/tmp/composer/cache
WORKDIR /app
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer/cache,id=composer-cache,sharing=locked composer install --no-dev --prefer-dist --no-interaction --no-progress --no-scripts
COPY . .
RUN --mount=type=cache,target=/tmp/composer/cache,id=composer-cache,sharing=locked composer dump-autoload --no-dev --classmap-authoritative
Ключевые моменты:
- Сначала копируем
composer.jsonиcomposer.lock; это даёт возможность повторно использовать кэш и слои, пока lock не меняется. sharing=lockedснижает риск гонок, если параллельно идут несколько сборок на одном билдере.- Кэш не попадёт в итоговый образ и не увеличит его размер.
Non-root пользователь и права
Если сборка идёт от не-привилегированного пользователя, задайте uid/gid, чтобы Composer мог писать кэш:
RUN --mount=type=cache,target=/tmp/composer/cache,id=composer-cache,uid=1000,gid=1000,mode=0775,sharing=locked composer install --no-dev --prefer-dist --no-interaction --no-progress
Секреты для приватных репозиториев
Часто нужен auth.json или токен для приватных пакетов. Используйте секреты BuildKit — так токен не попадёт в слои:
# Предположим, секрет доступен как id=composer_auth
RUN --mount=type=secret,id=composer_auth,target=/tmp/composer/auth.json,required=true --mount=type=cache,target=/tmp/composer/cache,id=composer-cache,sharing=locked COMPOSER_AUTH=/tmp/composer/auth.json composer install --no-interaction
NPM/Yarn/PNPM: где кэш и как монтировать
Для Node.js менеджеров пакетов важно знать путь к кэшу и зафиксировать его явно, чтобы mount попадал точно в нужную директорию.
- NPM: кэш по умолчанию в домашней директории пользователя. Рекомендуется задать
NPM_CONFIG_CACHE, например/root/.npm. - Yarn v1: можно указать
YARN_CACHE_FOLDER, например/usr/local/share/.cache/yarnили/root/.cache/yarn. - PNPM: хранилище в
~/.pnpm-store; задайтеPNPM_STORE_DIRили--store-dir.
Пример с NPM
# syntax=docker/dockerfile:1.4
FROM node:22-bookworm AS node-builder
WORKDIR /app
ENV NPM_CONFIG_CACHE=/root/.npm
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,id=npm-cache,sharing=locked npm ci --ignore-scripts --no-audit --no-fund
COPY . .
RUN --mount=type=cache,target=/root/.npm,id=npm-cache,sharing=locked npm run build
Пример с Yarn
# syntax=docker/dockerfile:1.4
FROM node:22-bookworm AS yarn-builder
WORKDIR /app
ENV YARN_CACHE_FOLDER=/root/.cache/yarn
COPY package.json yarn.lock ./
RUN --mount=type=cache,target=/root/.cache/yarn,id=yarn-cache,sharing=locked yarn install --frozen-lockfile --ignore-scripts --non-interactive
COPY . .
RUN --mount=type=cache,target=/root/.cache/yarn,id=yarn-cache,sharing=locked yarn build
Пример с PNPM
# syntax=docker/dockerfile:1.4
FROM node:22-bookworm AS pnpm-builder
WORKDIR /app
ENV PNPM_HOME=/root/.local/share/pnpm
ENV PNPM_STORE_DIR=/root/.pnpm-store
COPY package.json pnpm-lock.yaml ./
RUN corepack enable
RUN --mount=type=cache,target=/root/.pnpm-store,id=pnpm-store,sharing=locked pnpm install --frozen-lockfile --prefer-offline
COPY . .
RUN --mount=type=cache,target=/root/.pnpm-store,id=pnpm-store,sharing=locked pnpm build
Сборка PHP+Node в одном Dockerfile: multi-stage
Частый кейс: собрать PHP-зависимости и фронтенд ассеты, а затем собрать минимальный runtime-образ без dev-инструментов.
# syntax=docker/dockerfile:1.4
FROM php:8.3-cli-bookworm AS php-builder
WORKDIR /app
ENV COMPOSER_HOME=/tmp/composer
ENV COMPOSER_CACHE_DIR=/tmp/composer/cache
COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer/cache,id=composer-cache,sharing=locked composer install --no-dev --prefer-dist --no-interaction --no-progress
COPY . .
FROM node:22-bookworm AS assets-builder
WORKDIR /app
ENV NPM_CONFIG_CACHE=/root/.npm
COPY --from=php-builder /app /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,id=npm-cache,sharing=locked npm ci --ignore-scripts --no-audit --no-fund
RUN --mount=type=cache,target=/root/.npm,id=npm-cache,sharing=locked npm run build
FROM php:8.3-fpm-bookworm AS runtime
WORKDIR /var/www/app
COPY --from=php-builder /app /var/www/app
COPY --from=assets-builder /app/public/build /var/www/app/public/build
CMD ["php-fpm"]
В результате в финальном образе нет ни Composer, ни NPM, ни их кэшей. Кэш остаётся на стороне билдера и ускоряет последующие сборки.
А как насчёт apt/pip и других менеджеров?
Cache mounts подходят и для системных менеджеров пакетов, если нужно уменьшить сетевой шум. Пример для APT:
RUN --mount=type=cache,target=/var/cache/apt,id=apt-cache,sharing=locked --mount=type=cache,target=/var/lib/apt,id=apt-lib,sharing=locked apt-get update && apt-get install -y git unzip
Но помните: индексы репозиториев быстро устаревают. Если кэш стал слишком «липким», периодически очищайте его через docker builder prune или меняйте id.
Производительность: чего ожидать
Практические наблюдения на типичных проектах:
- Первый билд — без выигрыша (кэш пустой), но уже второй и далее: Composer/NPM скачивают лишь дельты и читают много из локального кэша.
- Ускорение шагов установки зависимостей в 2–10 раз в зависимости от числа пакетов и пропускной способности сети.
- Снижение пикового I/O и сетевой активности, особенно важно на средах с ограниченными IOPS.
Самый большой эффект cache mounts дают на CI-агентах и сборочных VDS, где десятки билдов проходят ежедневно: кэш «прогревается» и становится общим для проектов. Если собираете много образов, имеет смысл вынести builder на собственный VDS.

Стратегии инвалидации и стабильность сборок
Чтобы сборка была воспроизводимой и кэш не ломал детерминизм:
- Для Composer используйте
composer.lock; для NPM —package-lock.json, Yarn —yarn.lock, PNPM —pnpm-lock.yaml. - Копируйте в образ только lock-файлы перед установкой зависимостей. Остальной код копируйте позже.
- Фиксируйте версии инструментов (Composer, npm, yarn, pnpm), чтобы формат кэша не «гулял» между версиями.
CI/CD: GitHub Actions, GitLab, TeamCity и др.
Cache mounts живут внутри билдера. Это означает:
- Если каждый раз создаётся новый ephemeral builder, кэш не сохранится. Настройте persistent builder на агенте.
- Для нескольких агентов кэш не реплицируется сам по себе. Пингуйте сборки к одному билдеру или используйте внешний layer cache (отдельная тема).
- Очищайте периодически кэш, чтобы избежать «вспухания» диска: используйте
docker builder pruneи смотрите объёмы черезdocker system df.
Сравнение: cache mounts vs слойный кэш
Чем cache mounts отличаются от классического layer caching в Docker:
- Layer cache зависит от точности совпадения инструкций и контента. Любое изменение lock-файла — и слой пересобирается.
- Cache mounts не привязаны к слоям образа и «живут» в среде билдера. Даже если слой пересобирается, кэш пакетов остаётся тёплым.
- Cache mounts не увеличивают итоговый образ, в отличие от копирования кэшей внутрь.
Диагностика: как понять, что кэш работает
Признаки работающего cache mount:
- Менеджер пакетов сообщает о «from cache» или скачивает существенно меньше артефактов.
- Во время шага
RUNв директорииtargetпоявляются файлы; при последующих билдах их видно сразу. - Общий размер пространства билдера растёт после нескольких сборок — кэш заполняется.
Частые вопросы и грабли
Нужно ли уникализировать id для каждого проекта?
Не обязательно. Общий id для Composer или NPM позволит переиспользовать скачанные пакеты между проектами. Исключение — если вы опасаетесь конфликтов из-за нестабильных кэшей; тогда используйте префикс id с именем проекта.
Почему после очистки билдера сборка снова долгая?
Cache mounts локальны для билдера. После очистки (builder prune) кэш пуст, и нужно одно-два прохождения, чтобы прогреть его заново.
Можно ли сделать кэш «только для чтения»?
Технически да: ro. Практически для менеджеров пакетов это бесполезно — они должны записывать новые версии и индексы.
Как быть с monorepo и несколькими lock-файлами?
Монтируйте один общий кэш на менеджер пакетов и запускайте установку отдельно в каждом пакете. Это сохранит переиспользование артефактов на уровне менеджера.
Мини-бенчмарк
На проекте с ~80 PHP-пакетами и ~120 NPM-пакетами:
- Первый билд: Composer 40–60 секунд, NPM 90–120 секунд.
- Дальнейшие билды (без изменения lock): Composer 6–15 секунд, NPM 15–30 секунд.
Экономия времени в 4–8 раз и значительное снижение сетевого трафика. На дисках с ограниченным IOPS эффект ещё заметнее.
Рекомендованные практики
- Всегда явно задавайте директории кэша менеджера пакетов и монтируйте их как
type=cacheсsharing=locked. - Копируйте lock-файлы до установки зависимостей — это стабилизирует слои.
- Фиксируйте версии инструментов и node/php-образов в пределах минорных обновлений.
- Периодически чистите кэш билдера и следите за диском, особенно в CI.
- Используйте multi-stage, чтобы кэш не попадал в runtime-образ.
Итоги
Cache mounts в BuildKit — один из самых простых и недооценённых способов ускорить Docker-сборки с Composer и NPM/Yarn/PNPM. Пара строк в Dockerfile избавляют от повторной загрузки тысяч артефактов, уменьшают I/O, экономят трафик и время разработчиков. Настройте постоянный builder, зафиксируйте пути к кэшу, используйте sharing=locked, и ваши сборочные конвейеры станут ощутимо быстрее и стабильнее.


