Що таке 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, мінімальна поверхня атаки.
Моніторинг
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
- [[Що таке контейнеризація і навіщо вона потрібна]] — безпека образів