5.6. BuildKit
Цели
После этого материала вы сможете:
- объяснить, чем BuildKit отличается от legacy builder и что это даёт на практике;
- использовать директиву
# syntaxи понимать, зачем она нужна; - применять cache mounts для менеджеров пакетов и измерять эффект;
- передавать секреты через secret mounts вместо
ARGи доказывать их отсутствие в образе; - использовать bind mounts вместо копирования файлов;
- читать вывод сборки в режиме
--progress=plain; - объяснить, что такое build drivers и когда нужен не
dockerdriver.
Предварительные знания
- 5.3. COPY, ADD и RUN;
- 5.5. Build cache;
- 5.2. Базовые инструкции — почему
ARGнепригоден для секретов.
Ключевые термины
| Термин | Объяснение |
|---|---|
BuildKit | Современный сборщик образов, по умолчанию с Docker Engine 23.0 |
frontend | Компонент, интерпретирующий Dockerfile; версия задаётся директивой # syntax |
LLB | Low-Level Build — промежуточное представление сборки в виде графа |
cache mount | Каталог, сохраняющийся между сборками, но не попадающий в образ |
secret mount | Файл, доступный только во время выполнения одной инструкции RUN |
bind mount при сборке | Доступ к файлам контекста без копирования в слой |
build driver | Способ запуска сборки: docker, docker-container, kubernetes, remote |
Теория
Что изменил BuildKit
Legacy builder выполнял инструкции линейно, создавая промежуточный container для каждой. BuildKit преобразует Dockerfile в граф зависимостей (LLB) и выполняет его.
| Возможность | Legacy builder | BuildKit |
|---|---|---|
| Параллельная сборка стадий | нет | да |
| Пропуск неиспользуемых стадий | нет | да |
| Cache mounts | нет | да |
| Secret mounts | нет | да |
| Bind mounts при сборке | нет | да |
| Heredoc | нет | да |
| Экспорт кэша в registry | нет | да |
| Multi-platform без эмуляции на каждом шаге | нет | да |
| Передача контекста по запросу | нет | да |
С Docker Engine 23.0 BuildKit используется по умолчанию, а legacy builder объявлен устаревшим.
Директива # syntax
Первая строка файла может задавать версию frontend:
# 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 монтирует каталог, который:
- сохраняется между сборками;
- не попадает в образ;
- разделяется между сборками разных образов.
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
Обратите внимание на отсутствие --no-cache-dir: с cache mount кэш pip полезен, потому что переживает пересборку.
Для apt требуется дополнительная настройка, потому что образ по умолчанию удаляет кэш:
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 монтирует файл только на время выполнения инструкции:
RUN --mount=type=secret,id=pypi_token \
pip install --index-url "https://$(cat /run/secrets/pypi_token)@pypi.example.com/simple" -r requirements.txt
Передача при сборке:
docker build --secret id=pypi_token,src=./token.txt -t app .
Секрет доступен по пути /run/secrets/<id>, не попадает ни в слой, ни в историю, ни в конфигурацию.
Начиная с Dockerfile 1.10 доступна форма с переменной окружения:
RUN --mount=type=secret,id=token,env=API_TOKEN \
curl -H "Authorization: Bearer $API_TOKEN" https://api.example.com/data
И передача из окружения без файла:
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 даёт доступ к файлам без копирования в слой:
RUN --mount=type=bind,source=requirements.txt,target=/tmp/requirements.txt \
pip install -r /tmp/requirements.txt
Отличие от COPY: файл не остаётся в образе. Полезно, когда файл нужен только на время установки.
Можно монтировать и из другой стадии:
RUN --mount=type=bind,from=builder,source=/artifacts,target=/artifacts \
cp /artifacts/app /usr/local/bin/
Прочие типы монтирований
| Тип | Назначение |
|---|---|
type=tmpfs | Временная файловая система в памяти для промежуточных данных |
type=ssh | Доступ к SSH-агенту для клонирования приватных репозиториев |
Пример с SSH:
RUN --mount=type=ssh \
git clone git@github.com:company/private-lib.git /src
docker build --ssh default -t app .
Ключ не попадает в образ — используется проброшенный сокет агента.
Build drivers
Драйвер определяет, где и как выполняется сборка.
| Драйвер | Описание | Когда нужен |
|---|---|---|
docker | Встроенный в Docker Engine, по умолчанию | Обычная локальная сборка |
docker-container | BuildKit в отдельном container | Multi-platform, экспорт кэша в registry |
kubernetes | Сборка в кластере | CI на Kubernetes |
remote | Подключение к внешнему BuildKit | Общий сборочный сервер |
Ограничение драйвера docker: он не поддерживает экспорт кэша в registry (--cache-to type=registry) и multi-platform сборку в один образ. Для этого создаётся builder с драйвером docker-container:
docker buildx create --name multi --driver docker-container --use
Подробно применение в CI — в разделе 16.
Внутренний механизм
Как выполняется граф
BuildKit преобразует Dockerfile в граф операций, где узлы — шаги, а рёбра — зависимости по данным. Затем:
- Определяется, какие узлы нужны для запрошенного результата.
- Ненужные узлы отбрасываются (например, стадия, не используемая финальным образом).
- Независимые узлы выполняются параллельно.
- Для каждого узла проверяется кэш.
Отсюда наблюдаемое поведение: в multi-stage сборке с тремя независимыми стадиями все три собираются одновременно, а стадия test, не входящая в финальный образ, не выполняется вовсе — если её не запросить через --target.
Где хранятся cache mounts
Данные cache mount лежат в состоянии BuildKit, отдельно от образов и от build cache слоёв. Они видны в общем объёме Build Cache команды docker system df и удаляются через docker builder prune.
Идентификатор кэша по умолчанию равен пути target. Явный id позволяет разделять или объединять кэши между разными Dockerfile:
RUN --mount=type=cache,id=pip-cache,target=/root/.cache/pip pip install ...
Команды и примеры
Подготовка
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 активен
docker build --help | grep -q 'buildx' && echo "BuildKit активен" || echo "legacy builder"
docker buildx version
BuildKit активен
github.com/docker/buildx v0.34.1 ...
Cache mount для pip
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 "готово"
Теперь изменим зависимости — это инвалидирует слой установки в обоих вариантах:
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}'
=== пересборка после добавления пакета ===
без cache mount: 24.83 c
с cache mount: 6.12 c
В обоих случаях слой пересобрался — но во втором пакеты не скачивались заново, они взяты из сохранённого кэша pip.
Важно: cache mount не попадает в образ. Проверим:
docker run --rm bk:cachemount ls /root/.cache/pip 2>&1 | head -1
docker images --format '{{.Repository}}:{{.Tag}} {{.Size}}' | grep bk:
ls: cannot access '/root/.cache/pip': No such file or directory
bk:cachemount 412MB
bk:nocache 412MB
Размеры одинаковы: кэш живёт вне образа.
Cache mount для apt
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
=== первая сборка ===
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:
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
--- поиск токена в истории образа ---
API_TOKEN=ghp_SuperSecretToken123456789
Токен извлечён из образа. Теперь корректный вариант:
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
использую токен длиной 29
--- поиск токена в истории ---
0 — не найден
--- поиск в слоях образа ---
0 — в слоях отсутствует
Тот же результат сборки, но секрета нет ни в истории, ни в слоях.
Параметр required=true защищает от молчаливого сбоя:
docker build -q -f Dockerfile.secret -t bk:secret . 2>&1 | tail -2
ERROR: failed to build: secret api_token: not found
Без required=true файл /run/secrets/api_token был бы пустым, и сборка прошла бы с неверным результатом.
Секрет как переменная окружения
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
длина токена из переменной: 24
в истории отсутствует
Форма удобна, когда приложение читает конфигурацию из переменных окружения — не нужно менять код на чтение файла.
Bind mount вместо COPY
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('зависимости установлены')"
--- есть ли requirements.txt в образе? ---
ls: cannot access '/app/requirements.txt': No such file or directory
отсутствует — не копировался
--- работает ли приложение? ---
зависимости установлены
Файл использовался при сборке, но в образ не попал.
Обратите внимание на побочный эффект: инструкция
RUNс bind mount не зависит от содержимого файла в cache key. Изменениеrequirements.txtне инвалидирует эту инструкцию автоматически. BuildKit учитывает содержимое смонтированных файлов, но поведение зависит от версии frontend — при сомнениях проверяйте на своей версии, что обновление зависимостей применяется.
Проверим на нашей версии:
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 "новый пакет НЕ установлен — кэш не инвалидировался"
=> [3/4] RUN --mount=type=cache,target=/root/.cache/pip --mount=type=bind,source=req...
новый пакет установлен
Кэш инвалидировался корректно — содержимое смонтированного файла учтено.
Параллельная сборка стадий
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
три стадии по 5 секунд каждая:
общее время: 7.3 c
Не 15 секунд, а около 7: стадии собирались параллельно. Legacy builder выполнил бы их последовательно.
Неиспользуемые стадии не собираются
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 не выполнялась)"
время сборки: 2.4 c
(стадия unused со sleep 10 не выполнялась)
Стадия unused не участвует в результате, поэтому BuildKit её отбросил.
Собрать её явно можно через --target:
docker build -q --target unused -f Dockerfile.skip -t bk:unused . > /dev/null && echo "собрана по запросу"
Подробный вывод сборки
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
docker buildx ls
NAME/NODE DRIVER/ENDPOINT STATUS BUILDKIT PLATFORMS
default* docker
\_ default \_ default running v0.26.1 linux/amd64, linux/386
Драйвер docker не умеет экспортировать кэш в registry:
docker build --cache-to type=registry,ref=localhost:5000/cache -t test . 2>&1 | tail -2
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:
docker buildx create --name multiarch --driver docker-container --bootstrap > /dev/null 2>&1
docker buildx ls | head -5
Использование:
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, и результат нужно явно загрузить.
Удаление:
docker buildx rm multiarch > /dev/null 2>&1
Уборка
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 и измерьте эффект каждого изменения.
Исходный вариант:
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"]
Требуется:
- Убрать секрет из образа через secret mount и доказать его отсутствие.
- Добавить cache mount для
pipи измерить ускорение при изменении зависимостей. - Заменить
COPY requirements.txtна bind mount, чтобы файл не попадал в образ. - Проверить, что после всех изменений обновление зависимостей всё ещё применяется.
Подсказки
Подсказка 1
Директива # syntax=docker/dockerfile:1 обязательна — без неё --mount не распознается.
Подсказка 2
Несколько монтирований в одной инструкции указываются подряд:
RUN --mount=type=cache,target=/root/.cache/pip \
--mount=type=secret,id=token \
команда
Подсказка 3
Проверка отсутствия секрета: docker history --no-trunc плюс поиск в слоях через docker save и tar.
Решение
Сначала выполните задание самостоятельно.
Показать решение
#!/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 " ОШИБКА: новый пакет не установлен"
Ожидаемый вывод:
═══ 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. Без него отсутствующий секрет дал бы пустой файл, и сборка прошла бы с неверной конфигурацией. Проверить:
docker build -q -f Dockerfile.v1 -t mig:v1 . 2>&1 | tail -1
ERROR: failed to build: secret pypi_token: not found
Сборка падает явно — это лучше, чем молча собранный неработающий образ.
Проверка результата
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 |
Контрольные вопросы
На понимание:
- Зачем нужна директива
# syntaxи почему она должна быть первой строкой? - Чем cache mount отличается от слоя образа?
- Почему secret mount безопаснее
ARG, если оба не оставляют файл в слоях? - Почему для apt нужен
sharing=locked? - Почему стадия, не используемая финальным образом, не собирается?
На применение:
- Как ускорить установку зависимостей при их частом изменении?
- Как передать токен в сборку так, чтобы его нельзя было извлечь из образа?
- Как использовать файл при сборке, не оставляя его в образе?
На диагностику:
- Сборка падает с ошибкой синтаксиса на строке
RUN --mount=type=cache,.... Причина? - Cache mount для apt настроен, но пакеты скачиваются заново при каждой сборке. Что проверить?
Краткое резюме
- BuildKit — сборщик по умолчанию с Docker Engine 23.0; legacy builder устарел.
- Директива
# syntax=docker/dockerfile:1даёт доступ к возможностям frontend и должна быть первой строкой. - Cache mount сохраняет кэш менеджера пакетов между сборками и не попадает в образ.
- Для apt нужно удалить
docker-cleanи указатьsharing=locked. - Secret mount делает файл доступным только на время инструкции; в историю и слои он не попадает.
- Параметр
required=trueпревращает отсутствие секрета в явную ошибку. - Bind mount даёт доступ к файлам контекста без копирования в слой.
- BuildKit собирает независимые стадии параллельно и пропускает неиспользуемые.
--progress=plainпоказывает полный вывод команд — основной режим для отладки и CI.- Драйвер
dockerне поддерживает экспорт кэша в registry; для этого нуженdocker-container.
Официальные источники
| Источник | Ссылка | Что подтверждает |
|---|---|---|
| BuildKit | https://docs.docker.com/build/buildkit/ | Архитектура, LLB, отличия от legacy builder |
| Dockerfile syntax directive | https://docs.docker.com/build/buildkit/frontend/ | Назначение # syntax, требование первой строки |
| Dockerfile reference: RUN --mount | https://docs.docker.com/reference/dockerfile/#run---mount | Типы cache, secret, bind, tmpfs, ssh; параметр sharing |
| Build secrets | https://docs.docker.com/build/building/secrets/ | Secret mounts, формы src и env, required=true |
| Cache mounts | https://docs.docker.com/build/cache/optimize/#use-cache-mounts | Настройка cache mount, пример с apt и удалением docker-clean |
| Build drivers | https://docs.docker.com/build/builders/drivers/ | Драйверы docker, docker-container, kubernetes, remote и их ограничения |
| docker buildx build | https://docs.docker.com/reference/cli/docker/buildx/build/ | Флаги --secret, --ssh, --progress, --target, --load, --cache-to |
| Multi-stage builds | https://docs.docker.com/build/building/multi-stage/ | Параллельная сборка стадий, пропуск неиспользуемых |
Навигация
← Предыдущий материал
Вернуться к разделу
Следующий материал → Multi-stage builds
Главное оглавление