BuildKit и cache mounts: ускорение сборки
BuildKit это новый backend для docker build, который заменил legacy builder начиная с Docker 23 (default с 23.0, обязателен с 28). Он принёс параллельность сборок stages, лучший cache и принципиально новые возможности: RUN --mount для cache (pip / npm / apt) и secrets. Это превратило сборки из «pull зависимостей каждый build» в «pull один раз, потом мгновенно».
В этом уроке: что нового BuildKit, как использовать cache mounts для pip и npm, и как монтировать secrets без утечки в слои.
Что такое BuildKit
BuildKit это:
- Параллельная сборка stages — multi-stage stages, не зависящие друг от друга, собираются параллельно
- Continued building при ошибке — если падает один stage, BuildKit продолжает собирать те, что не зависят от него
- Лучший cache — content-aware hashing, поддержка remote cache backends
- Frontends — Dockerfile это один из «языков» для BuildKit, можно использовать другие (HCL, Earthly и т.п.)
RUN --mount— типа cache, типа secret, типа bind, типа ssh- Multi-platform builds —
docker buildxдля linux/amd64 + linux/arm64 одновременно
Проверить что используется:
$ docker version
Server: Docker Engine - Community
Engine:
Version: 28.0.0
...
builder:
Version: v0.26.0 # BuildKit
В современном Docker (23+) BuildKit это default. На очень старых versions нужно было DOCKER_BUILDKIT=1 docker build или включить в /etc/docker/daemon.json.
Mount, /etc/fstab и mount namespaces
RUN —mount=type=cache: персистентный cache между builds
Базовая проблема: при docker build каждый раз pip install скачивает пакеты с PyPI. Если кэш ~/.cache/pip сохраняется внутри образа — он раздувает слой; если очищается через --no-cache-dir или RM — теряем скорость на repeated builds.
RUN --mount=type=cache решает эту проблему:
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Что происходит:
- BuildKit создаёт persistent cache volume (не часть образа), смонтированный в
/root/.cache/pipво время выполнения этого RUN. - pip качает пакеты, складывает в /root/.cache/pip.
- После RUN cache volume отмонтируется, в слой попадает только результат
pip install(установленные packages в site-packages). - При следующем build: тот же cache volume монтируется снова, pip видит уже скачанные wheels — install мгновенный.
$ docker build -t app:v1 .
[+] Building 45.5s # первый раз: качаем pandas, numpy, etc
# Меняем код, не requirements.txt
$ docker build -t app:v1 .
[+] Building 1.8s # CACHED
# Меняем requirements.txt -- добавили новую зависимость
$ docker build -t app:v1 .
[+] Building 3.2s # только новая зависимость качается, остальные из cache mount
Cache живёт между builds, в /var/lib/docker/buildkit/cache-volumes/. Не попадает в финальный образ. Можно очистить через docker builder prune.
Cache mounts для разных языков
pip (Python):
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
npm (Node.js):
RUN --mount=type=cache,target=/root/.npm \
npm install
pnpm:
RUN --mount=type=cache,target=/pnpm/store \
pnpm install --frozen-lockfile
Go (modules):
RUN --mount=type=cache,target=/go/pkg/mod \
--mount=type=cache,target=/root/.cache/go-build \
go build ./...
apt:
RUN --mount=type=cache,target=/var/cache/apt \
--mount=type=cache,target=/var/lib/apt \
apt-get update && apt-get install -y libpq5
Для apt нужно ещё одно: убрать apt-get clean и не делать rm -rf /var/lib/apt/lists/* (это противоречит тому, что было в модуле 7 для single-stage; здесь apt-cache живёт в cache mount, не в слое). Также важно убрать default /etc/apt/apt.conf.d/docker-clean или поставить Acquire::http::No-Cache "false";.
Maven:
RUN --mount=type=cache,target=/root/.m2 \
mvn package
Параметры cache mount
RUN --mount=type=cache,target=/path,id=unique-id,sharing=locked,mode=0755,uid=1000,gid=1000 \
command
target— путь внутри контейнера build’аid— уникальный ID cache volume (default = target). Используй чтобы шарить cache между Dockerfile’ами, либо изолировать.sharing—shared(default, конкурентные builds могут одновременно работать с cache),private(каждый build создаёт свой, новый, копию),locked(один build за раз)mode— permissions cache mount (default 0755)uid,gid— owner если внутри Dockerfile USER не root
Pin id для shared cache между разными Dockerfile’ами:
# Dockerfile.web
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm install
# Dockerfile.api
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
npm install
Оба Dockerfile шарят cache по id npm-cache. Если в проекте web/ и api/ имеют общие npm-deps — cache переиспользуется.
RUN —mount=type=secret: secrets без утечки
До BuildKit secrets было сложно: --build-arg DB_PASSWORD=secret запинит секрет в image history навечно. Распространённые workarounds (multi-stage с throwaway копированием) сложны.
--mount=type=secret решает это:
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
# Доступ к private PyPI с auth
RUN --mount=type=secret,id=pypi_auth \
pip install --index-url https://$(cat /run/secrets/pypi_auth)@pypi.private.com/simple -r requirements.txt
COPY . .
CMD ["python", "main.py"]
Build:
docker build \
--secret id=pypi_auth,src=$HOME/.pypi-auth \
-t app:v1 .
Что происходит:
- BuildKit монтирует файл
~/.pypi-authв/run/secrets/pypi_authвнутри build’а - RUN читает оттуда секрет, использует для pip install
- После RUN секрет не попадает ни в слой, ни в image history
Альтернативно через env (BuildKit читает из env-переменной):
PYPI_AUTH=$(cat ~/.pypi-auth) docker build \
--secret id=pypi_auth,env=PYPI_AUTH \
-t app:v1 .
В GitHub Actions:
- name: Build
uses: docker/build-push-action@v5
with:
context: .
push: true
secrets: |
pypi_auth=${{ secrets.PYPI_AUTH }}
RUN —mount=type=ssh: SSH для git clone
Для приватных git-репо во время сборки:
# syntax=docker/dockerfile:1.7
RUN --mount=type=ssh \
git clone [email protected]:org/private-repo.git /tmp/repo
ssh-add ~/.ssh/id_ed25519 # добавить ключ в agent
docker build --ssh default -t app:v1 .
BuildKit пробрасывает SSH-agent в build, git может авторизоваться через него. Ключ в образ не попадает.
docker buildx и multi-platform builds
docker buildx это надстройка над BuildKit, которая добавляет:
- Multi-platform builds (
--platform linux/amd64,linux/arm64) - Remote builders (для shared CI cache)
buildx bake(HCL-конфиг для группированных builds)
# Создать builder с поддержкой multi-platform (через QEMU emulation)
docker buildx create --use --name multi
# Build для двух платформ
docker buildx build \
--platform linux/amd64,linux/arm64 \
-t ghcr.io/org/app:v1 \
--push \
.
Результат — multi-arch manifest list в registry. При pull на amd64 — amd64 вариант, на arm64 — arm64. Это критично для команд с MacBook M-серии (arm64) и Linux x86 production (amd64).
Remote cache: ускорение CI
В CI каждый runner начинает с пустым local cache. --cache-to / --cache-from решают это, сохраняя cache в registry:
# Push cache в registry после build
docker buildx build \
--cache-to type=registry,ref=ghcr.io/org/app:cache,mode=max \
--cache-from type=registry,ref=ghcr.io/org/app:cache \
-t ghcr.io/org/app:v1 \
--push \
.
Cache хранится как отдельный image-tag (app:cache). На следующем build CI pull’ит cache из registry, и build идёт «инкрементально». Это уменьшает CI-build с 5 минут до 30 секунд для типичного DE-сервиса.
Альтернативные cache backends:
type=gha— GitHub Actions cachetype=local— локальная директория (mount в CI-volume)type=s3— AWS S3
Попробуй сам
Сравни с и без cache mount:
mkdir buildkit-cache && cd buildkit-cache
cat > requirements.txt <<'EOF'
pandas==2.2.3
numpy==2.1.3
psycopg2-binary==2.9.10
requests==2.32.3
EOF
# Без cache mount
cat > Dockerfile.no-cache <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
EOF
# С cache mount
cat > Dockerfile.cache <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
EOF
# Первый build без cache
docker build --no-cache -f Dockerfile.no-cache -t test:no-cache .
# ~30s, качает с PyPI
# Изменим requirements -- добавим новую зависимость
echo "rich==13.9.4" >> requirements.txt
# Второй build без cache mount -- pip качает заново ВСЁ
time docker build -f Dockerfile.no-cache -t test:no-cache .
# 30s
# Build с cache mount (первый раз -- кэш пуст)
docker build --no-cache -f Dockerfile.cache -t test:cache .
# Снова добавим зависимость
echo "click==8.1.7" >> requirements.txt
# Build с cache mount -- pip использует cache, качает только новую
time docker build -f Dockerfile.cache -t test:cache .
# ~3s -- огромная разница
cd .. && rm -rf buildkit-cache
docker rmi test:no-cache test:cache