1. Що таке jarmode=tools
Якщо ви колись думали: «Чому не можна просто взяти jar і запустити?», — вітаю, це цілком нормальна думка. Але Docker-збирання любить, коли ми вміємо не лише запускати, а й розбирати артефакт на частини. jarmode=tools — це як сервісний режим в автомобілі: двигун той самий, але замість поїздки ми відкриваємо капот і проводимо техобслуговування.
Spring Boot уміє пакувати застосунок так, що всередині jar з’являється інформація про шари (layers index), а разом із цим до архіву додається підтримка спеціального jar mode — tools. Це дає змогу запускати jar не як «підняти сервер на порту», а як «виконати службову команду»: показати шари або вилучити їх у файлову систему. Саме так і працює jarmode=tools, і саме це є канонічним підходом в актуальній документації Spring Boot 4.0.x.
Важливо одразу зафіксувати одну думку — вона потім неабияк економить нерви: jarmode=tools — це не якась окрема магія, вигадана в Docker. Це звичайний запуск Java-процесу із системною властивістю -D.... Магії тут рівно стільки ж, скільки у фразі «запусти Java з параметром».
2. tools — режим jar, а не утиліта
Психологічно новачкові простіше уявити окрему утиліту, ніж jar, який уміє працювати в різних режимах. Тому часто виникає очікування, що tools — це якась зовнішня програма на кшталт gradle чи docker. Насправді все навпаки: jar — той самий, а спосіб його запуску змінюється завдяки параметру JVM -Djarmode=tools.
Технічно це виглядає так: ми запускаємо java -jar ..., але додаємо системну властивість jarmode. Boot loader — той самий завантажувач, який зазвичай запускає застосунок, — бачить, що треба не запускати ваш main(), а виконати службову команду. У результаті jar починає поводитися як міні-CLI. Документація прямо показує такий запуск як базовий вхід до режиму tools:
java -Djarmode=tools -jar my-app.jar
І так, це той рідкісний випадок, коли «запусти jar без аргументів» — корисна команда. Бо саме вона показує підказку, які команди доступні.
Окремо корисно не плутати дві схожі на вигляд речі: jarmode=tools і профілі Spring (spring.profiles.active). Профілі впливають на поведінку застосунку під час запуску. А jarmode=tools впливає на те, чи запускається застосунок узагалі. У режимі tools сервер не підіймається, тож жодні server.port тут поки що не мають сенсу.
3. Підготовка: правильний jar
Дуже легко потрапити в пастку: «Ну я ж просто подивлюся шари». Java, звісно, не проти, але шари з’являться лише тоді, коли jar зібрано як Spring Boot executable jar і він містить індекс шарів. У межах курсу ми працюємо з наскрізним проєктом Container-Ready Catalog Service, і його базовий артефакт для Docker — це результат bootJar.
У Gradle-проєкті це зазвичай виглядає так — показую в максимально «копійованому» вигляді:
# 1) Збираємо саме bootJar, щоб отримати виконуваний Spring Boot jar із шарами
./gradlew clean bootJar
# 2) Перевіряємо, що jar справді з’явився там, де ми очікуємо
ls -la build/libs
# побачите щось на кшталт:
# docker-java-catalog-service-0.0.1-SNAPSHOT.jar
Якщо ви на Windows і ls вам не звичний — не страшно, сенс той самий: jar лежить у build/libs. У документації Spring Boot навіть окремо нагадують, що приклад із target/*.jar треба замінити на build/libs/*.jar, якщо ви працюєте з Gradle.
Справжня назва bootJar на диску може містити версію, і це нормально. Щоб далі не танцювати навколо версії, будемо тримати просте правило: зовні беремо реальний файл із build/libs, а всередині Docker-стейджів одразу називаємо його application.jar. Це локальний псевдонім для extract-команд, а не нова «офіційна» назва артефакту проєкту. Тому в shell-командах нижче фігуруватиме build/libs/<ваш-bootJar>.jar, а в Dockerfile — стабільний application.jar.
4. Запуск tools-режиму і підказка
У нас є jar. Тепер найкорисніша вправа для мозку — запустити tools без команди й подивитися, що він узагалі вміє. Саме це — той випадок, коли «прочитати help» не занудство, а економія часу.
Команда така:
# Запускаємо jar у спеціальному режимі tools (застосунок при цьому НЕ стартує)
java -Djarmode=tools -jar build/libs/<ваш-bootJar>.jar
# Підказка: якщо все гаразд, ви побачите usage і список команд (extract/list-layers/help)
Документація Spring Boot 4.0.3 показує, що у відповідь ви побачите usage і список доступних команд. Там є щонайменше три: extract, list-layers, help.
Сенс кожної простий:
list-layers — подивитися, які шари взагалі є у вашому jar і як вони називаються. Це інспекція: вона нічого не розпаковує.
extract — розкласти jar по каталогах за шарами, щоб потім ці каталоги можна було копіювати окремими COPY у Dockerfile і тим самим отримувати точніші Docker-шари. Документація прямо каже, що extract потрібен, щоб «split the application into layers» для Dockerfile.
help — підказка щодо конкретної команди, корисна тоді, коли ви забули аргументи.
І тут з’являється важлива звичка професіонала: перед тим як писати Dockerfile навмання, ми спочатку читаємо, які команди доступні й як вони називаються. Це суттєво знижує шанс, що ви почнете гуглити «чому не працює layertools», хоча насправді ви вже на Boot 4.
5. list-layers: спочатку побачити назви шарів, потім писати Dockerfile
Коли студент уперше бачить layered Dockerfile, у нього зазвичай дві реакції. Перша: «О, так можна було?». Друга: «Я все одно не розумію, звідки беруться ці дивні папки». Команда list-layers — це якраз відповідь на другу реакцію: ви не вірите Dockerfile на слово, а спочатку запитуєте шари в jar.
Запускається це так:
# Команда нічого не розпаковує: вона лише друкує назви шарів із layer index
java -Djarmode=tools -jar build/libs/<ваш-bootJar>.jar list-layers
Що ви отримуєте на виході? Список назв шарів, які jar готовий вилучати. У типовому Spring Boot-застосунку ви побачите знайомий набір на кшталт dependencies, spring-boot-loader, snapshot-dependencies, application. Ключове тут інше: list-layers дає вам точні рядки, які потім фігуруватимуть у файлах і шляхах.
Педантичний, але дуже практичний момент: назви шарів краще сприймати як «API». Якщо команда вивела spring-boot-loader, значить у результаті вилучення ви отримаєте директорію spring-boot-loader/. Не loader/, не boot-loader/, а рівно те, що сказав інструмент. Чим менше ви «здогадуєтеся», тим менше потім дебажите.
Ще один приємний момент: list-layers — це швидкий спосіб перевірити, що ваш jar справді підтримує шари. Якщо ви з якоїсь причини вимкнули layering під час збирання, тут усе стане ясно ще до того, як ви напишете Dockerfile на 40 рядків.
6. extract: розкладаємо jar за шарами
Тепер ми переходимо від «подивитися» до «зробити». Команда extract — це робоча конячка layered-підходу. Вона бере ваш jar і розкладає його вміст у директорію, яку ви вкажете, причому так, щоб кожен шар опинився окремою папкою. Документація Spring Boot прямо показує канонічну форму команди для Dockerfile: extract --layers --destination layers.
Ось базовий варіант для локального запуску — без Dockerfile, просто щоб зрозуміти, що вийде:
# Очищаємо попередній результат, щоб не сплутати вивід і не отримати "змішані" файли
rm -rf layers
mkdir -p layers
# Вилучаємо jar за шарами в явно задану директорію
java -Djarmode=tools -jar build/libs/<ваш-bootJar>.jar extract \
--layers \
--destination layers
Зверніть увагу на два прапорці, які тут зовсім не «косметика».
--destination layers робить результат передбачуваним. У навчальному проєкті та в Dockerfile ми не хочемо «десь воно розпакувалося, не знаю де». Ми хочемо точний шлях, який зручно потім копіювати. Документація теж робить акцент на --destination.
--layers каже: «розкладай не абияк, а за шарами». Тобто це не просто «unzip jar», а саме «дотримуйся layer index».
Після виконання команди ви побачите, що всередині layers/ з’явилися піддиректорії шарів. Якщо ви любите дивитися очима, можна зробити так:
# Швидка перевірка: чи з’явилися директорії шарів
ls -la layers
# очікувано побачите папки на кшталт:
# dependencies/
# spring-boot-loader/
# snapshot-dependencies/
# application/
І ось тепер відбувається важлива зміна картини світу: ваш jar перестає бути «одним файлом» з точки зору Docker build. Він стає набором директорій, які можна копіювати окремими командами COPY, перетворюючи кожну з них на окремий шар образу.
Якщо уявити це схемою — без спроби намалювати шедевр, — вийде приблизно так:
flowchart TD %% Ліворуч — один JAR, праворуч — каталоги за шарами після extract A["bootJar: один файл .jar"] --> B["java -Djarmode=tools -jar ... list-layers"] B --> C["назви шарів (список)"] A --> D["java -Djarmode=tools -jar ... extract --layers --destination layers"] D --> E["layers/dependencies"] D --> F["layers/spring-boot-loader"] D --> G["layers/snapshot-dependencies"] D --> H["layers/application"]
Ця схема хороша тим, що в ній немає Docker. Бо зараз ми навчаємося саме «отримувати правильні каталоги», а не «збирати фінальний образ». Але саме ці каталоги Docker потім використовуватиме як будівельні блоки.
7. Що відбувається після extract і навіщо тут application.jar
На цьому місці в новачків часто виникає мініпаніка: «Зачекайте, ми ж бачили application.jar. Це і є фінальний runtime-артефакт?» Важливо розділити дві ролі.
У stage extract application.jar — це просто зручна локальна назва для вихідного bootJar, який ми взяли з build/libs. Вона потрібна, щоб команди list-layers і extract були короткими та не залежали від версії в імені файлу.
Після extract головним результатом стають уже не jar-файли, а каталоги шарів: layers/dependencies, layers/spring-boot-loader, layers/snapshot-dependencies, layers/application. Саме їх потім копіюють у runtime окремими COPY, щоб Docker отримав кілька незалежних шарів замість одного COPY app.jar.
Тому фінальний ручний layered-шлях зазвичай закінчується не повторним java -jar application.jar, а запуском через org.springframework.boot.loader.launch.JarLauncher. Це важливе розрізнення: application.jar живе як зручний вхідний артефакт stage extract, а runtime уже працює з розкладеною exploded-структурою застосунку.
Якщо згорнути це в короткий ланцюжок, вийде так:
- build/libs/<ваш-bootJar>.jar — реальний executable jar після bootJar;
- application.jar — його стабільна назва всередині stage extract;
- layers/... — результат extract, який Docker копіює частинами;
- JarLauncher — спосіб запуску вже зібраної exploded-структури в runtime.
Саме цей ланцюжок перетворює layering із «подивитися список шарів» на нормальний спосіб збирання Docker-образу.
8. Зв’язок із Dockerfile
На цьому місці дуже хочеться одразу зібрати весь layered Dockerfile — руки сверблять, Dockerfile кличе, а COPY просить бути розумним. Але спочатку важливо втримати дві опори: побачити назви шарів і навчитися їх вилучати. Без цього runtime-фрагмент лишається просто набором рядків.
Втім, корисно знати, як це виглядає в мінімальному вигляді з боку Dockerfile, бо команда extract найчастіше виконуватиметься саме під час docker build. Документація Spring Boot показує multi-stage патерн, де в builder stage робиться extract, а потім у runtime stage каталоги шарів копіюються окремо.
Мініфрагмент — лише як «скелет», без усього іншого:
FROM bellsoft/liberica-openjre-debian:25-cds AS extract
# У цьому stage ми лише "розпаковуємо за шарами", а не запускаємо застосунок
WORKDIR /builder
# Шаблон шляху зручний у навчальному проєкті: версія jar змінюється, а Dockerfile — ні
ARG JAR_FILE=build/libs/*.jar
# Кладемо jar під стабільним іменем, щоб команда RUN була передбачуваною
COPY ${JAR_FILE} application.jar
# Вилучаємо вміст за шарами в папку layers (її потім будемо COPY'ити по шарах)
RUN java -Djarmode=tools -jar application.jar extract --layers --destination layers
Тут є три принципові ідеї, які ви вже повинні впізнавати за попередніми темами курсу. По-перше, це окремий stage, тобто runtime-образ ми не «забруднюємо» логікою збирання. По-друге, jar перейменовується на стабільний application.jar, щоб команди були короткими і не залежали від версії. По-третє, --destination задає стабільний шлях, і Dockerfile стає читабельним.
Назва каталогу тут не декоративна: важливий саме стабільний шлях. Далі runtime-контейнер забирає вміст layers/... окремими COPY і стартує через JarLauncher, бо після extraction головним результатом уже стають каталоги шарів, а не вихідний файл application.jar.
Тут важливо не завчити Dockerfile напам’ять, а зрозуміти, що саме робить jarmode=tools всередині stage extract.
9. Шпаргалка за командами
Після всієї цієї теорії хочеться мати щось на кшталт «шпаргалки, але без сорому». Таблиця нижче — це саме вона: мінімум команд, максимум сенсу.
| Дія | Команда | Що отримуємо на виході | Навіщо нам це в layered image |
|---|---|---|---|
| Увійти в tools-режим і побачити підказку | java -Djarmode=tools -jar application.jar | список команд (extract, list-layers, help) | швидко зрозуміти, що jar підтримує tools і як це використовувати |
| Подивитися назви шарів | ... list-layers | список назв шарів | щоб не «вгадувати» папки в Dockerfile, а працювати за фактом |
| Вилучити шари в директорію | ... extract --layers --destination layers | папки layers/<layer>/... | щоб копіювати шари окремими COPY і точніше використовувати кеш Docker |
Якщо ви зможете відтворити ці три рядки й пояснити, що вони роблять, ви вже «в темі» і готові до наступного інженерного кроку — збирати layered Dockerfile не навмання, а з розумінням.
10. Типові помилки під час роботи з jarmode=tools
Помилка №1: запускати tools до того, як jar зібрано.
У голові це звучить логічно: «Я ж просто подивитися». На практиці java -Djarmode=tools -jar build/libs/*.jar не знайде файл або знайде не той, і ви отримаєте помилку рівня «Unable to access jarfile». Лікується не магією, а звичайною дисципліною: спочатку ./gradlew bootJar, потім tools.
Помилка №2: переплутати Maven-шлях target/*.jar із Gradle-шляхом build/libs/*.jar.
Ця помилка особливо підступна, бо ви можете копіювати приклад зі статті, і він виглядає «схожим на правду». У документації Spring Boot прямо сказано, що target/*.jar треба замінити на build/libs/*.jar, якщо ви використовуєте Gradle. Якщо цього не зробити, Docker build буде чесно шукати jar там, де його немає.
Помилка №3: забути, що list-layers нічого не розпаковує.
Інколи очікують, що після list-layers уже з’являться папки. Не з’являться. list-layers — це інспекція, читання «карти шарів». Папки створює лише extract. Добра перевірка: після list-layers у файловій системі не повинно змінитися майже нічого.
Помилка №4: не вказати --destination і потім шукати файли «десь у контейнері».
Це класична проблема не лише tools-режиму, а й будь-якого build-кроку: якщо шлях нестабільний, ви не зможете нормально писати COPY --from=.... Тому --destination layers — не прикраса, а спосіб зробити Dockerfile читабельним і передбачуваним. Документація використовує саме таку форму.
Помилка №5: сприймати jarmode=tools як runtime-налаштування застосунку.
Інколи намагаються «запустити контейнер у tools-режимі», очікуючи, що це якось прискорить або покращить роботу сервісу. Ні: tools-режим потрібен на етапі збирання образу та підготовки шарів. У runtime контейнер має запускати застосунок, а не розпаковувати самого себе. Це вже звучить як сюжет із фільму жахів про jar, який їсть сам себе.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ