Как использовать multistage builds для повышения безопасности?
Multistage build – это техника построения Docker-образов, при которой Dockerfile содержит несколько стадий сборки, и в финальный образ копируются только необходимые артефакты, исключая компилятор, исходный код, тесты и build-утилиты.
Аналогия: multistage build – как кухня ресторана. Гостю (production) подают только готовое блюдо, а все ножи, разделочные доски и отходы (JDK, Maven, исходники) остаются на кухне и не попадают в зал.
Проблема без multistage build
Пример
# ПЛОХО: всё в одном образе
FROM eclipse-temurin:21-jdk
WORKDIR /app
COPY . .
RUN ./mvnw clean package
ENTRYPOINT ["java", "-jar", "target/app.jar"]
# Результат: образ ~800 MB, содержит JDK, Maven, исходники, тесты, .git
Этот образ содержит JDK с компилятором, Maven wrapper, все исходные файлы, тестовые зависимости, .git директорию – всё это увеличивает поверхность атаки и даёт атакующему инструменты для дальнейшего проникновения.
Правильный multistage build для Java
Multistage build с layered Spring Boot jar
# ============ Этап 1: Сборка ============
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
# Кэширование зависимостей Maven
COPY pom.xml .
COPY .mvn .mvn
COPY mvnw .
RUN ./mvnw dependency:go-offline -B
# Копирование исходного кода и сборка
COPY src ./src
RUN ./mvnw clean package -DskipTests -B && \
# Извлечение Spring Boot layered jar для лучшего кэширования
java -Djarmode=layertools -jar target/*.jar extract --destination /extracted
# ============ Этап 2: Финальный образ ============
FROM eclipse-temurin:21-jre-alpine
RUN addgroup -S banking && adduser -S banking -G banking
WORKDIR /app
# Копирование только скомпилированного приложения (layered)
COPY --from=builder --chown=banking:banking /extracted/dependencies/ ./
COPY --from=builder --chown=banking:banking /extracted/spring-boot-loader/ ./
COPY --from=builder --chown=banking:banking /extracted/snapshot-dependencies/ ./
COPY --from=builder --chown=banking:banking /extracted/application/ ./
USER banking
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/actuator/health || exit 1
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
Multistage build с кастомным JRE (jlink)
Трёхстадийная сборка с jlink
# Этап 1: Сборка приложения
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
COPY . .
RUN ./mvnw clean package -DskipTests
# Этап 2: Определение нужных Java-модулей и создание кастомного JRE
FROM eclipse-temurin:21-jdk-alpine AS jre-builder
COPY --from=builder /build/target/*.jar /app/app.jar
RUN jar xf /app/app.jar && \
jdeps \
--ignore-missing-deps \
--print-module-deps \
--multi-release 21 \
--class-path 'BOOT-INF/lib/*' \
/app/app.jar > /modules.txt && \
jlink \
--add-modules $(cat /modules.txt) \
--strip-debug \
--no-man-pages \
--no-header-files \
--compress=zip-9 \
--output /custom-jre
# Этап 3: Минимальный финальный образ
FROM alpine:3.20
COPY --from=jre-builder /custom-jre /opt/java
COPY --from=builder /build/target/*.jar /app/app.jar
RUN addgroup -S app && adduser -S app -G app
USER app
ENV JAVA_HOME=/opt/java
ENV PATH="${JAVA_HOME}/bin:${PATH}"
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Что удаляется из финального образа
| Компонент | Угроза | Удалён в multistage? |
|---|---|---|
| JDK (javac, jdb) | Компиляция вредоносного кода на месте | Да |
| Maven/Gradle | Скачивание вредоносных зависимостей извне | Да |
| Исходный код | Реверс-инжиниринг бизнес-логики | Да |
| Тестовый код и данные | Утечка тестовых учётных данных | Да |
| Build-утилиты (git, curl) | Инструменты для развития атаки | Да |
.git директория |
История изменений, возможные секреты в коммитах | Да |
Файл .dockerignore
Не забывайте о .dockerignore – он предотвращает копирование ненужных файлов в контекст сборки:
Пример
.git
.gitignore
*.md
docker-compose*.yml
.env
.idea
*.iml
target
!target/*.jar
Вывод
Multistage build – обязательная практика для production-образов. Она уменьшает размер образа в 5-10 раз, удаляет из финального образа все build-инструменты и исходный код, что радикально сокращает поверхность атаки.
На собеседовании: покажите понимание двух- или трёхстадийной сборки, объясните, почему каждый удалённый компонент – это закрытая дверь для атакующего. Бонус – упомяните layered jars Spring Boot и jlink для создания кастомного JRE.