Кеш-маунты и секреты сборки: --mount=type=cache держит загрузки тёплыми, --mount=type=secret держит токены вне слоёв
RUN --mount=type=cache сохраняет кеш пакетов между сборками даже на слоях-промахах, делая холодную загрузку npm/go/pip тёплой. RUN --mount=type=secret отдаёт креденшл одной команде, не записывая его в слой — лекарство от токена, который ARG и ENV запекают в историю образа.
Алерт на ротацию пришёл от самого GitHub: бот сканирования секретов нашёл живой npm automation-токен в публичном образе на Docker Hub. Первая реакция команды — недоверие: токен передавался как build-arg, --build-arg NPM_TOKEN=..., а Dockerfile удалял .npmrc сразу после npm ci. Они были уверены, что его нет. Потом кто-то запустил docker history --no-trunc на опубликованном образе — и вот он, открытым текстом, в метаданных слоя ARG NPM_TOKEN, и второй раз — в строке команды RUN echo "//registry..._authToken=${NPM_TOKEN}" > .npmrc. Удаление файла позже не изменило ничего: каждый слой неизменяем и поставляется, так что rm лишь добавил слой, прячущий файл, всё ещё присутствующий байт в байт в слое ниже. Токен был запечён в историю образа каждой сборки три месяца. Лекарством был не лучший rm. Им был RUN --mount=type=secret, который отдаёт креденшл ровно одной команде и не пишет его ни в один слой вообще.
Кеш-маунты: держим кеш пакетов тёплым между сборками
Вот замкнутый круг, на который натыкается каждый: ты переносишь package-lock.json до COPY . ., оптимизируешь порядок — но lockfile всё равно меняется при каждом обновлении зависимостей, и снова три минуты npm ci. Хороший порядок минимизирует перезапуски, но не исключает их. Кеширование слоёв — всё-или-ничего на вершину: измени байт, от которого зависит RUN npm ci, и весь шаг перезапускается холодно, перекачивая каждую зависимость. Но у пакетного менеджера уже есть свой внутренний кеш (npm ~/.npm, Go /go/pkg/mod, pip ~/.cache/pip, apt /var/cache/apt) — и обычно этот кеш живёт внутри слоя, так что промах слоя выбрасывает и его. RUN --mount=type=cache разрывает эту связь: он монтирует постоянную, управляемую BuildKit директорию в сборку, которая переживает сборки независимо от кеширования слоёв.
# syntax=docker/dockerfile:1
FROM node:22 AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ciТеперь даже когда package-lock.json меняется и слой npm ci промахивается, кеш-маунт ~/.npm всё ещё наполнен из прошлых сборок, так что npm переиспользует уже скачанные tarball-ы и тянет лишь то, что действительно новое. На большом монорепо это превращает ~3-минутный холодный npm ci в ~25-секундный тёплый; go mod download Go-сервиса поверх кеш-маунта /go/pkg/mod падает с минут до секунд. Кеш-маунт не входит ни в один слой — он никогда не шлётся — а BuildKit сериализует конкурентный доступ режимом sharing (по умолчанию shared; используйте locked для инструментов, не терпящих конкурентных писателей, вроде некоторых баз пакетов).
Шаг RUN npm ci использует --mount=type=cache,target=/root/.npm. Разработчик меняет одну строку в package-lock.json и пересобирает, так что слой npm ci — промах. Что делает кеш-маунт для этой пересборки?
▸Почему это работает
Чем кеш-маунт отличается от просто хорошего порядка инструкций? Порядок (manifest до исходников) держит слой попаданием, пока его входы неизменны — но в момент, когда lockfile законно меняется, этот слой обязан перезапуститься, и без кеш-маунта он перезапускается полностью холодно. Кеш-маунт решает ортогональную проблему: делает неизбежные перезапуски дешёвыми. Эти двое композируются. Хороший порядок минимизирует, как часто установка перезапускается; кеш-маунт минимизирует, как дорог каждый перезапуск. Пропусти маунт — и каждый апдейт зависимостей платит полный налог холодной загрузки; пропусти порядок — и перезапускаешь куда чаще нужного. Senior-Dockerfile-ы делают оба, поэтому их CI быстр даже на churn зависимостей.
Секреты сборки: креденшл, не касающийся ни одного слоя
Отказ из Hook структурный, а не ошибка забытого rm. Значения ARG и ENV записываются в конфиг образа и docker history; любая строка команды, интерполирующая секрет, записывается дословно; а каждый слой неизменяем, так что поздний rm не может удалить байты из раннего слоя — он лишь складывает whiteout сверху, пока данные лежат читаемыми в слое ниже, поставляемые любому, кто спулит. RUN --mount=type=secret решает это в корне: BuildKit монтирует секрет как файл (по умолчанию /run/secrets/<id>) только на время этого одного RUN, и он никогда не пишется в слой, конфиг или историю.
# syntax=docker/dockerfile:1
FROM node:22
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npmtoken \
NPM_TOKEN=$(cat /run/secrets/npmtoken) \
npm ci
# сборка: docker build --secret id=npmtoken,env=NPM_TOKEN .Токен читается из смонтированного файла внутри команды, используется npm ci и исчезает в конце шага. docker history показывает RUN с флагом --mount и без токена. Опубликованный образ не несёт ничего. Значение вы подаёте на этапе сборки из env-переменной (--secret id=npmtoken,env=NPM_TOKEN) или файла (--secret id=npmtoken,src=./token) — никогда как build-arg. Тот же механизм доставляет SSH-агенты (--mount=type=ssh) для приватных git-зависимостей без запекания ключа. Правило, предотвращающее весь класс инцидента: креденшл, нужный лишь на этапе сборки, проходит через секрет-маунт; он никогда не появляется в ARG, ENV или строке команды.
Dockerfile делает ARG TOKEN, затем RUN echo "token=$TOKEN" > .npmrc && npm ci && rm .npmrc. Финальный образ удаляет .npmrc. Восстановим ли токен из опубликованного образа?
- 01Объясни, что делает RUN --mount=type=cache и почему он помогает даже когда слой — промах.
- 02Почему ARG, ENV и rm не удерживают секрет сборки вне образа, и как --mount=type=secret это чинит?
Два маунта BuildKit решают две повторяющиеся проблемы сборки, которые порядок в одиночку не может. RUN —mount=type=cache монтирует постоянную, управляемую BuildKit директорию — npm ~/.npm, Go /go/pkg/mod, кеш pip или apt, — которая живёт вне слоёв образа и переживает сборки независимо от кеширования слоёв. Кеширование слоёв — всё-или-ничего на шаг, так что единственная правка lockfile форсит перезапуск RUN npm ci; обычно это выбрасывает собственный кеш загрузок пакетного менеджера, и установка перезапускается холодно, но кеш-маунт держит этот кеш целым между сборками, так что перезапуск переиспользует прошлые tarball-ы и тянет лишь изменившееся — ~3-минутная холодная установка становится ~25 секунд, go mod download Go падает с минут до секунд, и кеш никогда не шлётся в слое. Он композируется с хорошим порядком: порядок минимизирует, как часто установка перезапускается, кеш-маунт минимизирует, как дорог каждый перезапуск. RUN —mount=type=secret адресует структурную утечку, а не небрежный rm. ARG и ENV записываются в конфиг образа и docker history, интерполированные строки команд записываются дословно, а поскольку слои неизменяемы и все шлются, поздний rm лишь складывает whiteout, пока байты секрета остаются читаемыми в слое ниже — так что удалённый .npmrc всё равно утекает токен из опубликованного образа, тремя путями. Секрет-маунт отдаёт креденшл ровно одному RUN как файл под /run/secrets, использованный и исчезнувший в конце шага, записанный ни в один слой, конфиг или history, со значением, поданным при сборке через —secret id=…,env=… или src=…, а не build-arg. Теперь, когда встречаешь build-arg с токеном — или трёхминутную установку при каждом обновлении зависимостей — знай, какой маунт нужен: —mount=type=secret держит креды вне слоёв, —mount=type=cache держит перезапуски быстрыми.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.