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

BuildKit cache mounts: ускоряем Composer/NPM сборки и экономим I/O

Cache mounts в Docker BuildKit — простой способ ускорить Composer и NPM/Yarn/PNPM сборки, уменьшить трафик и износ дисков. В статье — практические Dockerfile-примеры без раздутия образов, параметры mount, грабли с правами и инвалидацией кэша, мини-бенчмарк и рекомендации по CI/CD.
BuildKit cache mounts: ускоряем Composer/NPM сборки и экономим I/O

Если вы собираете 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 и других расширений.

Диаграмма потока кэша BuildKit для менеджеров пакетов

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.

Архитектура CI-билдера с персистентным BuildKit-кэшем на сервере

Стратегии инвалидации и стабильность сборок

Чтобы сборка была воспроизводимой и кэш не ломал детерминизм:

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

Частые вопросы и грабли

Нужно ли уникализировать 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, и ваши сборочные конвейеры станут ощутимо быстрее и стабильнее.

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

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

Как безопасно выполнить 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, а три ...