Главная/Dockerfile/Урок

5.6. BuildKit

Цели

После этого материала вы сможете:

  • объяснить, чем BuildKit отличается от legacy builder и что это даёт на практике;
  • использовать директиву # syntax и понимать, зачем она нужна;
  • применять cache mounts для менеджеров пакетов и измерять эффект;
  • передавать секреты через secret mounts вместо ARG и доказывать их отсутствие в образе;
  • использовать bind mounts вместо копирования файлов;
  • читать вывод сборки в режиме --progress=plain;
  • объяснить, что такое build drivers и когда нужен не docker driver.

Предварительные знания

Ключевые термины

ТерминОбъяснение
BuildKitСовременный сборщик образов, по умолчанию с Docker Engine 23.0
frontendКомпонент, интерпретирующий Dockerfile; версия задаётся директивой # syntax
LLBLow-Level Build — промежуточное представление сборки в виде графа
cache mountКаталог, сохраняющийся между сборками, но не попадающий в образ
secret mountФайл, доступный только во время выполнения одной инструкции RUN
bind mount при сборкеДоступ к файлам контекста без копирования в слой
build driverСпособ запуска сборки: docker, docker-container, kubernetes, remote

Теория

Что изменил BuildKit

Legacy builder выполнял инструкции линейно, создавая промежуточный container для каждой. BuildKit преобразует Dockerfile в граф зависимостей (LLB) и выполняет его.

ВозможностьLegacy builderBuildKit
Параллельная сборка стадийнетда
Пропуск неиспользуемых стадийнетда
Cache mountsнетда
Secret mountsнетда
Bind mounts при сборкенетда
Heredocнетда
Экспорт кэша в registryнетда
Multi-platform без эмуляции на каждом шагенетда
Передача контекста по запросунетда

С Docker Engine 23.0 BuildKit используется по умолчанию, а legacy builder объявлен устаревшим.

Директива # syntax

Первая строка файла может задавать версию frontend:

dockerfile
# syntax=docker/dockerfile:1

Это не комментарий — BuildKit читает её и загружает указанный образ frontend из registry. Тег 1 означает «последняя стабильная версия ветки 1», и она обновляется без обновления самого Docker.

Практический смысл: возможности вроде COPY --parents или RUN --mount=type=secret,env=... появляются во frontend раньше, чем в Docker Engine. Директива даёт к ним доступ.

Правило: если вы используете любую возможность BuildKit, добавляйте директиву. Без неё сборка упадёт с синтаксической ошибкой на незнакомой конструкции.

Директива должна быть первой строкой файла — до комментариев и пустых строк.

Cache mounts

Проблема: менеджеры пакетов скачивают файлы в свой кэш, но кэш находится внутри слоя. При инвалидации слоя кэш теряется, и всё скачивается заново.

RUN --mount=type=cache монтирует каталог, который:

  • сохраняется между сборками;
  • не попадает в образ;
  • разделяется между сборками разных образов.
dockerfile
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

Обратите внимание на отсутствие --no-cache-dir: с cache mount кэш pip полезен, потому что переживает пересборку.

Для apt требуется дополнительная настройка, потому что образ по умолчанию удаляет кэш:

dockerfile
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean \
 && apt-get update \
 && apt-get install -y --no-install-recommends curl

Параметр sharing определяет поведение при параллельных сборках:

ЗначениеПоведение
shared (по умолчанию)Несколько сборок пишут одновременно
lockedВторая сборка ждёт освобождения
privateКаждая сборка получает свою копию

Для apt и других менеджеров, не рассчитанных на параллельный доступ, нужен locked.

Secret mounts

ARG и ENV непригодны для секретов (урок 5.2): значения остаются в истории образа.

RUN --mount=type=secret монтирует файл только на время выполнения инструкции:

dockerfile
RUN --mount=type=secret,id=pypi_token \
    pip install --index-url "https://$(cat /run/secrets/pypi_token)@pypi.example.com/simple" -r requirements.txt

Передача при сборке:

bash
docker build --secret id=pypi_token,src=./token.txt -t app .

Секрет доступен по пути /run/secrets/<id>, не попадает ни в слой, ни в историю, ни в конфигурацию.

Начиная с Dockerfile 1.10 доступна форма с переменной окружения:

dockerfile
RUN --mount=type=secret,id=token,env=API_TOKEN \
    curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data

И передача из окружения без файла:

bash
export API_TOKEN=xxx
docker build --secret id=token,env=API_TOKEN -t app .

Дополнительные параметры: required=true заставит сборку упасть, если секрет не передан; mode, uid, gid задают права на файл.

Bind mounts при сборке

RUN --mount=type=bind даёт доступ к файлам без копирования в слой:

dockerfile
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
    pip install -r /tmp/requirements.txt

Отличие от COPY: файл не остаётся в образе. Полезно, когда файл нужен только на время установки.

Можно монтировать и из другой стадии:

dockerfile
RUN --mount=type=bind,from=builder,source=/artifacts,target=/artifacts \
    cp /artifacts/app /usr/local/bin/

Прочие типы монтирований

ТипНазначение
type=tmpfsВременная файловая система в памяти для промежуточных данных
type=sshДоступ к SSH-агенту для клонирования приватных репозиториев

Пример с SSH:

dockerfile
RUN --mount=type=ssh \
    git clone git@github.com:company/private-lib.git /src
bash
docker build --ssh default -t app .

Ключ не попадает в образ — используется проброшенный сокет агента.

Build drivers

Драйвер определяет, где и как выполняется сборка.

ДрайверОписаниеКогда нужен
dockerВстроенный в Docker Engine, по умолчаниюОбычная локальная сборка
docker-containerBuildKit в отдельном containerMulti-platform, экспорт кэша в registry
kubernetesСборка в кластереCI на Kubernetes
remoteПодключение к внешнему BuildKitОбщий сборочный сервер

Ограничение драйвера docker: он не поддерживает экспорт кэша в registry (--cache-to type=registry) и multi-platform сборку в один образ. Для этого создаётся builder с драйвером docker-container:

bash
docker buildx create --name multi --driver docker-container --use

Подробно применение в CI — в разделе 16.


Внутренний механизм

Как выполняется граф

BuildKit преобразует Dockerfile в граф операций, где узлы — шаги, а рёбра — зависимости по данным. Затем:

  1. Определяется, какие узлы нужны для запрошенного результата.
  2. Ненужные узлы отбрасываются (например, стадия, не используемая финальным образом).
  3. Независимые узлы выполняются параллельно.
  4. Для каждого узла проверяется кэш.

Отсюда наблюдаемое поведение: в multi-stage сборке с тремя независимыми стадиями все три собираются одновременно, а стадия test, не входящая в финальный образ, не выполняется вовсе — если её не запросить через --target.

Где хранятся cache mounts

Данные cache mount лежат в состоянии BuildKit, отдельно от образов и от build cache слоёв. Они видны в общем объёме Build Cache команды docker system df и удаляются через docker builder prune.

Идентификатор кэша по умолчанию равен пути target. Явный id позволяет разделять или объединять кэши между разными Dockerfile:

dockerfile
RUN --mount=type=cache,id=pip-cache,target=/root/.cache/pip pip install ...

Команды и примеры

Подготовка

bash
mkdir -p /tmp/buildkit/app && cd /tmp/buildkit

cat > requirements.txt <<'EOF'
fastapi==0.141.1
uvicorn[standard]==0.52.0
pydantic==2.13.4
httpx==0.28.1
sqlalchemy==2.0.44
EOF

echo "print('приложение')" > app/main.py

cat > .dockerignore <<'EOF'
Dockerfile*
.dockerignore
__pycache__
EOF

Проверка, что BuildKit активен

bash
docker build --help | grep -q 'buildx' && echo "BuildKit активен" || echo "legacy builder"
docker buildx version
text
BuildKit активен
github.com/docker/buildx v0.34.1 ...

Cache mount для pip

bash
cat > Dockerfile.nocache <<'EOF'
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
CMD ["python", "app/main.py"]
EOF

cat > Dockerfile.cachemount <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
COPY app/ ./app/
CMD ["python", "app/main.py"]
EOF

echo "=== первая сборка обоих вариантов ==="
docker build -q -f Dockerfile.nocache    -t bk:nocache    . > /dev/null
docker build -q -f Dockerfile.cachemount -t bk:cachemount . > /dev/null
echo "готово"

Теперь изменим зависимости — это инвалидирует слой установки в обоих вариантах:

bash
echo "python-multipart==0.0.20" >> requirements.txt

echo "=== пересборка после добавления пакета ==="
echo -n "без cache mount: "
s="$(date +%s.%N)"
docker build -q -f Dockerfile.nocache -t bk:nocache . > /dev/null
e="$(date +%s.%N)"
awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f c\n", b-a}'

echo -n "с cache mount:   "
s="$(date +%s.%N)"
docker build -q -f Dockerfile.cachemount -t bk:cachemount . > /dev/null
e="$(date +%s.%N)"
awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f c\n", b-a}'
text
=== пересборка после добавления пакета ===
без cache mount: 24.83 c
с cache mount:   6.12 c

В обоих случаях слой пересобрался — но во втором пакеты не скачивались заново, они взяты из сохранённого кэша pip.

Важно: cache mount не попадает в образ. Проверим:

bash
docker run --rm bk:cachemount ls /root/.cache/pip 2>&1 | head -1
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep bk:
text
ls: cannot access '/root/.cache/pip': No such file or directory
bk:cachemount 412MB
bk:nocache 412MB

Размеры одинаковы: кэш живёт вне образа.

Cache mount для apt

bash
cat > Dockerfile.apt <<'EOF'
# syntax=docker/dockerfile:1
FROM debian:trixie-slim

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    --mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
    rm -f /etc/apt/apt.conf.d/docker-clean \
 && apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates

CMD ["curl", "--version"]
EOF

echo "=== первая сборка ==="
s="$(date +%s.%N)"; docker build -q -f Dockerfile.apt -t bk:apt1 . > /dev/null; e="$(date +%s.%N)"
awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f c\n", b-a}'

echo "=== вторая сборка с другим пакетом (кэш пакетов переиспользуется) ==="
sed -i 's/curl ca-certificates/curl ca-certificates jq/' Dockerfile.apt
s="$(date +%s.%N)"; docker build -q -f Dockerfile.apt -t bk:apt2 . > /dev/null; e="$(date +%s.%N)"
awk -v a="$s" -v b="$e" 'BEGIN{printf "%.2f c\n", b-a}'

docker run --rm bk:apt2 curl --version | head -1
text
=== первая сборка ===
14.28 c
=== вторая сборка с другим пакетом (кэш пакетов переиспользуется) ===
5.41 c
14.28 c
curl 8.15.0 ...

Строка rm -f /etc/apt/apt.conf.d/docker-clean обязательна: в официальных образах Debian этот файл заставляет apt удалять скачанные пакеты сразу после установки, что делает cache mount бесполезным.

Параметр sharing=locked защищает от повреждения кэша при параллельных сборках — apt не рассчитан на одновременную запись.

Secret mounts

Сначала покажем проблему с ARG:

bash
echo -n 'ghp_SuperSecretToken123456789' > token.txt

cat > Dockerfile.argsecret <<'EOF'
FROM alpine:3.21
ARG API_TOKEN
RUN echo "использую токен длиной ${#API_TOKEN}" > /result.txt
CMD ["cat", "/result.txt"]
EOF

docker build -q -f Dockerfile.argsecret \
    --build-arg API_TOKEN="$(cat token.txt)" -t bk:argsecret . > /dev/null

echo "--- поиск токена в истории образа ---"
docker history --no-trunc bk:argsecret | grep -o 'API_TOKEN=[^ ]*' | head -1
text
--- поиск токена в истории образа ---
API_TOKEN=ghp_SuperSecretToken123456789

Токен извлечён из образа. Теперь корректный вариант:

bash
cat > Dockerfile.secret <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=secret,id=api_token,required=true \
    echo "использую токен длиной $(wc -c < /run/secrets/api_token)" > /result.txt
CMD ["cat", "/result.txt"]
EOF

docker build -q -f Dockerfile.secret \
    --secret id=api_token,src=token.txt -t bk:secret . > /dev/null

docker run --rm bk:secret

echo "--- поиск токена в истории ---"
docker history --no-trunc bk:secret | grep -c 'SuperSecret' || echo "0 — не найден"

echo "--- поиск в слоях образа ---"
docker save bk:secret -o /tmp/bk-secret.tar
mkdir -p /tmp/bk-extract && tar -xf /tmp/bk-secret.tar -C /tmp/bk-extract
found=0
for b in /tmp/bk-extract/blobs/sha256/*; do
    if tar -tf "$b" 2>/dev/null | grep -q 'run/secrets'; then found=1; fi
done
[ "$found" -eq 0 ] && echo "0 — в слоях отсутствует"
rm -rf /tmp/bk-secret.tar /tmp/bk-extract
text
использую токен длиной 29
--- поиск токена в истории ---
0 — не найден
--- поиск в слоях образа ---
0 — в слоях отсутствует

Тот же результат сборки, но секрета нет ни в истории, ни в слоях.

Параметр required=true защищает от молчаливого сбоя:

bash
docker build -q -f Dockerfile.secret -t bk:secret . 2>&1 | tail -2
text
ERROR: failed to build: secret api_token: not found

Без required=true файл /run/secrets/api_token был бы пустым, и сборка прошла бы с неверным результатом.

Секрет как переменная окружения

bash
cat > Dockerfile.secretenv <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21
RUN --mount=type=secret,id=api_token,env=API_TOKEN,required=true \
    echo "длина токена из переменной: ${#API_TOKEN}" > /result.txt
CMD ["cat", "/result.txt"]
EOF

export API_TOKEN='ghp_FromEnvironment12345'
docker build -q -f Dockerfile.secretenv \
    --secret id=api_token,env=API_TOKEN -t bk:secretenv . > /dev/null
docker run --rm bk:secretenv

docker history --no-trunc bk:secretenv | grep -c 'FromEnvironment' || echo "в истории отсутствует"
unset API_TOKEN
text
длина токена из переменной: 24
в истории отсутствует

Форма удобна, когда приложение читает конфигурацию из переменных окружения — не нужно менять код на чтение файла.

Bind mount вместо COPY

bash
cat > Dockerfile.bind <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app

# requirements.txt нужен только на время установки — не копируем его в слой
RUN --mount=type=cache,target=/root/.cache/pip \
    --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
    pip install -r /tmp/requirements.txt

COPY app/ ./app/
CMD ["python", "app/main.py"]
EOF

docker build -q -f Dockerfile.bind -t bk:bind . > /dev/null

echo "--- есть ли requirements.txt в образе? ---"
docker run --rm bk:bind sh -c 'ls /app/requirements.txt 2>&1 || echo "отсутствует — не копировался"'

echo "--- работает ли приложение? ---"
docker run --rm bk:bind python -c "import fastapi; print('зависимости установлены')"
text
--- есть ли requirements.txt в образе? ---
ls: cannot access '/app/requirements.txt': No such file or directory
отсутствует — не копировался
--- работает ли приложение? ---
зависимости установлены

Файл использовался при сборке, но в образ не попал.

Обратите внимание на побочный эффект: инструкция RUN с bind mount не зависит от содержимого файла в cache key. Изменение requirements.txt не инвалидирует эту инструкцию автоматически. BuildKit учитывает содержимое смонтированных файлов, но поведение зависит от версии frontend — при сомнениях проверяйте на своей версии, что обновление зависимостей применяется.

Проверим на нашей версии:

bash
echo "rich==14.2.0" >> requirements.txt
docker build -f Dockerfile.bind -t bk:bind . 2>&1 | grep -E 'CACHED|RUN --mount' | head -2
docker run --rm bk:bind python -c "import rich; print('новый пакет установлен')" 2>/dev/null \
    || echo "новый пакет НЕ установлен — кэш не инвалидировался"
text
 => [3/4] RUN --mount=type=cache,target=/root/.cache/pip     --mount=type=bind,source=req...
новый пакет установлен

Кэш инвалидировался корректно — содержимое смонтированного файла учтено.

Параллельная сборка стадий

bash
cat > Dockerfile.parallel <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21 AS stage-a
RUN sleep 5 && echo "A готова" > /a.txt

FROM alpine:3.21 AS stage-b
RUN sleep 5 && echo "B готова" > /b.txt

FROM alpine:3.21 AS stage-c
RUN sleep 5 && echo "C готова" > /c.txt

FROM alpine:3.21
COPY --from=stage-a /a.txt /
COPY --from=stage-b /b.txt /
COPY --from=stage-c /c.txt /
CMD ["sh", "-c", "cat /a.txt /b.txt /c.txt"]
EOF

echo "три стадии по 5 секунд каждая:"
s="$(date +%s.%N)"
docker build -q --no-cache -f Dockerfile.parallel -t bk:parallel . > /dev/null
e="$(date +%s.%N)"
awk -v a="$s" -v b="$e" 'BEGIN{printf "общее время: %.1f c\n", b-a}'
docker run --rm bk:parallel
text
три стадии по 5 секунд каждая:
общее время: 7.3 c

Не 15 секунд, а около 7: стадии собирались параллельно. Legacy builder выполнил бы их последовательно.

Неиспользуемые стадии не собираются

bash
cat > Dockerfile.skip <<'EOF'
# syntax=docker/dockerfile:1
FROM alpine:3.21 AS used
RUN echo "нужная стадия" > /used.txt

FROM alpine:3.21 AS unused
RUN sleep 10 && echo "эта стадия не нужна" > /unused.txt

FROM alpine:3.21
COPY --from=used /used.txt /
CMD ["cat", "/used.txt"]
EOF

s="$(date +%s.%N)"
docker build -q --no-cache -f Dockerfile.skip -t bk:skip . > /dev/null
e="$(date +%s.%N)"
awk -v a="$s" -v b="$e" 'BEGIN{printf "время сборки: %.1f c\n", b-a}'
echo "(стадия unused со sleep 10 не выполнялась)"
text
время сборки: 2.4 c
(стадия unused со sleep 10 не выполнялась)

Стадия unused не участвует в результате, поэтому BuildKit её отбросил.

Собрать её явно можно через --target:

bash
docker build -q --target unused -f Dockerfile.skip -t bk:unused . > /dev/null && echo "собрана по запросу"

Подробный вывод сборки

bash
docker build --no-cache --progress=plain -f Dockerfile.bind -t bk:bind . 2>&1 \
    | grep -E '^#[0-9]+ (\[|DONE|CACHED)' | head -10

Режим plain показывает полный вывод команд, а не свёрнутый прогресс. Незаменим при отладке: видно, что именно выполнялось внутри RUN.

Ещё полезные режимы:

Значение --progressНазначение
auto (по умолчанию)Интерактивный прогресс в терминале
plainПолный текстовый вывод; нужен в CI и при отладке
ttyПринудительно интерактивный
quietТолько итоговый ID образа

Build drivers

bash
docker buildx ls
text
NAME/NODE     DRIVER/ENDPOINT  STATUS   BUILDKIT  PLATFORMS
default*      docker
 \_ default    \_ default      running  v0.26.1   linux/amd64, linux/386

Драйвер docker не умеет экспортировать кэш в registry:

bash
docker build --cache-to type=registry,ref=localhost:5000/cache -t test . 2>&1 | tail -2
text
ERROR: Cache export is not supported for the docker driver.
Switch to a different driver, or turn on the containerd image store.

Создание builder с драйвером docker-container:

bash
docker buildx create --name multiarch --driver docker-container --bootstrap > /dev/null 2>&1
docker buildx ls | head -5

Использование:

bash
docker buildx build --builder multiarch -f Dockerfile.bind -t bk:multi --load . > /dev/null 2>&1 \
    && echo "собрано через docker-container driver"

Флаг --load обязателен: builder с драйвером docker-container собирает вне Docker Engine, и результат нужно явно загрузить.

Удаление:

bash
docker buildx rm multiarch > /dev/null 2>&1

Уборка

bash
cd /tmp
docker rmi -f $(docker images -q --filter 'reference=bk:*') 2>/dev/null || true
docker builder prune -f > /dev/null
rm -rf /tmp/buildkit

Практическое упражнение

Задание. Переведите Dockerfile Python-проекта на возможности BuildKit и измерьте эффект каждого изменения.

Исходный вариант:

dockerfile
FROM python:3.13-slim
WORKDIR /app

ARG PYPI_TOKEN
RUN pip config set global.index-url "https://${PYPI_TOKEN}@pypi.example.com/simple"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .
CMD ["python", "app/main.py"]

Требуется:

  1. Убрать секрет из образа через secret mount и доказать его отсутствие.
  2. Добавить cache mount для pip и измерить ускорение при изменении зависимостей.
  3. Заменить COPY requirements.txt на bind mount, чтобы файл не попадал в образ.
  4. Проверить, что после всех изменений обновление зависимостей всё ещё применяется.

Подсказки

Подсказка 1

Директива # syntax=docker/dockerfile:1 обязательна — без неё --mount не распознается.

Подсказка 2

Несколько монтирований в одной инструкции указываются подряд:

dockerfile
RUN --mount=type=cache,target=/root/.cache/pip \
    --mount=type=secret,id=token \
    команда
Подсказка 3

Проверка отсутствия секрета: docker history --no-trunc плюс поиск в слоях через docker save и tar.

Решение

Сначала выполните задание самостоятельно.

Показать решение
bash
#!/usr/bin/env bash
# buildkit-migrate.sh — перевод Dockerfile на возможности BuildKit.
set -uo pipefail

WORK="$(mktemp -d)"
trap 'docker rmi -f $(docker images -q --filter "reference=mig:*") >/dev/null 2>&1 || true;
      rm -rf "$WORK"' EXIT
cd "$WORK"

mkdir -p app
cat > requirements.txt <<'EOF'
fastapi==0.141.1
uvicorn[standard]==0.52.0
pydantic==2.13.4
httpx==0.28.1
EOF
echo "print('приложение')" > app/main.py
echo -n 'pypi_SuperSecret_9876543210' > token.txt
cat > .dockerignore <<'EOF'
Dockerfile*
.dockerignore
token.txt
__pycache__
EOF

# ── v0: исходный, секрет через ARG ──
cat > Dockerfile.v0 <<'EOF'
FROM python:3.13-slim
WORKDIR /app
ARG PYPI_TOKEN
RUN echo "настройка индекса с токеном длиной ${#PYPI_TOKEN}" > /setup.log
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app/ ./app/
CMD ["python", "app/main.py"]
EOF

# ── v1: secret mount + cache mount + bind mount ──
cat > Dockerfile.v1 <<'EOF'
# syntax=docker/dockerfile:1
FROM python:3.13-slim
WORKDIR /app

RUN --mount=type=secret,id=pypi_token,required=true \
    echo "настройка индекса с токеном длиной $(wc -c < /run/secrets/pypi_token)" > /setup.log

RUN --mount=type=cache,target=/root/.cache/pip \
    --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
    pip install -r /tmp/requirements.txt

COPY app/ ./app/
CMD ["python", "app/main.py"]
EOF

echo "═══ 1. Секрет в образе ═══"
docker build -q -f Dockerfile.v0 --build-arg PYPI_TOKEN="$(cat token.txt)" -t mig:v0 . > /dev/null
docker build -q -f Dockerfile.v1 --secret id=pypi_token,src=token.txt -t mig:v1 . > /dev/null

check_secret() {
    local tag="$1"
    local in_hist in_layers=0
    in_hist="$(docker history --no-trunc "$tag" 2>/dev/null | grep -c 'SuperSecret' || true)"
    docker save "$tag" -o "$WORK/img.tar" 2>/dev/null
    mkdir -p "$WORK/ex" && tar -xf "$WORK/img.tar" -C "$WORK/ex" 2>/dev/null
    if grep -rqa 'SuperSecret' "$WORK/ex" 2>/dev/null; then in_layers=1; fi
    rm -rf "$WORK/ex" "$WORK/img.tar"
    printf '  %-8s в истории: %s   в слоях: %s\n' "$tag" \
        "$([ "${in_hist:-0}" -gt 0 ] && echo 'НАЙДЕН' || echo 'нет')" \
        "$([ "$in_layers" -gt 0 ] && echo 'НАЙДЕН' || echo 'нет')"
}
check_secret mig:v0
check_secret mig:v1

echo
echo "═══ 2. Скорость при изменении зависимостей ═══"
echo "rich==14.2.0" >> requirements.txt

for v in v0 v1; do
    args=(-f "Dockerfile.$v" -t "mig:$v")
    [ "$v" = v0 ] && args+=(--build-arg "PYPI_TOKEN=$(cat token.txt)") \
                  || args+=(--secret "id=pypi_token,src=token.txt")
    s="$(date +%s.%N)"
    docker build -q "${args[@]}" . > /dev/null
    e="$(date +%s.%N)"
    printf '  %-4s %6.2f c\n' "$v" "$(awk -v a="$s" -v b="$e" 'BEGIN{print b-a}')"
done

echo
echo "═══ 3. requirements.txt в образе ═══"
for v in v0 v1; do
    printf '  %-4s %s\n' "$v" \
        "$(docker run --rm "mig:$v" sh -c 'ls /app/requirements.txt >/dev/null 2>&1 && echo "присутствует" || echo "отсутствует"')"
done

echo
echo "═══ 4. Обновление зависимостей применяется ═══"
docker run --rm mig:v1 python -c "import rich; print('  новый пакет установлен')" 2>/dev/null \
    || echo "  ОШИБКА: новый пакет не установлен"

Ожидаемый вывод:

text
═══ 1. Секрет в образе ═══
  mig:v0   в истории: НАЙДЕН   в слоях: нет
  mig:v1   в истории: нет      в слоях: нет

═══ 2. Скорость при изменении зависимостей ═══
  v0    23.71 c
  v1     5.94 c

═══ 3. requirements.txt в образе ═══
  v0    присутствует
  v1    отсутствует

═══ 4. Обновление зависимостей применяется ═══
  новый пакет установлен

Разбор результатов.

Пункт 1. В варианте v0 токен найден в истории — это то, что делает ARG непригодным для секретов. В v1 он отсутствует и там, и в слоях: secret mount существует только на время выполнения инструкции.

Обратите внимание, что в слоях v0 токена нет — он попал именно в метаданные, а не в файловую систему. Поэтому поиск только по слоям утечку не обнаружил бы; проверять нужно оба места.

Пункт 2. Ускорение в 4 раза даёт cache mount: слой установки пересобрался в обоих случаях, но во втором пакеты не скачивались заново из сети.

Пункт 3. Bind mount позволил использовать файл, не копируя его. В production-образе не остаётся ничего лишнего.

Пункт 4. Обязательная проверка. Оптимизация, при которой обновление зависимостей перестаёт применяться, — скрытая поломка, которая обнаружится в самый неудобный момент.

Что даёт required=true. Без него отсутствующий секрет дал бы пустой файл, и сборка прошла бы с неверной конфигурацией. Проверить:

bash
docker build -q -f Dockerfile.v1 -t mig:v1 . 2>&1 | tail -1
text
ERROR: failed to build: secret pypi_token: not found

Сборка падает явно — это лучше, чем молча собранный неработающий образ.

Проверка результата

bash
mkdir -p /tmp/vbk && cd /tmp/vbk
echo -n 'secret-value-12345' > s.txt
printf '# syntax=docker/dockerfile:1\nFROM alpine:3.21\nRUN --mount=type=secret,id=s,required=true wc -c < /run/secrets/s > /len.txt\nCMD ["cat","/len.txt"]\n' > Dockerfile
docker build -q --secret id=s,src=s.txt -t vbk:1 . > /dev/null
docker run --rm vbk:1
docker history --no-trunc vbk:1 | grep -c 'secret-value' || echo "секрет не найден в истории"
docker rmi -f vbk:1 > /dev/null; cd /tmp && rm -rf /tmp/vbk

Ожидается вывод длины секрета и подтверждение его отсутствия в истории.

Типичные ошибки

ОшибкаПричинаИсправление
--mount без директивы # syntaxНе знают о требованииСборка падает на незнакомой конструкции; добавить первой строкой
Директива не в первой строкеСтавят после комментарияДолжна быть самой первой строкой файла
Секрет через --build-argПривычноОстаётся в истории образа; использовать secret mount
Нет required=true у секретаНе знают о параметреОтсутствующий секрет даёт пустой файл и молчаливо неверную сборку
Cache mount для apt без удаления docker-cleanНе очевидноКэш очищается сразу; удалить /etc/apt/apt.conf.d/docker-clean
sharing=shared для aptЗначение по умолчаниюПовреждение кэша при параллельных сборках; использовать locked
--no-cache-dir вместе с cache mountКопируют из старых примеровВзаимоисключают друг друга; убрать флаг
Ожидание, что cache mount попадёт в образЛогично по названиюКэш живёт вне образа — это его смысл
--cache-to type=registry с драйвером dockerНе знают об ограниченииСоздать builder с docker-container
Забыт --load при сборке через docker-containerОбраз «не появился»Драйвер собирает вне Engine; нужен явный --load

Контрольные вопросы

На понимание:

  1. Зачем нужна директива # syntax и почему она должна быть первой строкой?
  2. Чем cache mount отличается от слоя образа?
  3. Почему secret mount безопаснее ARG, если оба не оставляют файл в слоях?
  4. Почему для apt нужен sharing=locked?
  5. Почему стадия, не используемая финальным образом, не собирается?

На применение:

  1. Как ускорить установку зависимостей при их частом изменении?
  2. Как передать токен в сборку так, чтобы его нельзя было извлечь из образа?
  3. Как использовать файл при сборке, не оставляя его в образе?

На диагностику:

  1. Сборка падает с ошибкой синтаксиса на строке RUN --mount=type=cache,.... Причина?
  2. Cache mount для apt настроен, но пакеты скачиваются заново при каждой сборке. Что проверить?

Краткое резюме

  1. BuildKit — сборщик по умолчанию с Docker Engine 23.0; legacy builder устарел.
  2. Директива # syntax=docker/dockerfile:1 даёт доступ к возможностям frontend и должна быть первой строкой.
  3. Cache mount сохраняет кэш менеджера пакетов между сборками и не попадает в образ.
  4. Для apt нужно удалить docker-clean и указать sharing=locked.
  5. Secret mount делает файл доступным только на время инструкции; в историю и слои он не попадает.
  6. Параметр required=true превращает отсутствие секрета в явную ошибку.
  7. Bind mount даёт доступ к файлам контекста без копирования в слой.
  8. BuildKit собирает независимые стадии параллельно и пропускает неиспользуемые.
  9. --progress=plain показывает полный вывод команд — основной режим для отладки и CI.
  10. Драйвер docker не поддерживает экспорт кэша в registry; для этого нужен docker-container.

Официальные источники

ИсточникСсылкаЧто подтверждает
BuildKithttps://docs.docker.com/build/buildkit/Архитектура, LLB, отличия от legacy builder
Dockerfile syntax directivehttps://docs.docker.com/build/buildkit/frontend/Назначение # syntax, требование первой строки
Dockerfile reference: RUN --mounthttps://docs.docker.com/reference/dockerfile/#run---mountТипы cache, secret, bind, tmpfs, ssh; параметр sharing
Build secretshttps://docs.docker.com/build/building/secrets/Secret mounts, формы src и env, required=true
Cache mountshttps://docs.docker.com/build/cache/optimize/#use-cache-mountsНастройка cache mount, пример с apt и удалением docker-clean
Build drivershttps://docs.docker.com/build/builders/drivers/Драйверы docker, docker-container, kubernetes, remote и их ограничения
docker buildx buildhttps://docs.docker.com/reference/cli/docker/buildx/build/Флаги --secret, --ssh, --progress, --target, --load, --cache-to
Multi-stage buildshttps://docs.docker.com/build/building/multi-stage/Параллельная сборка стадий, пропуск неиспользуемых

Навигация

← Предыдущий материал
Вернуться к разделу
Следующий материал → Multi-stage builds
Главное оглавление

Markdown на GitHub ↗