Packaging Spring Boot Apps: Executable and Layered JARs
Introduction
This tutorial covers how a Spring Boot application is packaged and run outside the IDE. We will start by building the application into an executable jar and looking inside it - its folders, its manifest, and the launcher that makesjava -jar work for a jar full of other jars. We will then run the jar and configure it from the command line. Next we will look at layered jars: the layers Spring Boot records in the jar, extracting them with the jarmode=tools launcher, and why they make container images smaller to update. Lastly, we will see two ways to build a container image - a Dockerfile that uses the layers, and Cloud Native Buildpacks - and how extraction helps with faster startup through Java's AOT cache.(This tutorial packages the application from Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr.)
Building the executable jar
Thespring-boot-maven-plugin that Spring Initializr adds to every project hooks into Maven's package phase. A normal build produces the jar -
./mvnw package
first-app-0.0.1-SNAPSHOT.jar first-app-0.0.1-SNAPSHOT.jar.original
.jar.original. The executable jar is the one you ship. (The tests ran too in a normal build; they were skipped here with -DskipTests to keep the example short.)
Inside the executable jar
A selection of the jar's entries shows its layout (the full listing has hundreds of lines) - OUTPUT - selected entries of jar tf first-app-0.0.1-SNAPSHOT.jarMETA-INF/MANIFEST.MF org/springframework/boot/loader/launch/JarLauncher.class BOOT-INF/classes/application.properties BOOT-INF/classes/com/javabrahman/firstapp/FirstAppApplication.class BOOT-INF/classes/com/javabrahman/firstapp/HelloController$AppInfo.class BOOT-INF/classes/com/javabrahman/firstapp/HelloController.class BOOT-INF/lib/tomcat-embed-core-11.0.27.jar BOOT-INF/lib/spring-boot-4.1.2.jar BOOT-INF/lib/spring-webmvc-7.0.10.jar BOOT-INF/classpath.idx BOOT-INF/layers.idx
Explanation of the layout
BOOT-INF/classes/holds the application's own classes and resources - the controller, the main class (and its nested record), andapplication.properties.BOOT-INF/lib/holds every dependency as a complete, unmodified jar - 34 of them for this application, including Spring Boot, Spring MVC and embedded Tomcat. That is why the file is about 20 MB and why it is called a fat jar or uber jar.BOOT-INF/classpath.idxlists the library jars in classpath order, andBOOT-INF/layers.idxassigns every entry to a layer, as shown below.org/springframework/boot/loader/is the Spring Boot loader, copied in as plain classes at the root of the jar.
Manifest-Version: 1.0 Created-By: Maven JAR Plugin 3.5.1 Java-Version: 25 Build-Jdk-Spec: 25 Implementation-Title: first-app Implementation-Version: 0.0.1-SNAPSHOT Main-Class: org.springframework.boot.loader.launch.JarLauncher Start-Class: com.javabrahman.firstapp.FirstAppApplication Spring-Boot-Version: 4.1.2 Spring-Boot-Classes: BOOT-INF/classes/ Spring-Boot-Lib: BOOT-INF/lib/ Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Main-Class is not our main class but JarLauncher. When you run java -jar, Java starts JarLauncher, which reads the Spring-Boot-* entries, creates a class loader that can read the nested jars in BOOT-INF/lib, and then calls the main() method of Start-Class - our FirstAppApplication. (This is the LaunchedClassLoader that appeared in the log of the DevTools tutorialClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity.)
Running the jar
The jar runs anywhere a Java 25 runtime is installed. Command-line arguments become properties, as explained in application.properties vs application.ymlClick to read tutorial: application.properties vs application.yml in Spring Boot, so the same jar can be configured at launch - here with a different port and application name -java -jar target/first-app-0.0.1-SNAPSHOT.jar --server.port=9090 --spring.application.name=shop-prod
> curl http://localhost:9090/hello?name=Harry
Hello, Harry!
> curl http://localhost:9090/info
{"application":"first-app","builtWith":"Spring Boot","date":"2026-11-06"}
2026-11-06T13:43:17.140+05:30 INFO 19552 --- [shop-prod] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 9090 (http) 2026-11-06T13:43:19.650+05:30 INFO 19552 --- [shop-prod] [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 9090 (http) with context path '/'
shop-prod - both settings came from the command line, with no change to the jar. JVM options go before -jar, as in java -Xmx512m -jar app.jar; application arguments go after the jar name. To run the application as a service, a process manager such as systemd on Linux, or a container platform, starts this same command.
Layered jars
Container images are built in layers, and when an image is rebuilt, only the layers that changed have to be rebuilt, pushed and pulled. A fat jar copied into an image is one 20 MB layer, so even a one-line code change ships all 20 MB again - although the 34 library jars almost never change. Spring Boot therefore writesBOOT-INF/layers.idx into every jar, splitting its contents into four layers ordered from least to most likely to change -
OUTPUT - BOOT-INF/layers.idx- "dependencies": - "BOOT-INF/lib/" - "spring-boot-loader": - "org/" - "snapshot-dependencies": - "application": - "BOOT-INF/classes/" - "BOOT-INF/classpath.idx" - "BOOT-INF/layers.idx" - "META-INF/"
dependencies: the released library jars - large, and they change only when you upgrade a library.spring-boot-loader: the launcher classes, which change only with Spring Boot upgrades.snapshot-dependencies: libraries with-SNAPSHOTversions, which may change at any build (empty here).application: our classes and resources - small, and they change with every commit.
Extracting the layers with jarmode=tools
To use the layers, they have to be taken out of the jar. Every Spring Boot jar contains a second mode of operation for this: started with the system property-Djarmode=tools, it runs Spring Boot's jar tools instead of the application -
OUTPUT of java -Djarmode=tools -jar first-app-0.0.1-SNAPSHOT.jar helpUsage: java -Djarmode=tools -jar first-app-0.0.1-SNAPSHOT.jar Available commands: extract Extract the contents from the jar list-layers List layers from the jar that can be extracted help Help about any command
dependencies spring-boot-loader snapshot-dependencies application
extract command with --layers writes one folder per layer, and --launcher keeps the Spring Boot launcher so that the extracted application starts the same way as the jar -
OUTPUT - the layer folders, a sample from the dependencies layer, and the application layer's classesapplication dependencies snapshot-dependencies spring-boot-loader spring-boot-4.1.2.jar FirstAppApplication.class HelloController$AppInfo.class HelloController.class
BOOT-INF/classes/com/javabrahman/firstapp. A Dockerfile copies the folders into the image one by one, least-changing first, so that each becomes its own image layer -
Dockerfile - a two-stage build that copies the layers separately
FROM eclipse-temurin:25-jre AS builder
WORKDIR /builder
COPY target/first-app-0.0.1-SNAPSHOT.jar application.jar
RUN java -Djarmode=tools -jar application.jar extract --layers --launcher --destination extracted
FROM eclipse-temurin:25-jre
WORKDIR /application
COPY --from=builder /builder/extracted/dependencies/ ./
COPY --from=builder /builder/extracted/spring-boot-loader/ ./
COPY --from=builder /builder/extracted/snapshot-dependencies/ ./
COPY --from=builder /builder/extracted/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
COPY produces a new layer - a few kilobytes instead of 20 MB. (Docker was not available on the machine that ran this tutorial's examples, so the Dockerfile is shown but was not built here.)
Container images with Cloud Native Buildpacks
Writing and maintaining aDockerfile is optional. The Spring Boot Maven plugin can build an OCI container image directly, with Cloud Native Buildpacks (the Paketo buildpacks by default) -
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=javabrahman/first-app:1.0
Dockerfile. The command needs a running Docker (or compatible) daemon, which is why it is shown but not run in this tutorial. In Gradle, the task is bootBuildImage.
Extracting for faster startup
extract without --layers produces a plain application jar next to a lib/ folder of dependencies, which starts with an ordinary java -jar and no nested-jar launcher. That form is what the JVM's startup optimizations need: on Java 25, a training run records an AOT cache of loaded and linked classes, and later runs start faster from it -
java -Djarmode=tools -jar target/first-app-0.0.1-SNAPSHOT.jar extract --destination app java -XX:AOTCacheOutput=app.aot -Dspring.context.exit=onRefresh -jar app/first-app-0.0.1-SNAPSHOT.jar java -XX:AOTCache=app.aot -jar app/first-app-0.0.1-SNAPSHOT.jar
spring.context.exit=onRefresh makes the training run stop as soon as the application context has started. How the AOT cache works is explained in the tutorial on Java 24 and 25 AOT cache for faster startupClick to read: Java 24 and 25 AOT cache for faster startup, and startup tuning comes back in module 11 of this series, together with GraalVM native images.
Other packaging options
- WAR files: choosing War packaging on Spring Initializr produces a WAR that can be deployed to an external servlet container such as a standalone Tomcat, and still runs with
java -jar. Use it only when the deployment platform requires it. - Gradle:
./gradlew bootJarbuilds the same executable, layered jar. - Native images: GraalVM can compile the application ahead of time into a native executable that starts in milliseconds, with
./mvnw -Pnative native:compile; covered in module 11.
Summary
In this tutorial we first built the application with./mvnw package and saw the Spring Boot plugin repackage it into an executable fat jar. We looked inside the jar - the application in BOOT-INF/classes, 34 dependency jars in BOOT-INF/lib, the index files and the Spring Boot loader - and read the manifest that makes JarLauncher start our main class. We then ran the jar on another port with command-line properties. Next we read the four layers in layers.idx, used the jarmode=tools launcher to list and extract them, and wrote a Dockerfile that copies each layer separately. Lastly, we saw how Cloud Native Buildpacks build an image without a Dockerfile, how a plain extraction supports Java's AOT cache, and other packaging options. This completes the Spring Boot essentials module; the series continues with building REST APIs with Spring MVC.Spring Boot Tutorials
Spring Boot DevTools: Automatic Restart and ProductivityClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr How Spring Boot Auto-Configuration WorksClick to read tutorial: How Spring Boot Auto-Configuration Works Spring Boot Starters Explained with ExamplesClick to read tutorial: Spring Boot Starters Explained with Examples application.properties vs application.yml in Spring BootClick to read tutorial: application.properties vs application.yml in Spring Boot Type-Safe Configuration with @ConfigurationPropertiesClick to read tutorial: Type-Safe Configuration with @ConfigurationProperties Spring Boot Profiles and Environment-Specific ConfigurationClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration
Spring Boot DevTools: Automatic Restart and ProductivityClick to read tutorial: Spring Boot DevTools: Automatic Restart and Productivity Your First Spring Boot Application with Spring InitializrClick to read tutorial: Your First Spring Boot Application with Spring Initializr How Spring Boot Auto-Configuration WorksClick to read tutorial: How Spring Boot Auto-Configuration Works Spring Boot Starters Explained with ExamplesClick to read tutorial: Spring Boot Starters Explained with Examples application.properties vs application.yml in Spring BootClick to read tutorial: application.properties vs application.yml in Spring Boot Type-Safe Configuration with @ConfigurationPropertiesClick to read tutorial: Type-Safe Configuration with @ConfigurationProperties Spring Boot Profiles and Environment-Specific ConfigurationClick to read tutorial: Spring Boot Profiles and Environment-Specific Configuration