Scala давно зарекомендовала себя как мощный язык программирования для построения высокопроизводительных серверных приложений, при этом его тесная интеграция с JVM предоставляет разработчикам доступ к огромной экосистеме Java. Популярность контейнеризации, прежде всего благодаря Docker, позволяет создавать универсальные образы приложений, которые можно запускать на различных платформах без изменения кода. Однако, когда речь заходит о JVM-приложениях, в частности на Scala, создаются образы, вес которых многим кажется излишне большим. В данной публикации исследуется вопрос минимизации контейнерных образов Scala с помощью системы сборки Nix, позволяющей получить воспроизводимые и легкие образы, не теряя при этом функциональность. Изначально при упаковке Scala-приложения в Docker контейнер основное внимание уделяется формированию так называемого «über JAR» — архивa, который содержит не только собственный байт-код, но и все необходимые зависимости.
Это сильно упрощает запуск приложения, так как не нужно отдельно устанавливать и настраивать все библиотеки. Тем не менее, для работы такого байнари требуется Java Virtual Machine, которую зачастую включают целиком в образ. Полная версия JDK (Java Development Kit) весит несколько сотен мегабайт и часто выходит избыточной для целей рантайма, ведь JDK нужна в основном для компиляции и разработки, а для выполнения достаточно JRE (Java Runtime Environment). Важным моментом является выбор между полным JDK и минимальным JRE. Nix предлагает возможность создания минимального JRE с помощью модуля jre_minimal, который использует инструмент jlink.
Он позволяет собрать «кастомный» рантайм, включающий только те модули Java, которые действительно нужны вашей программе, что значительно сокращает размер итогового окружения. Для определения требуемых модулей применяют утилиту jdeps, анализирующую зависимости вашего приложения и выдающую список необходимых библиотек. Использование jre_minimal с правильно подобранным набором модулей позволяет снизить размер контейнера на несколько сотен мегабайт по сравнению с традиционным подходом, при котором в образ включается полный JDK. Однако стоит учитывать, что минимальный JRE может не включать критичные для работы библиотеки вроде sun.misc.
Unsafe или криптографические расширения, которые необходимы для сетевых соединений и безопасности. Для их добавления в конфигурацию jre_minimal требуется указывать дополнительные модули, например, jdk.crypto.ec и jdk.crypto.
cryptoki, чтобы избежать ошибок, связанных с SSL-соединениями. Другим фактором влияния на размер образа является организация файловой структуры внутри контейнера. При использовании Nix существуют особенности при копировании зависимостей в образ – если просто скопировать готовый билд, велика вероятность дублирования артефактов в разных директориях, что приводит к избыточному расходу места. Вместо этого разумно использовать механизм pkgs.buildEnv, который создаёт окружение с символическими ссылками на необходимые пакеты из nix/store.
Такой подход упрощает управление зависимостями, минимизирует дублирование данных и позволяет сохранить компактность образа. Применение makeWrapper помогает упаковать java-приложение так, чтобы оно выглядело как обычный исполняемый файл. Этот бинари-обёртка вызывает java с нужными параметрами, указывая путь к вашему assembly JAR и основной точке входа. Благодаря этому команду запуска можно указать как запуск одного бинарника без лишних настроек и манипуляций. Использование такого комплексного подхода — создание минимального JRE с jlink, точный выбор модулей с jdeps, контроль переноса файлов с помощью buildEnv и упаковка приложения через makeWrapper — даёт итоговое решение, позволяющее получить Docker-образ весом порядка 200 МБ.
Это существенно меньше, чем самые распространённые размеры в сотни мегабайт с традиционными образами, и при этом приложение остаётся полностью функциональным и готовым к продакшену. Сравнительный анализ показывает, что решение на базе Nix оказывается не только компактным, но и более управляемым с точки зрения зависимостей. В то время как некоторые альтернативные подходы позволяют достигать даже меньшего веса (около 120 МБ), они часто сопровождаются большим количеством побочных бинарников и обильным шумом в корне файловой системы контейнера. Такой «засорённый» образ увеличивает вероятность ошибок и усложняет сопровождение и обновление. Одним из ключевых преимуществ Nix является способность создавать воспроизводимые сборки.
Это значит, что вне зависимости от окружения, в котором вы строите ваше приложение, вы получите идентичный результат, что улучшает стабильность и интеграцию процессов CI/CD. Это прекрасно сочетается с требованиями современных облачных платформ и корпоративных систем, где важна надежность и предсказуемость. Не стоит забывать, что JVM-приложения и контейнеризация – достаточно сложная тема, и оптимизация образов требует знаний особенностей обеих технологий. Большой размер типичного образа не просто следствие неаккуратности, а часть экосистемы JVM, которая заточена под универсальность, а не малый размер. Тем не менее, инструменты вроде Nix и jlink меняют ситуацию, делая образы компактнее и эффективнее.