У нас на работе настроены автобилды проектов в МРах, и я недавно заметил, что время пайплайна уж слишком увеличилось: кол-во тестов всё увеличивается и увеличивается, добавляются новые stages в пайплайн. Возникла идея как-то уменьшить время сборки, поразмыслив вместе с ИИ, пришёл к новой для меня технологии — BuildKit cache mounts.
Суть технологии
Обычный docker build кэширует слои. Проблема в том, что слой со сборкой (RUN npm run build) инвалидируется при любом изменении исходников — то есть в каждом реальном МРе. Мб, прикрутить кешью?
BuildKit решает это через cache mount:
RUN --mount=type=cache,target=/app/node_modules/.cache npm run build
Такой mount подключает папку кэша только на время шага. Она живёт НЕ внутри слоя образа, а в отдельном кэш-сторе демона. Отсюда два свойства:
1. Кэш переживает инвалидацию слоя. Шаг пересобирается, а накопленный кэш остаётся на месте.
2. Стор общий на агенте. Это не «кэш одного МРа» — любая сборка на том же раннере читает и пишет в один стор, и разные МРы переиспользуют кэш друг друга.
Куда прикрутил
Кэш сам по себе ничего не ускоряет — нужен инструмент, который делает инкрементальную работу через папку-кэш. У меня таких оказалось два.
webpack. Включил cache: { type: 'filesystem' } и примонтировал папку кэша. webpack перестал гонять babel и минификацию по неизменённым модулям. Замер на агенте: около -2 минут на тёплой сборке.
jest. Он не запускает .ts/.tsx напрямую — сначала транспилирует каждый файл через ts-jest в JS, а это фактически прогон компилятора TypeScript. Кэш этого jest по умолчанию пишет в системный tmpdir, который в контейнере одноразовый. Перенёс cacheDirectory в node_modules/.cache и примонтировал тем же mount. Теперь на тёплой сборке транспилируются только изменённые тест-файлы.
Корректность: почему это не врёт на CI
Наш раннер делает свежий git clone на каждую сборку — у всех файлов новые mtime. Я думал, что это обнулит кэш. Но и webpack (в production) и ts-jest хешируют кэш по КОНТЕНТУ файла, а не по времени. Контент тот же — хэш тот же — кэш попадает.
Что кэшируется, а что нет
- Кэшируется дорогое: транспиляция и минификация. Тесты при этом выполняются каждый раз — экономия на компиляции, не на прогоне.
- SCSS-модули webpack в кэш не сериализует, так что стили пересобираются всегда.
Результат
Значимое сокращение времени сборки, у нас получился результат от 2 до 5 минут. Результат зависит от проекта, поэтому у вас уменьшение может быть ещё бОльшим.
Грабли
- Нужен DOCKER_BUILDKIT=1 и директива # syntax=docker/dockerfile:1.4, иначе RUN --mount падает.
- Стор надо чистить, иначе растёт бесконечно: docker builder prune --filter type=exec.cachemount --filter until=72h (удаляет только простаивающее 3+ дня).
- Кэш локален для агента. Новая виртуалка — потеря пользы от кэша.
Полезные материалы
- Cache mounts: https://docs.docker.com/build/cache/optimize/#use-cache-mounts
- RUN --mount reference: https://docs.docker.com/reference/dockerfile/#run---mounttypecache
- webpack persistent cache: https://webpack.js.org/configuration/cache/
- jest cacheDirectory: https://jestjs.io/docs/configuration#cachedirectory-string
#fullstack
Суть технологии
Обычный docker build кэширует слои. Проблема в том, что слой со сборкой (RUN npm run build) инвалидируется при любом изменении исходников — то есть в каждом реальном МРе. Мб, прикрутить кешью?
BuildKit решает это через cache mount:
RUN --mount=type=cache,target=/app/node_modules/.cache npm run build
Такой mount подключает папку кэша только на время шага. Она живёт НЕ внутри слоя образа, а в отдельном кэш-сторе демона. Отсюда два свойства:
1. Кэш переживает инвалидацию слоя. Шаг пересобирается, а накопленный кэш остаётся на месте.
2. Стор общий на агенте. Это не «кэш одного МРа» — любая сборка на том же раннере читает и пишет в один стор, и разные МРы переиспользуют кэш друг друга.
Куда прикрутил
Кэш сам по себе ничего не ускоряет — нужен инструмент, который делает инкрементальную работу через папку-кэш. У меня таких оказалось два.
webpack. Включил cache: { type: 'filesystem' } и примонтировал папку кэша. webpack перестал гонять babel и минификацию по неизменённым модулям. Замер на агенте: около -2 минут на тёплой сборке.
jest. Он не запускает .ts/.tsx напрямую — сначала транспилирует каждый файл через ts-jest в JS, а это фактически прогон компилятора TypeScript. Кэш этого jest по умолчанию пишет в системный tmpdir, который в контейнере одноразовый. Перенёс cacheDirectory в node_modules/.cache и примонтировал тем же mount. Теперь на тёплой сборке транспилируются только изменённые тест-файлы.
Корректность: почему это не врёт на CI
Наш раннер делает свежий git clone на каждую сборку — у всех файлов новые mtime. Я думал, что это обнулит кэш. Но и webpack (в production) и ts-jest хешируют кэш по КОНТЕНТУ файла, а не по времени. Контент тот же — хэш тот же — кэш попадает.
Что кэшируется, а что нет
- Кэшируется дорогое: транспиляция и минификация. Тесты при этом выполняются каждый раз — экономия на компиляции, не на прогоне.
- SCSS-модули webpack в кэш не сериализует, так что стили пересобираются всегда.
Результат
Значимое сокращение времени сборки, у нас получился результат от 2 до 5 минут. Результат зависит от проекта, поэтому у вас уменьшение может быть ещё бОльшим.
Грабли
- Нужен DOCKER_BUILDKIT=1 и директива # syntax=docker/dockerfile:1.4, иначе RUN --mount падает.
- Стор надо чистить, иначе растёт бесконечно: docker builder prune --filter type=exec.cachemount --filter until=72h (удаляет только простаивающее 3+ дня).
- Кэш локален для агента. Новая виртуалка — потеря пользы от кэша.
Полезные материалы
- Cache mounts: https://docs.docker.com/build/cache/optimize/#use-cache-mounts
- RUN --mount reference: https://docs.docker.com/reference/dockerfile/#run---mounttypecache
- webpack persistent cache: https://webpack.js.org/configuration/cache/
- jest cacheDirectory: https://jestjs.io/docs/configuration#cachedirectory-string
#fullstack