Spring Boot Dockerfile with Layered Jar (Multi-Stage, JRE 21)

This Spring Boot Dockerfile extracts your executable jar into layers so dependencies, the Spring Boot loader and your own classes become separate image layers. Code-only changes then rebuild and push just the small application layer. The app runs on eclipse-temurin:21-jre-alpine as a non-root user on port 8080.

CI status
Build passing
Image size (uncompressed)
227.8 MB
Build time
71 s
Runtime image
eclipse-temurin:21-jre-alpine
Port
8080
Last built
2026-10-15
Dockerfile
# syntax=docker/dockerfile:1
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn -B -q dependency:go-offline
COPY src ./src
RUN mvn -B -q package -DskipTests \
 && cp target/*.jar app.jar \
 && java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted

FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
RUN addgroup -S spring && adduser -S spring -G spring
COPY --from=build /app/extracted/dependencies/ ./
COPY --from=build /app/extracted/spring-boot-loader/ ./
COPY --from=build /app/extracted/snapshot-dependencies/ ./
COPY --from=build /app/extracted/application/ ./
USER spring:spring
EXPOSE 8080
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]
.dockerignore
.git
target/
.idea/
*.iml
.env
Dockerfile
.dockerignore

Before you build

How this Dockerfile works

  1. FROM maven:3.9-eclipse-temurin-21 AS build

    Maven and a full JDK 21 compile and package the app. This stage is discarded once the layers are extracted.

  2. COPY pom.xml . RUN mvn -B -q dependency:go-offline

    Resolving dependencies from pom.xml alone caches the slow download step until the dependency list changes.

  3. && java -Djarmode=tools -jar app.jar extract --layers --launcher --destination extracted

    Spring Boot's jar tools split the fat jar into dependencies, spring-boot-loader, snapshot-dependencies and application directories.

  4. FROM eclipse-temurin:21-jre-alpine

    The runtime needs only a Java runtime. The Alpine JRE image is much smaller than the JDK build image.

  5. COPY --from=build /app/extracted/dependencies/ ./

    Third-party libraries change rarely, so copying them first gives a large layer that stays cached across most rebuilds.

  6. COPY --from=build /app/extracted/application/ ./

    Your own classes and resources are copied last. A typical code change only rebuilds and pushes this small layer.

  7. ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

    Starts the app from the extracted layout with Spring Boot's launcher class, instead of running java -jar on a fat jar.

Build and run

terminal
docker build -t spring-boot-app .
docker run --rm -p 8080:8080 spring-boot-app

Set profiles and secrets at runtime, for example with -e SPRING_PROFILES_ACTIVE=prod. Rebuild after a small code change and watch Docker reuse the cached dependency layers.

Other Spring Boot Dockerfile variants

The plain fat-jar variant is shorter and easier to read. The layered variant pays off for larger apps that are rebuilt and pushed often, because unchanged dependency layers are reused by the registry and every node that pulls the image.

VariantRuntime imageImage sizeCI
Layered jar (Maven + JRE 21)eclipse-temurin:21-jre-alpine227.8 MBPassing
Maven build + Temurin 21 JRE Alpineeclipse-temurin:21-jre-alpine227.6 MBPassing

Troubleshooting

Unsupported jarmode 'tools'
Your Spring Boot version is older than 3.3. Upgrade Spring Boot, or use the older layertools jarmode with its extract command instead.
Error: Could not find or load main class org.springframework.boot.loader.launch.JarLauncher
The launcher class moved to a new package in recent Spring Boot versions. On older versions use org.springframework.boot.loader.JarLauncher in the ENTRYPOINT instead.
cp: target/*.jar matches several files
Your build produces more than one jar, for example a sources jar. Set finalName in pom.xml or copy the exact artifact name instead.

FAQ

What do layered jars actually save?
When only your code changes, Docker rebuilds and pushes the small application layer, while the large dependency layers are reused. That speeds up CI builds and deployments.
Is the snapshot-dependencies layer needed?
It holds SNAPSHOT versions, which change more often than releases. Keeping them separate stops snapshots from invalidating the stable dependencies layer.
Can I combine this with a native image?
No. Native images compile the app into a single executable without a JVM, so there are no jar layers to extract. That approach needs a different Dockerfile.