Dockerize Spring Boot Wisely: 6 tips to improve the container images of your Spring Boot apps

 

Transcript

Your Spring Boot applications deserve a top-notch package! And so in this video, I will give you six tips to improving the container images for your Spring Boot apps. You may already be using some of them. Some of them may be new to you. But the best thing about all of these solutions is that they are compatible with any application. Plus, they are super easy to implement, so you can try them out right away.

Use Docker multi-stage builds

Each command in a Docker file creates a new layer. If all of this layers are added to the final image, it can get bloated. To keep your final images clean, you can use Docker multi-stage builds. They enable you to use several FROM statements in a Docker file. Each FROM statement uses its own base image, and you can copy only those artifacts from the previous stage that you need. The final layer won't contain the layersfrom the parent images, tools required for building the application, or files that weren't explicitly copied. As a result, the final image will be smaller.

Without multi-stage build:

FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder

WORKDIR /app

ADD spring-petclinic-main /app/spring-petclinic-main

RUN cd spring-petclinic-main && ./mvnw clean package

EXPOSE 8081

ENTRYPOINT java -jar /app/spring-petclinic-main/target/spring-petclinic*.jar

Resulting container size: 368MB

With multi-stage build:

FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder

WORKDIR /app

ADD spring-petclinic-main /app/spring-petclinic-main

RUN cd spring-petclinic-main && ./mvnw clean package

FROM bellsoft/liberica-runtime-container:jre-21-slim-musl

WORKDIR /app

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "-XX:MaxRAMPercentage=80.0", "/app/petclinic.jar"]

COPY --from=builder /app/spring-petclinic-main/target/*.jar /app/petclinic.jar

Resulting container size: 195MB

Choose a small base image

Reducing the size of container images will help you to reduce the drive space and network traffic in your clusters. So the easiest way to cut down the size of your container images is to use a base image based on a minimalistic Linux distribution. There are several popular Linux distributions for the cloud, and their size may vary. For example, let's compare the size of two container images. One of them is based on Liberica JDK Standard and Debian. Liberica JDK is recommended by Spring, so I will be using it as an example.

The container image with Debian:

FROM bellsoft/liberica-openjdk-debian:21 as builder

WORKDIR /app

ADD spring-petclinic-main /app/spring-petclinic-main

RUN cd spring-petclinic-main && ./mvnw clean package

FROM bellsoft/liberica-openjre-debian:21

WORKDIR /app

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "-XX:MaxRAMPercentage=80.0", "/app/petclinic.jar"]

COPY --from=builder /app/spring-petclinic-main/target/*.jar /app/petclinic.jar

The container image with Debian takes 386MB.

And now let's use Liberica Runtime Container based on Liberica JDK Lite optimized for the cloud and minimalistic Alpaquita Linux.

The container image with Liberica JDK Lite and Alpaquita Linux:

FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder

WORKDIR /app

ADD spring-petclinic-main /app/spring-petclinic-main

RUN cd spring-petclinic-main && ./mvnw clean package

FROM bellsoft/liberica-runtime-container:jre-21-slim-musl

WORKDIR /app

EXPOSE 8081

ENTRYPOINT ["java", "-jar", "-XX:MaxRAMPercentage=80.0", "/app/petclinic.jar"]

COPY --from=builder /app/spring-petclinic-main/target/*.jar /app/petclinic.jar

This container image takes 195MB, which is almost two times smaller than the first image.

Opt for a layered JAR for efficient caching

A traditional way of building Spring Boot container images is to use a fat jar Av fat jar contains the application with all of its dependencies. But each time we change the application, we need to build a new artifact. Plus, pulling the container images of the application with all of its dependencies may take time. Spring Boot makes it possible to create layered jars. In a layer jar the application and its dependencies are stored in different layers. The most frequently updated layers, such as the application layer, are placed on the top in the container image. So every time you introduce changes to the application, only this layer is changed and others are pulled from the cache, and so the updates will be faster.

FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as builder

WORKDIR /home/app

ADD spring-petclinic-main /home/app/spring-petclinic-main

RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package

FROM bellsoft/liberica-runtime-container:jdk-21-stream-musl as optimizer

WORKDIR /home/app

COPY --from=builder /home/app/spring-petclinic-main/target/*.jar petclinic.jar

RUN java -Djarmode=layertools -jar petclinic.jar extract

FROM bellsoft/liberica-runtime-container:jre-21-stream-musl

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

COPY --from=optimizer /home/app/dependencies/ ./

COPY --from=optimizer /home/app/spring-boot-loader/ ./

COPY --from=optimizer /home/app/snapshot-dependencies/ ./

COPY --from=optimizer /home/app/application/ ./

Upgrade the JDK and Spring Boot versions

Java Virtual machine, the heart of the Java platform, is getting more powerful with each JDK release. Brand new features, improved features, numerous enhancements to the garbage collection, JIT compiler, and the intrinsics can make your application run faster even without code changes. Spring Boot also gets numerous improvements and new features with each release. For instance, Spring Boot 3 includes baked-in support for GraalVM Native Image, Spring Boot 3.3 includes support for AppCDS for faster startup, and Spring Boot 3.4 offers a smaller base image for Buildpacks. These are only few examples of how your favorite framework can help you boost the performance, and reduce the memory footprint of your container images, so updating the JDK and Spring Boot versions is essential to keep up with the performance requirements for modern applications.

Enable AOT and CDS for faster startup

If application startup time is critical for you, consider using Application Class Data Sharing and Ahead-of-time processing. AppCDS and AOT enable up to 50% faster startup of applications, and there are no need for code changes. Take Spring Petclinic, for example. It starts in a little bit more than 4 seconds on my machine in the container image, but with AppCDS and AOT enabled, it starts in less than 3 seconds. Note that the container with CDS enabled will be bigger than the usual one. Plus, if you need more drastic startup reduction, consider using GraalVM Native Image or Coordinated Restore at Checkpoint. Writing Dockerfiles is difficult and error-prone, and plus, maintaining them might be challenging. Besides, there is a risk of using outdated versions of base images if you don't update the Dockerfile on a regular basis. Buildpacks can help you solve these issues. They turn the application source code into a production-ready container image, and all you have to do is run just one command. And Buildpacks always use the latest version of software for base images, so you will always be up to date. And by the way, Buildpacks for Spring Boot automatically create layered JARs, so you don't have to do it manually. But of course you can configure Buildpacks, it is really easy to do. For instance, if you want to use AppCDS and AOT processing, all you have to do is to specify these two features in a configuration file, and that’s it! The Buildpacks will do everything for you.

FROM bellsoft/liberica-runtime-container:jdk-21-crac-cds-musl as builder

WORKDIR /home/app

ADD spring-petclinic /home/app/spring-petclinic-main

RUN cd spring-petclinic-main && ./mvnw -Dmaven.test.skip=true clean package

FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl as optimizer

WORKDIR /app

COPY --from=builder /home/app/spring-petclinic-main/target/*.jar petclinic.jar

RUN java -Djarmode=tools -jar petclinic.jar extract --layers --launcher

FROM bellsoft/liberica-runtime-container:jdk-21-cds-slim-musl

ENTRYPOINT ["java", "-Dspring.aot.enabled=true", "-XX:SharedArchiveFile=application.jsa", "org.springframework.boot.loader.launch.JarLauncher"]

COPY --from=optimizer /app/petclinic/dependencies/ ./

COPY --from=optimizer /app/petclinic/spring-boot-loader/ ./

COPY --from=optimizer /app/petclinic/snapshot-dependencies/ ./

COPY --from=optimizer /app/petclinic/application/ ./

RUN java -Dspring.aot.enabled=true \

-XX:ArchiveClassesAtExit=./application.jsa \

-Dspring.context.exit=onRefresh org.springframework.boot.loader.launch.JarLauncher

In this video, we’ve looked at different approaches for optimizing the Docker container images for Spring Boot applications. If this video was useful to you, don't forget to like it, subscribe to our channel. And until next time!

Summary

This video provides six tips for optimizing Docker container images for Spring Boot applications, including using Docker multi-stage builds to reduce image size, choosing minimal base images, and leveraging layered JARs for efficient caching. It also highlights the benefits of upgrading JDK and Spring Boot versions for better performance, enabling AOT and CDS for faster startup, and using Buildpacks to automate image creation and maintenance. By applying these strategies, developers can create leaner, more efficient, and high-performing Spring Boot container images.

About Catherine

Java developer passionate about Spring Boot. Writer. Developer Advocate at BellSoft

Social Media

Tags

Videos
card image
Jan 13, 2026
Hibernate: Ditch or Double Down? When ORM Isn't Enough

Every Java team debates Hibernate at some point: productivity champion or performance liability? Both are right. This video shows you when to rely on Hibernate's ORM magic and when to drop down to SQL. We walk through production scenarios: domain models with many-to-many relations where Hibernate excels, analytical reports with window functions where JDBC dominates, and hybrid architectures that use both in the same Spring Boot codebase. You'll see real code examples: the N+1 query trap that kills performance, complex window functions and anti-joins that Hibernate can't handle, equals/hashCode pitfalls with lazy loading, and practical two-level caching strategies. We also explore how Hibernate works under the hood—translating HQL to database-specific SQL dialects, managing sessions and transactions through JDBC, implementing JPA specifications. The strategic insight: modern applications need both ORM convenience for transactional business logic and SQL precision for data-intensive analytics. Use Hibernate for CRUD and relationship management. Use SQL where ORM abstractions leak or performance demands direct control.

Videos
card image
Dec 30, 2025
Java in 2025: LTS Release, AI on JVM, Framework Modernization

Java in 2025 isn't about headline features, it's about how production systems changed under the hood. While release notes focus on individual JEPs, the real story is how the platform, frameworks, and tooling evolved to improve stability, performance, and long-term maintainability. In this video, we look at Java from a production perspective. What does Java 25 LTS mean for teams planning to upgrade? How are memory efficiency, startup time, and observability getting better? Why do changes like Scoped Values and AOT optimizations matter beyond benchmarks? We also cover the broader ecosystem: Spring Boot 4 and Framework 7, AI on the JVM with Spring AI and LangChain4j, Kotlin's growing role in backend systems, and tooling updates that make upgrades easier. Finally, we touch on container hardening and why runtime and supply-chain decisions matter just as much as language features.

Further watching

Videos
card image
Jan 29, 2026
JDBC Connection Pools in Microservices. Why They Break Down (and What to Do Instead)

In this livestream, Catherine is joined by Rogerio Robetti, the founder of Open J Proxy, to discuss why traditional JDBC connection pools break down when teams migrate to microservices, and what is a more efficient and reliable approach to organizing database access with microservice architecture.

Videos
card image
Jan 27, 2026
Sizing JDBC Connection Pools for Real Production Load

Many production outages start with connection pool exhaustion. Your app waits seconds for connections while queries take milliseconds; yet, most teams run default settings that collapse under load. This video shows how to configure connection pools that survive real production traffic: sizing based on database limits and thread counts, setting timeouts that prevent cascading failures, and implementing an open source database proxy Open J Proxy for centralized connection management with virtual connection handles, client-side load balancing, and slow query segregation. For senior Java developers, DevOps engineers, and architects who need database performance that holds under pressure.

Videos
card image
Jan 20, 2026
JDBC vs ORM vs jOOQ: Choose the Right Java Database Tool

Still unsure what is the difference between JPA, Hibernate, JDBC, or jOOQ and when to use which? This video clarifies the entire Java database access stack with real, production-oriented examples. We start at the foundation, which is JDBC, a low-level API every other tool eventually relies on for database communication. Then, we go through the ORM concept, JPA as a specification of ORM, Hibernate as the implementation and extension of JPA, and Blaze Persistence as a powerful upgrade to JPA Criteria API. From there, we take a different path with jOOQ: a database-first, SQL-centric approach that provides type-safe queries and catches many SQL errors at compile time instead of runtime. You’ll see when raw JDBC makes sense for small, focused services, when Hibernate fits CRUD-heavy domains, and when jOOQ excels at complex reporting and analytics. We discuss real performance pitfalls such as N+1 queries and lazy loading, and show practical combination strategies like “JPA for CRUD, jOOQ for reports.” The goal is to equip you with clarity so that you can make informed architectural decisions based on domain complexity, query patterns, and long-term maintainability.