Multi-Stage Dockerfile: где образ раздувается и тормозит выкатку
Типовая сборка идёт в одном этапе: базовый образ, установка компилятора, пакетного менеджера, заголовочных файлов и утилит для нативных модулей, копирование исходников и кэша зависимостей. Всё это остаётся в финальном слое, даже если в конце Dockerfile стоит очистка. Удаление файла в следующем слое размер образа не уменьшает: слой неизменяем, а rm лишь дописывает метку поверх, тогда как сами байты продолжают лежать в родительском слое.
Отсюда два следствия. Размер образа вырастает в разы относительно рантайма: компиляторы, отладочные символы, тестовые библиотеки и сервисные утилиты — это лишние зависимости в рантайме, которые не участвуют в работе приложения, но тянутся на каждую ноду. Время сборки растёт по другой причине: правка в коде выше установки зависимостей сбрасывает кэш, и пакеты ставятся заново на каждом прогоне.
Выкатка тормозит не из-за кода, а из-за транспорта: каждая нода вытягивает образ целиком, контейнер дольше выходит в готовность, автомасштабирование под нагрузкой запаздывает. Дополнительно расширяется поверхность атаки — компилятор, отладчик и пакетный менеджер в рантайме дают готовый набор инструментов тому, кто получил доступ внутрь контейнера.
Multi-stage dockerfile разделяет эти роли: один этап собирает артефакт, второй получает только его и минимальное окружение запуска. В финальный слой не попадают ни сборочные инструменты, ни кэш — разбираем, как это устроено и что даёт на практике.
Что даёт разделение сборки на этапы: время, слои, откаты
Разделение сборки на этапы (multi-stage Dockerfile) убирает из финального образа всё, что нужно только для компиляции: компиляторы, заголовочные файлы, менеджеры пакетов и промежуточные артефакты. Остаётся только то, что нужно для запуска: собранное приложение и его библиотеки времени выполнения. На практике из образа уходит 60–90% содержимого — это десятки пакетов, которых в рабочем контейнере просто не должно быть.
Меньше пакетов — меньше поверхность атаки: сборочные зависимости перестают попадать в рабочий контейнер вместе со своими уязвимостями. Меньше размер — быстрее выкатка: узел забирает образ из реестра за секунды, а не минуты, и перезапуск сервиса укладывается в окно обновления.
Воспроизводимость держится на двух вещах: базовый образ фиксируется по digest, а порядок инструкций выстраивается так, чтобы сначала шли редко меняющиеся слои — зависимости, затем код. Docker переиспользует кэш слоёв, пока не изменился вход: правка одного файла пересобирает один слой, а не весь образ. Типовая пересборка после коммита занимает секунды против минут у одноэтапной сборки.
Для откатов это даёт предсказуемость: предыдущий образ остаётся в реестре с прежним тегом, возврат — переключение тега и перезапуск, без повторной сборки. Класс отказов «выкатили не то состояние исходников, что проверяли» уходит: проверенный артефакт и есть артефакт в проде.
Сравнение подходов: обычный Dockerfile и multi-stage сборка
Выбор между multi-stage dockerfile и сборкой в один этап — не про «правильно и неправильно», а про то, что именно вы оптимизируете: скорость итерации, размер образа или воспроизводимость. Сборка в один этап — то, с чего начинают почти все проекты на Docker, и ниже видно, на каком шаге она перестаёт быть выгодной.
| Вариант | Когда подходит | Чем ограничен |
|---|---|---|
| Сборка в один этап | Небольшой сервис, зависимости из готовых пакетов | В финальный образ уходят компиляторы и кэш пакетного менеджера: размер растёт, вместе с ним — поверхность атаки |
| Multi-stage Dockerfile | Компилируемый стек, важны размер образа и скорость выкатки | Артефакты нужно явно переносить между этапами, а кэш зависимостей — выносить отдельным слоем |
| Отдельный образ сборки | Несколько сервисов с общим тулчейном, изолированный контур | Образы сборки надо версионировать и обновлять — это отдельный регулярный процесс |
| Сборка на стороне сборочного агента | Локальные правки, быстрый цикл разработки | Результат зависит от окружения агента, воспроизводимость сборки ниже |
Ключевой эффект multi-stage — работа с кэшем зависимостей. Слой с установкой пакетов ставится до копирования исходников, поэтому правка кода не сбрасывает его: повторная сборка идёт за секунды, а не заново. В финальный образ попадают только артефакты и рантайм — служебные утилиты и исходники остаются в промежуточном этапе.
Ограничения тоже реальны. Если базовый рантайм тянет лишние библиотеки, multi-stage размер не спасёт — нужен другой базовый образ. Отдельный образ сборки оправдан, когда сборок много и тулчейн у них общий: тогда обновление компилятора делается в одном месте, а не в каждом файле.
Порядок работ: от аудита слоёв до передачи регламентов
Прежде чем менять сборку, разбираем, из чего состоит текущий образ: какие слои дают основной объём, что тянет лишние зависимости, какие файлы попадают в артефакт по недосмотру. Аудит образа показывает, где multi-stage dockerfile даст реальный выигрыш, а где узкое место лежит в порядке инструкций или в кешировании.
- Аудит образа — снимаем состав слоёв, находим самые тяжёлые, отмечаем пакеты и файлы, которые не нужны при запуске: кеши пакетных менеджеров, компиляторы, тестовые данные, служебные каталоги.
- Схема этапов сборки — разделяем сборку и запуск, решаем, что переносится в финальный слой: собранный артефакт, конфигурации, сертификаты, а что остаётся только на этапе компиляции.
- Перенос сборки — переписываем инструкции в новый файл, фиксируем базовые образы на актуальной ветке, задаём непривилегированного пользователя и рабочий каталог.
- Проверка воспроизводимости — собираем дважды на пустом кеше и сверяем результат: сборка не должна зависеть от локальных файлов, порядка сети и состояния машины сборщика.
- Сверка до и после — сравниваем размер образа, время сборки, число слоёв и время старта контейнера, убеждаемся, что сервис проходит свои проверки работоспособности.
- Передача регламента сборки — отдаём файл, схему этапов, правила обновления базовых образов и порядок отката на предыдущий тег.
Что заказчик получает: конфигурации, схемы слоёв, регламенты
Результат работы над multi-stage dockerfile — не один файл, а комплект артефактов, по которым команда собирает и обновляет образы без нашего участия. Всё уходит в ваш репозиторий, проверяется на вашем стенде и сопровождается разбором: что изменено и почему именно так.
Комплект закрывает цикл от контекста сборки до отката: фиксируем, какие слои кэшируются и где образ начинает расти.
- Dockerfile с явным разделением этапов — сборка зависимостей, промежуточная обработка, финальный образ; с комментариями, что попадает в каждый слой.
- Схема этапов сборки: что копируется между стадиями и какие файлы не доживают до финального образа.
- Файл исключений и правила контекста сборки — лишнее не отправляется в сборку с самого начала.
- Регламент обновления базовых образов: периодичность, ответственный, проверка совместимости и порядок отката.
- Инструкция для команды: как добавить зависимость, не раздувая финальный слой, и как собрать образ локально.
- Порядок проверок: сравнение размера до и после, разбор состава слоёв, проверка запуска и работоспособности.
- Настроенный конвейер сборки с кэшированием слоёв и публикацией тегов.
- Запись разбора с командой: где узкие места, что оставлено намеренно и какой запас на рост.
Опишите задачу — вернёмся с оценкой и планом и покажем, какие артефакты нужны вашему контуру, а что уже закрыто вашими процессами.
Кейс: как переработка образа сократила время выкатки
До переработки сервис собирался одним Dockerfile: базовый образ с полным набором инструментов сборки, поверх — исходники, компиляторы и кэш пакетных менеджеров. Образ весил около 1,2 ГБ, каждая нода при выкатке тянула его целиком, а правка одной строки в зависимостях сбрасывала кэш и запускала сборку с нуля.
Что изменили
Разложили процесс на стадии. Первая — сборка: ставит зависимости, компилирует, прогоняет тесты. Вторая — рантайм: тонкая база, куда копируется только результат через COPY --from. Добавили .dockerignore, зафиксировали тег базового образа и запуск от непривилегированного пользователя — стадии собираются на актуальной ветке Docker, этого достаточно для предсказуемого кэша слоёв.
Что стало
Сборка сократилась с одиннадцати минут до четырёх: слой зависимостей кэшируется отдельно и не пересобирается при правке кода. Размер образа упал с 1,2 ГБ до 190 МБ — нода забирает его за секунды, поэтому выкатка перестала быть окном ожидания, а откат на предыдущую версию стал обычной операцией.
Стабильность выросла не из-за скорости, а из-за состава образа: в рантайм больше не попадают компиляторы, заголовочные файлы и кэш пакетов, где и жили «плавающие» зависимости. Доля выкаток, которые приходилось откатывать вручную, за квартал упала с 18% до нуля, а размер реестра перестал расти линейно с каждым релизом.
Что делать, если базовый образ уже используется в проде
Переводить сервис на multi-stage dockerfile без остановки работы можно: новая схема собирается параллельно с текущей, из того же репозитория и по тому же триггеру сборки, только результат получает отдельный тег. Прод продолжает жить на старом образе, пока новый проходит проверку на стенде и сравнение метрик.
Первый шаг в multi-stage dockerfile — вынести установку зависимостей и сборку в отдельный этап, оставив в финальном только артефакты и рантайм-библиотеки. Базовый образ на этом шаге не меняем и фиксируем по дайджесту, чтобы результат совпадал с продом: тогда откат — это возврат прежнего тега.
Поэтапное внедрение идёт через тестовое окружение и канареечный деплой на часть инстансов. Сравниваем не только размер образа, но и время старта, потребление памяти и работу с сертификатами — основной веткой новый тег становится только при совпадении метрик.
Обновление базового образа — отдельный, более поздний шаг: смена рантайм-слоя ломает обычно не код, а системные библиотеки и корневые сертификаты, поэтому образ закрепляют по дайджесту, а не по плавающему тегу.
Главные риски при переходе — несовпадение путей и прав доступа между этапами, потеря кеша слоёв из-за перестановки инструкций и сборочный мусор, который раньше попадал в образ. Поэтому в конвейер добавляют проверку: запуск под непривилегированным пользователем и сверку списка файлов и портов с продом.
Технические сомнения: кэш, зависимости, базовые образы
Перед стартом инженеры задают одни и те же вопросы: что происходит с кэшем при изменении кода, как передать доступ к закрытым реестрам и как часто пересобирать образ, чтобы базовый слой не устаревал. Ниже — как это устроено в наших сборках.
Как порядок инструкций влияет на кэш слоёв?
Кэш переиспользует слои до первой изменившейся инструкции. Манифесты зависимостей копируем раньше исходного кода — правка одного модуля не инвалидирует установку пакетов, и повторная сборка идёт минуты, а не десятки минут. Разделение этапов сборки и рантайма убирает компиляторы и промежуточные файлы из финального образа.
Как передавать приватные зависимости, не оставляя их в образе?
Учётные данные нельзя передавать аргументами сборки — они фиксируются в истории слоёв. Доступ к закрытому реестру даём монтированием секрета на время установки: временный конфиг живёт внутри шага и в образ не попадает. Проверяем это разбором готового образа — токенов и ключей в нём не остаётся.
Как часто обновлять базовый образ и пересобирать?
Базовый образ фиксируем по дайджесту: сборка воспроизводима, но уязвимости не подтягиваются сами. Обновляем планово — смотрим, что изменилось в базовом слое, меняем фиксацию, прогоняем тесты. Пересборка идёт на каждый коммит и дополнительно по расписанию, поэтому обновления безопасности доезжают до прода в течение суток.
Если у вас закрытый реестр за периметром, несколько аппаратных архитектур или жёсткие требования к воспроизводимости — эти ограничения разбираем на этапе проектирования, до того как сборка уйдёт в продакшн.Обсудим задачу: разберём ваш образ и вернёмся с оценкой
Чтобы вернуться с оценкой сроков, а не с общими формулировками, нам нужен минимум исходных данных: текущий Dockerfile со всеми стадиями, файл зависимостей, команду сборки из конвейера и лог последнего прогона с итоговым размером образа. По ним сразу видно, какие артефакты сборки остаются в финальном слое, сколько весят инструменты компиляции и где теряется место.
Дальше разбор идёт по шагам: на каждом понятно, что мы отдаём и что требуется от вашей стороны.
- Сбор контекста: Dockerfile, манифесты зависимостей, переменные окружения, инструкция запуска.
- Проверка текущей сборки: распределение размера по слоям и время сборки на чистом кеше.
- Ограничения: целевые платформы, требования к отладочным инструментам в рабочем образе, правила сканирования уязвимостей.
- План работ: какие стадии выделяем, что переносим в стадию сборки, что оставляем в финальной.
- Что отдаём: переработанный multi-stage dockerfile, схему стадий и пояснение по каждому изменению.
- Оценка сроков: разбивка по этапам, точки возврата на согласование и порядок проверки результата.
Опишите задачу и приложите то, что уже есть под рукой, — вернёмся с оценкой и планом работ. Если данных пока мало, подскажем, что собрать в первую очередь, чтобы разбор шёл по фактам, а не по предположениям.







