Что такое multi-stage build?
В одном Dockerfile может быть несколько инструкций FROM -- каждая начинает новый этап. Из финального образа попадают только файлы последнего этапа. На первом этапе вы собираете...
🟢 Junior Level
Простое объяснение
Multi-stage build (Многоэтапная сборка) — это способ создать маленький Docker-образ, используя несколько этапов в одном Dockerfile.
В одном Dockerfile может быть несколько инструкций FROM – каждая начинает новый этап. Из финального образа попадают только файлы последнего этапа. На первом этапе вы собираете приложение, на втором — запускаете его. В итоговый образ попадает только готовый файл, а все инструменты сборки остаются «за бортом».
Аналогия
Представьте, что вы печёте торт. Для приготовления нужны: миксер, форма, духовка, ингредиенты. Но когда вы отдаёте торт клиенту — вы не отдаёте миксер и форму. Клиент получает только готовый торт. Multi-stage build — это то же самое: для сборки нужен Maven и JDK, для запуска — только JRE и jar-файл.
Проблема без multi-stage
FROM maven:3.8-openjdk-17
COPY src ./src
COPY pom.xml .
RUN mvn package -DskipTests
# Образ весит ~800 МБ (Maven + JDK + исходники + зависимости)
CMD ["java", "-jar", "target/app.jar"]
Как работает multi-stage
# Этап 1: Сборка
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn package -DskipTests
# Этап 2: Запуск
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Результат: образ весит ~300 МБ вместо ~800 МБ!
Объяснение по шагам
FROM ... AS builder— первый этап с именем «builder»COPY --from=builder— копируем файл из первого этапа во второй- Второй этап — финальный образ, который идёт в продакшен
Что запомнить
- Multi-stage build = несколько
FROMв одном Dockerfile - Каждый
FROM— отдельный этап COPY --from=имякопирует файлы из другого этапа- Итоговый образ содержит только последний этап
- Потому что только последний FROM попадает в финальный образ. Все предыдущие этапы (компиляторы, зависимости, исходники) остаются «за бортом».
- Это уменьшает размер и повышает безопасность
🟡 Middle Level
Зачем появился multi-stage build?
До этой технологии было два пути:
- Всё в одном образе — итоговый образ содержал Maven, JDK, исходники. Раздутый размер, большая поверхность атаки.
- Builder Pattern — сложные скрипты для переноса артефактов между образами. Неудобно, требует внешних скриптов.
Multi-stage build решил обе проблемы, позволив описать всё в одном Dockerfile.
Детальный разбор
# ЭТАП 1: Сборка (называем 'builder')
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline # Кэшируем зависимости
COPY src ./src
RUN mvn package -DskipTests
# ЭТАП 2: Финальный образ (Runtime)
FROM openjdk:17-jdk-slim
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Типичные ошибки
| Ошибка | Последствие | Как избежать |
|---|---|---|
Неправильный путь в COPY --from |
Файл не найден при сборке | Используйте абсолютные пути |
| Копирование кода до зависимостей | Кэш не работает, медленная сборка | Сначала pom.xml, потом src |
Забыв указать AS для этапов |
Нельзя ссылаться по имени | Всегда давайте этапам имена |
Использование latest на любом этапе |
Недетерминированность | Фиксируйте версии всех базовых образов |
Копирование всего /build |
Лишние файлы в финальном образе | Копируйте только артефакт |
Преимущества
- Минимальный размер — финальный образ содержит только JRE и
.jar. - Безопасность — нет инструментов сборки, меньше поверхность атаки.
- Удобство CI/CD — весь процесс в одном файле.
- Кэширование — Docker кэширует каждый этап независимо.
Продвинутые техники
Остановка на определённом этапе:
docker build --target builder -t my-app-test .
Использование внешних образов:
COPY --from=nginx:latest /etc/nginx/nginx.conf /my/path
Несколько промежуточных этапов:
FROM node:18 AS frontend-build
# ... сборка фронтенда
FROM maven:17 AS backend-build
# ... сборка бэкенда
FROM openjdk:17-slim
COPY --from=backend-build /app.jar .
COPY --from=frontend-build /dist ./static/
Оптимизация кэширования
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline -B # Кэш зависимостей
COPY src ./src
RUN mvn package -DskipTests -B
При изменении кода слой зависимостей берётся из кэша.
Сравнение подходов
| Подход | Размер | Безопасность | Сложность |
|---|---|---|---|
| Single-stage | ~800 МБ | Низкая | Низкая |
| Multi-stage (slim) | ~300 МБ | Средняя | Средняя |
| Multi-stage (alpine) | ~120 МБ | Высокая | Средняя |
| Multi-stage (distroless) | ~80 МБ | Очень высокая | Высокая |
Что запомнить
- Multi-stage build — индустриальный стандарт
- Разделяйте «инструменты сборки» и «среду выполнения»
- Используйте
slimилиalpineна финальной стадии - Можно копировать файлы из внешних образов
--targetпозволяет собрать конкретный этап
Когда НЕ использовать multi-stage build
Для Python/Node.js без этапа компиляции multi-stage часто не нужен – достаточно COPY и RUN. Multi-stage полезен когда есть этап сборки (Java, Go, C++), который не нужен в рантайме.
🔴 Senior Level
Multi-stage build как архитектурный паттерн
Multi-stage build — это реализация принципа минимальных привилегий на уровне образов: runtime-образ содержит только то, что необходимо для выполнения, ничего лишнего.
Анализ безопасности
Без multi-stage:
Образ: maven:3.8-openjdk-17 (~800 МБ)
Содержит: Maven, компилятор Java, исходный код, все зависимости
Риск: взломщик может перекомпилировать код, использовать компилятор для эксплойтов
С multi-stage:
Образ: openjdk:17-jre-slim (~300 МБ)
Содержит: JRE + jar-файл
Риск: минимальный — нет инструментов для модификации кода
Trade-offs
| Подход | Размер | Безопасность | Сложность | Debug |
|---|---|---|---|---|
| Single-stage | ~800 МБ | Низкая | Низкая | Легко |
| Multi-stage (slim) | ~300 МБ | Средняя | Средняя | Нормально |
| Multi-stage (jlink) | ~100 МБ | Высокая | Высокая | Сложно |
| Multi-stage (distroless) | ~80 МБ | Очень высокая | Высокая | Очень сложно |
| Multi-stage (native) | ~50 МБ | Максимальная | Очень высокая | Требует отладчик |
jlink – инструмент JDK для создания минимальной версии Java только с нужными модулями.
Spring Boot 3.x + jlink
FROM ubuntu AS jre-build
RUN apt-get update && apt-get install -y openjdk-17-jdk-headless
RUN jlink \
--add-modules java.base,java.sql,java.xml \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=2 \
--output /jre
FROM eclipse-temurin:17-jdk-slim AS builder
# ... сборка ...
FROM debian:buster-slim
COPY --from=jre-build /jre /jre
COPY --from=builder /build/target/app.jar /app.jar
ENV JAVA_HOME=/jre
ENV PATH="$JAVA_HOME/bin:$PATH"
ENTRYPOINT ["java", "-jar", "/app.jar"]
Кастомный JRE весит 40-60 МБ вместо 300+ МБ полного JDK.
Distroless образы
FROM maven:3.9-eclipse-temurin-17 AS build
# ... сборка ...
FROM gcr.io/distroless/java17-debian12
COPY --from=build /build/target/app.jar /app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
Distroless образы содержат только runtime и не имеют shell/package manager. Атакующий не может выполнить sh внутри контейнера.
Edge Cases
- Файл не найден на втором этапе: используйте абсолютные пути.
COPY --from=builder /build/target/*.jar app.jar— если jar-файл один, glob работает. Если несколько — все скопируются в/app.jarкак директория. - Кэш не работает: код скопирован до зависимостей. Решение: сначала
pom.xml, затемRUN dependency:go-offline, потомsrc. - Временные файлы сборки: если
RUNсоздаёт временные файлы, они остаются в слое builder-этапа. Это нормально — они не попадут в финальный образ. - Секреты на этапе сборки: если на этапе сборки нужен доступ к private Maven repo, используйте BuildKit
--mount=type=sshили--mount=type=secret. Не передавайте токены через ARG. - Cross-compilation: сборка ARM-образа на amd64 хосте. Используйте
docker buildxс QEMU или remote builders.
Native Image (GraalVM) + Multi-stage
FROM ghcr.io/graalvm/native-image:ol8-java17 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN native-image -jar target/app.jar -o app
FROM debian:buster-slim
COPY --from=builder /build/app /app
ENTRYPOINT ["/app"]
Итоговый образ: ~50-100 МБ, мгновенный запуск (< 100ms), минимальная память. Trade-off: долгое время сборки Native Image (минуты), не все фичи Spring поддерживаются (нужен Spring Native / AOT).
// Native Image – компиляция Java в нативный бинарник. // Плюс: запуск < 100ms. Минус: сборка занимает минуты, // не все фичи Spring поддерживаются (рефлексия, прокси).
Производительность
| Стратегия | Размер образа | Время запуска | RAM footprint |
|---|---|---|---|
| Full JDK | ~500 МБ | ~5-8s | ~400-600 MB |
| JRE slim | ~300 МБ | ~5-8s | ~300-500 MB |
| jlink custom JRE | ~100 МБ | ~4-6s | ~200-400 MB |
| Distroless | ~80 МБ | ~4-6s | ~200-400 MB |
| GraalVM Native | ~50 МБ | < 0.1s | ~50-150 MB |
Troubleshooting
Проблема: файл не найден.
COPY --from=builder /build/target/app.jar app.jar
# ERROR: file not found
Решение: проверьте абсолютные пути. Используйте RUN ls -la /build/target/ на этапе builder для отладки. Или docker build --target builder для инспекции промежуточного образа.
Проблема: кэш не работает.
# ПЛОХО
COPY src ./src
COPY pom.xml .
RUN mvn package
Решение: поменяйте порядок. Сначала pom.xml, потом RUN dependency:go-offline, потом src.
Production Story
Команда микро-сервисов (40+ сервисов) использовала single-stage образы по 700-900 МБ. Деплой одного сервиса занимал 3-5 минут (pull образа). Общий registry потреблял 35 ГБ. Внедрение multi-stage с distroless образами сократило средний размер до 90 МБ, время деплоя — до 30 секунд, registry — до 4 ГБ. Экономия: 88% storage, 90% времени деплоя. Дополнительный бонус: distroless образы прошли security audit без замечаний — нет shell, нет package manager, минимальная поверхность атаки.
Monitoring
docker images— отслеживайте размеры образовdive <image>— анализ содержимого каждого слоя- Registry size metrics — мониторинг хранилища образов
- Build cache hit rate — эффективность кэширования в CI/CD
- Image scan results (Trivy, Snyk) — количество уязвимостей на образ
Резюме
- Multi-stage build — стандарт создания production-образов.
- Лучший способ соблюсти баланс между удобством разработки и компактностью образа.
- Всегда разделяйте «инструменты сборки» и «среду выполнения».
- Для максимальной оптимизации: jlink, distroless, GraalVM Native Image.
- Кэширование зависимостей отдельно от кода — ключ к быстрым CI/CD сборкам.
- Безопасность: чем меньше образ, тем меньше поверхность атаки.
- Используйте
--targetдля тестирования промежуточных этапов.
🎯 Шпаргалка для интервью
Обязательно знать:
- Multi-stage build = несколько FROM в одном Dockerfile; только последний попадает в финальный образ
- Разделяем «инструменты сборки» (Maven, JDK) и «среду выполнения» (JRE, jar)
- Уменьшение размера: single-stage ~800MB → multi-stage slim ~300MB → distroless ~80MB
- Безопасность: нет инструментов сборки в production-образе, меньше attack surface
- Кэширование зависимостей отдельно от кода — ключ к быстрым CI/CD сборкам
- Для максимальной оптимизации: jlink (custom JRE), distroless, GraalVM Native Image
--targetпозволяет собрать и протестировать промежуточный этап
Частые уточняющие вопросы:
- «Почему образ уменьшается?» — Компиляторы, исходники, зависимости сборки не попадают в финальный образ
- «Что такое distroless?» — Образ без shell/package manager; атакующий не может выполнить
shвнутри контейнера - «Когда НЕ нужен multi-stage?» — Python/Node.js без этапа компиляции; достаточно COPY + RUN
- «Что даёт jlink?» — Кастомный JRE только с нужными модулями (40-60MB вместо 300MB)
Красные флаги (НЕ говорить):
- «Использую один образ для сборки и запуска» (раздутый размер, security risk)
- «Multi-stage замедляет сборку» (кэширование делает его быстрее)
- «Копирую весь каталог builder’а» (нужен только артефакт)
- «Distroless образы невозможно отлаживать» (ephemeral debug containers решают)
Связанные темы:
- [[Что такое Dockerfile]] — основы Dockerfile
- [[Какие основные инструкции используются в Dockerfile]] — инструкция COPY –from
- [[Что такое контейнеризация и зачем она нужна]] — безопасность образов