Всем привет, меня зовут Сергей Прощаев. Я руководитель направления Java-разработки в FinTech. В этой статье я хочу поговорить о том, что часто остаётся за кадром хайповых докладов — о конкретной, измеримой эффективности Docker-образов. О том, как сборка образа перестаёт быть «лишь бы запустить» и превращается в инженерную дисциплину, влияющую на скорость деплоя, безопасность и даже кошелёк компании.
Покажу вам на реальных примерах, как оптимизация образа может сократить время развёртывания в 5–7 раз, и расскажу, какие практики сегодня используют команды, которым действительно важно доставлять код быстро и надёжно. А в конце поделюсь, где можно научиться проектировать такие системы не на костылях, а на рельсах современных подходов.
Толстый образ — пожиратель ресурсов инфраструктуры
В сети есть не мало описаний перевода монолита на независимые сервисы. При этом если к этому относиться как попало — то простой docker pull нового образа может занимать до 3–4 минут. А если нужно было обновить сразу 5 сервисов? Деплой может растянуться на полчаса. Если посмотреть, что в итоге таскается по сети, то вполне реально найти сервисы с весом до 2-х гигабайт. Два гигабайта! Зачем? Мы затащили туда JDK, Maven, все зависимости для сборки, кучу системных утилит «на всякий случай». Образ был собран по принципу «что вижу, то пою» — установил всё в один слой, скопировал исходники, собрал прямо там.
Образ — это не просто упаковка кода. Это артефакт, который должен быть минимальным, безопасным и быстрым в транспортировке. И вот почему размер образа — это не просто цифра в реестре. Давайте выделим основные моменты:
Скорость деплоя. Каждый мегабайт образа тянется по сети. Если у вас кластер на 50 нод, и каждая тянет 2 ГБ, считайте трафик и время. Даже в дата-центре с гигабитными линками это ощутимо. А в облаке за трафик ещё и платить.
Поверхность атаки. Внутри образа часто валяются ненужные бинарники: компиляторы, отладчики, bash, утилиты. Если злоумышленник получит доступ к контейнеру, эти инструменты станут его верными помощниками.
Время запуска. Чем больше слоёв и файлов, тем дольше контейнерный рантайм распаковывает образ. Особенно это критично для serverless и функций, которые стартуют «на лету».
Ресурсы реестра. Хранить и сканировать огромные образы — дорого (в прямом и переносном смысле).
Рекомендации по цифрам из практики: после первой такой оптимизации у вас сократится размер среднего образа с 800 МБ до 120 МБ. За ним упадет время docker pull с 2 минут до 20 секунд.
Почему образы жиреют?
Прежде чем лечить, надо понять причины. Типичный «толстый» Dockerfile выглядит примерно так:
FROM maven:3.8-openjdk-11 WORKDIR /app COPY . . RUN mvn clean package CMD ["java", "-jar", "target/myapp.jar"]
Красиво? Да. Работоспособно? Да. Оптимально? Нет.
Что здесь не так?
Базовый образ. maven:3.8-openjdk-11 весит под 800 МБ. Внутри полно всего: сам Maven, JDK, куча библиотек Linux. Нам для рантайма нужна только JRE (а ещё лучше — JRE на минималках).
Инструменты сборки в рантайме. Maven нужен только чтобы собрать jar. Но он остаётся в финальном образе. Это как нанять строителей, а потом оставить их жить в построенном доме.
Кэширование слоёв. Каждый раз при изменении любого файла исходников слой COPY . . инвалидируется, и Maven скачивает все зависимости заново. Деплой тормозит ещё и на этапе сборки.
Права root. Приложение по умолчанию запускается от root. Если злоумышленник взломает приложение, он получит полный контроль над контейнером (и, возможно, над хостом, если контейнер запущен с привилегиями).
Лекарство: Best Practices, проверенные деплоем
Со временем индустрия выработала набор практик, которые превращают Dockerfile в произведение инженерного искусства. Я разберу основные, а потом покажу, как их применить на Java-проекте.
1. Multi-stage builds — разделяй и властвуй
Это главный инструмент для удаления сборочного мусора. Идея проста: в одном Dockerfile описывается несколько этапов (стадий). На первом этапе мы собираем артефакт, используя тяжёлый образ со всеми инструментами. На втором — берём минимальный базовый образ и копируем в него только готовый артефакт.
2. Правильный базовый образ
eclipse-temurin:17-jre-alpine(или-slim) — JRE на Alpine Linux, ~150–200 МБ.openjdk:17-slim— Debian Slim, чуть больше, но привычнее.gcr.io/distroless/java17— от Google, вообще без shell и лишних утилит, только JRE и приложение. Минимальный размер и максимальная безопасность. Минус — в контейнер не зайти сdocker exec, но в проде это и не нужно.
3. Использование .dockerignore
Аналог .gitignore. Не тащите в образ исходники, .git, логи, временные файлы. Это не только уменьшает размер, но и ускоряет сборку (контекст меньше).
4. Минимизация слоёв и кэширование зависимостей
Каждая инструкция RUN, COPY, ADD создаёт слой. Слои кэшируются. Если расположить команды так, чтобы редко меняющиеся слои были вверху, сборка будет летать. Для Java это означает: сначала скопировать pom.xml и скачать зависимости, потом уже копировать исходники.
COPY pom.xml . RUN mvn dependency:go-offline # скачает все зависимости и закэширует слой COPY src ./src RUN mvn package
5. Запуск от непривилегированного пользователя
Создайте пользователя в контейнере и переключитесь на него.
RUN addgroup --system --gid 1001 appgroup && \ adduser --system --uid 1001 appuser --ingroup appgroup USER appuser
Как это делают в Uber: история про Python и distroless
Когда речь заходит о промышленных масштабах, кейс Uber — хрестоматийный пример. Несколько лет назад их команда столкнулась с проблемой: тысячи микросервисов на Python, каждый со своим окружением. Базовые образы были большими, содержали кучу пакетов ОС, и, что хуже, в них регулярно находили уязвимости (CVE). Каждое обновление безопасности требовало пересборки всех образов, что занимало вечность.
Инженеры Uber решили пойти радикальным путём: они начали использовать distroless-образы для Python. Это специальные образы от Google, в которых есть только интерпретатор Python и необходимые системные библиотеки — никакого shell, никаких пакетных менеджеров, ничего лишнего. Но как же тогда устанавливать Python-зависимости? Ведь нужен pip!
Решение — классический multi-stage. На этапе сборки использовался тяжёлый образ с pip, туда копировался requirements.txt, устанавливались пакеты в отдельную папку. На финальном этапе из distroless-образа брался голый Python, и в него копировалась папка с установленными пакетами и код приложения.
Результат: размер образа сократился в среднем на 40%, количество уязвимостей упало на 97%, а время деплоя уменьшилось кратно. Кроме того, distroless-образы запрещают запуск shell, ��то делает эксплуатацию уязвимостей типа RCE значительно сложнее — злоумышленнику просто нечем будет пользоваться.
Кейс достаточно интересный, и если ранее с этим не работали — можно попробовать перевести несколько Java-сервисов на distroless. По началу будет непривычно, что нельзя зайти в контейнер и посмотреть логи (логи должны быть в stdout). Но безопасность и скорость этого стоит!
Визуализируем: как устроен оптимальный образ
Чтобы понимать разницу, давайте посмотрим на схемы. Первая показывает классический «толстый» образ

и оптимальный — с multi-stage, изображенный на рисунке 2:

Следующая схема, изображенная на рисунке 3, показывает процесс multi-stage сборки.

Пример на Java: от плохого к хорошему
Давайте возьмём простое Spring Boot приложение с одним REST-контроллером. Я создал два Dockerfile — плохой и хороший. Плохой — это копия того, что я часто вижу на стажировках.
Плохой Dockerfile.bad:
FROM maven:3.8-openjdk-11 WORKDIR /app COPY . . RUN mvn clean package EXPOSE 8080 CMD ["java", "-jar", "target/demo-0.0.1-SNAPSHOT.jar"]
Размер образа после сборки: ~720 МБ. Внутри куча всего: Maven, исходники, кэш зависимостей.
Хороший Dockerfile.good (multi-stage):
## ---- Builder stage ---- FROM maven:3.8-openjdk-11 AS builder WORKDIR /build # Копируем только pom.xml для кэширования зависимостей COPY pom.xml . # Скачиваем все зависимости (слой закэшируется) RUN mvn dependency:go-offline -B # Теперь копируем исходники и собираем COPY src ./src RUN mvn clean package -DskipTests ## ---- Final stage ---- FROM eclipse-temurin:17-jre-alpine # Создаём непривилегированного пользователя RUN addgroup -g 1001 -S appuser && adduser -u 1001 -S appuser -G appuser WORKDIR /app # Копируем только собранный jar из builder COPY --from=builder /build/target/demo-0.0.1-SNAPSHOT.jar app.jar # Переключаемся на непривилегированного пользователя USER appuser EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]
Размер образа: ~180 МБ. Это в 4 раза меньше. И безопаснее: нет shell, нет компилятора, приложение не от root.
Можно пойти дальше и использовать distroless:
FROM maven:3.8-openjdk-11 AS builder # ... сборка как выше ... FROM gcr.io/distroless/java17-debian11 WORKDIR /app COPY --from=builder /build/target/demo-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 CMD ["app.jar"]
Размер станет около 150 МБ, а поверхность атаки ещё меньше. Правда, отладка в таком контейнере сложнее — нет shell. Но для прода это плюс.
Что ещё можно улучшить?
На этом оптимизация не заканчивается. Вот несколько приёмов из арсенала профи:
Линтеры. Используйте
docker scanилиtrivyдля поиска уязвимостей.hadolintпроверит Dockerfile на антипаттерны.
Сжатие слоёв. Инструменты типа
docker-slimанализируют приложение и выбрасывают из образа всё, что не используется во время выполнения.Разделение по слоям в Spring Boot. Начиная с Spring Boot 2.3, можно собирать jar со слоями (layers), что позволяет кэшировать зависимости отдельно от кода даже без multi-stage. В Dockerfile можно скопировать слои по отдельности.
Заключение: от ремесла к инженерии
Сборка Docker-образа — это не просто запись инструкций в файл. Это этап, на котором закладываются эксплуатационные характеристики вашего сервиса. Маленький образ быстрее доставляется, безопаснее, экономит ресурсы. Лучшие практики, о которых мы говорили — мультистейдж, минимальные базовые образы, непривилегированный пользователь, — должны стать вашим стандартом.
Многие разработчики считают: «Подумаешь, образ большой — диски сейчас дешёвые». Но когда у вас 100 микросервисов, которые обновляются по 10 раз в день, эти «мелочи» превращаются в недели простоя и миллионы потраченного впустую трафика. Инвестируйте время в правильную сборку образа сейчас — сэкономите его потом многократно!

Если вам интересно глубже разобраться в том, как проектировать микросервисные архитектуры, управлять их жизненным циклом и внедрять современные практики DevOps (разработка и эксплуатация), обратите внимание на курс «DevOps (разработка и эксплуатация) практики и инструменты». Чтобы узнать, подойдет ли вам программа курса, пройдите вступительный тест.
Для знакомства с форматом обучения и экспертами приходите на бесплатные уроки:
2 марта, 20:00. «Docker образы. Микросервисы». Записаться
17 марта, 20:00. «Работа с данными и сетями в Docker». Записаться
24 марта, 20:00. «Технология контейнеризации. Введение в Docker». Записаться
