Spring Boot Buildpacks: Optimize, Secure, and Shrink Your Containers!

Writing and maintaining a perfect Docker file might be timeconuming. But what if you could automate the containerization of your application and still get the optimal results? In come the build packs.

In this video, I will show you how to use and fine-tune build packs to make your Spring Boot containers faster, smaller, and more secure. All of that without the hassle of writing a Docker  file. Imagine that building a container image is like baking waffles. So in real life to bake waffles you need flour, you need eggs, sugar, butter, you need to measure these ingredients, mix them, then you pour the batter and you bake a waffle. Build packs are like high quality ready to make waffle mix. And I mean it. No additives, only natural ingredients, only the best for you. But it is a ready mix. So all you have to do is to add some milk, put the batter into the waffle iron and press the button and your perfect waffles are ready.

So that's what build packs do for your application. They take the source code of your application and turn it into a production ready container image. But what happens under the hood? Let's break it down. So build packs analyze your code. Then they figure out which ingredients are necessary and start baking a container image. There are no Docker files, no manual management. All you have to do is press the button or run one command. Build packs have been around for quite a while. Hioku introduced them in 2011. In 2018, Hioku teamed up with Pivotal and that's how cloudnative build packs were born. Cloud native build packs are an open-source standard and it's now part of the CNCF cloud native computing foundation. One of the most uh popular implementations of this standard is Pquetto build packs. They are supported by cloud foundry and VMware. So how do build packs work under the hood?

They go through two main phases detect and build. During the detect phase, build packs inspect your code like a detective searching for clues. If they recognize a language or framework, then they proceed to building a container image. So that's the build phase. During the build phase, build packs patch necessary dependencies, builders. They compile your code if it is necessary and make the container image ready for production. So as a result you get a clean, secure and production ready container image which can be run on any cloud. And you know what getting this production ready container is just a matter of one command. So if you use Java and you use one of the build systems like Maven or Gradle, it means that you can simply run Maven Springwood build image or Gradle boot build image and that's it. To make this magic work, build packs use builders. Think of these builders as preconfigured toolboxes. A builder includes the following components. A set of build packs to analyze and build the application.

A stack, a build image and a run image, and a life cycle manager that assembles everything into the final container image. So it means that one builder can handle different applications. So forget about Docker files. Just push your application and build packs will do all the heavy lifting. For cloudnative applications, build packs can save your time by optimizing and automating the process of building container images. So let's look at the process of building a container image. So as I already said, all you need is just one command. So I use Maven. So I will run maven spring boot build image and that's it. The process has started and it is very detailed as you can see. Let's wait for a  bit. Okay, the image is ready, right? Build success.

Let's look closely at what is happening under the hood. So let's roll up a little bit. This is our command scanning for projects. Our build pack found the project springpad clinic and it's building it from pom.xml. Then you can see how the project is built, right? You can see all the details. You can see here several stages analyzing, detecting means that there are several builds that take part in building the image. There are CA certificates. Bellsoft libera libera JDK is used by default in paketa build packs for spring boot apps. You can see sift built pack. This one is for creating nsbomb executable jar zans spring boot finally. And then later you can see the configuration of your image. There's the build configuration.

So these are vp embed certificates if you want to embed the certificates into the image. AP enable runtime certificate binding right certificate binding disabled. Then there's spec build pack for bells of libera and uh build configuration for JDK. We have a bunch of options that you can play around with. I'll show you later in the video what you can do. So there is a BP JVM JLink arguments and BP JVM G-link enabled. This is the tool for cutting out a custom JRE to cut down the container image size. There's the PP JVM type. In our case, it's JRE because we need only JRE for the application to run. And it's all chosen for you automatically, right? So, you don't have to specify that explicitly in the Docker file. AP JVM version. In this case, it's 21, the latest LTS version. Then there is the launch configuration that uh we can also tune. So, there is the debug enabled uh debug port to enable debugging your image. There is the Java native memory tracking enabled and level. There is the JFR arguments and JFR enabled. That's for enabling the profiling of your application in the container image with the help of JFR which is part of JVM. Then there is the Jmix enabled and JMX port. This is uh if you want to monitor your application behavior in real time and uh some other options like uh Java tool options. This is if uh you want to specify some JVM arguments. Then what do we have? There's the Paketto build pack for spring boot and options that you can adjust for spring boot. There is the JVM CDS enabled and spring AOT enabled.

This is for cutting down the startup time of your application. And uh there are some other options like cloud bindings. Then important thing is creating a layer jar. Build packs create a layer jar of a spring boot application by default. What does it mean? It means that the application is divided into several layers that is dependencies, spring boot loader, snapshot dependencies and application. As a result, if you want to update your image, if you want to for instance change something in your application, then when you rebuild the image, only the application layer will be changed and other layers will be pulled from the cache and that can greatly accelerate uh the build. These layers are created by default, but you can also create your custom layers. And then there are layers for build packs, right? So certificates, libera, executable jar, spring boot, so on so forth. And then there is the sbomb. An asbomb is created automatically when you build a container image which is super convenient because software builds of material are very important and we don't have to look for ways to generate an bomb. It's created automatically with build packs and it is available in the container image.

So you can inspect it later or pull it from the container image. And that's it. That's how our container image was built. Even if you use default settings, you can still get a great container image. But in some cases, the default settings might not be enough. And you want to change something. you want to change the JVM arguments or you want to enable profiling or debugging or you want to cut out the custom JRE and so on so forth. In this case, we can adjust the build pack settings and actually it is quite easy to do. I'll show you how to do that right  now.

So, first thing first, you can configure the JVM. You can choose uh a JVM version or a JVM type. For that purpose, you need to add an environmental variable to your build pack configuration. So we will look at Maven and Gradle. With Gradle, it's a little bit more laconic. With Maven, you will have to add a couple of additional sublocks in your pom.xml file, but well, nothing fancy. So with Gradle you just need to specify the environment variable BPJ JVM version and with Maven you need to add the configuration block and here you can specify the variable also BPJ JVM version for instance 17 or 21 right whichever you choose you can also choose the JVM type so by default build packs use JDK to build the application and containerize it and JRE to run the application. ation in a container. For some reason, you might want to use JDK in a container.

So in this case, you can specify that. There are also several JDKs available if you don't want to use the default builder. So you can choose any of them. You can also configure the JVM. So all you need is Java tool options variable for Maven and Parlu. And here you can specify the necessary JVM settings. For instance, you can choose another garbage collector, set the garbage collector arguments, the heap size, and so on and so forth. What about the image size? Newer Spring Boot versions use jammy tiny by default for running the application. It doesn't contain a shell and the resulting container image is quite small. You can make it even smaller if you choose another builder. You can use for instance Alpakita build packs. It is based on library GDK light and minimalistic alpakita Linux. In this case, your container image will be really small.

On the other hand, you may need some additional libraries that are absent in striped down or minimalistic Linuxes. For instance, you want to use a sync profiler and it requires lip std C++ library to run and it is absent uh in the build packs that I mentioned. In this case, you can use build packs jam base which was used previously in build packs. the container image will be bigger but in this case you will have a shell and you will also have this library and maybe some other tools that you require. Another way of cutting down the container image size is to use JLink. JLink cuts out a custom JRE that contains only modules required by your application to run. Using JLink is super simple with build packs.

You only need one option, BP JVM JLink enabled, and that's basically it. The tool will automatically scan your project and choose the modules that your application acquires. Of course, you can add these modules manually with another option for JLink arguments, but in most cases, uh using only one option to enable JLink will be enough. What if you want to cut down the startup and warm up times of your container images? In this case, you can use CDS and Spring AOT. CDS creates an archive of JVM and application classes. So, the JVM can read this archive instead of loading the classes from scratch. And Spring AoT offers ahead of time compilation of some code that can also be added at runtime. And uh so it reduces the startup even more.

Again, using CDS and AOT is super simple with build packs. You only need to specify a goal process AOT and then two options, BP spring AOT enabled and BP JVM CDS enabled. And that's it. You build the container image and it will start faster. How fast? Well, that depends on your application. In general, CDS and AOT provide uh 50 or even 60% better startup time. What if you want your application to start up almost instantly? In this case, you can use GrowlVM native image. Yes, you can use native image with build packs too. All you need is to add a plug-in growvm build tools and that's it. Of course, you can customize the native image too, just like with the regular JVM, but in this case, we can build a native image with the default settings.

And then you run a Maven Spring Boot build image P native. That's important. And with Gradle, the command is the same. Gradle boot build image. And voila, you have a containerized native image. How cool is that? Okay. What if you want to profile your application? In this case, we can use JFR. And there are settings that are available for JFR with build packs. I have already shown them in the terminal. So let's see how we can enable profiling with JFR and build packs. There are two ways of enabling JFR at build time and at runtime. First of all, we need to add a couple of JVM options. We need the option unlock diagnostic VM options and debug non-safe points. In this case, the profiling will be more complete.

So if you want to enable profiling at runtime, then that's it. You are building the container image and when you start your image, you specify the JFR arguments that is BPL JFR enabled set to true. And you can also specify the JFR arguments if you need to like the duration of the profiling, the name of the file, and so on. You can also enable profiling at build time. For that purpose, you need to add additional JVM option that is start flight recording. And here you specify the necessary arguments. After that, you build your container image and when you start it like uh you usually do the JFR will start with your application. If you want to monitor your application in real time by using Java mission control for instance, you can enable JMX. In this case, you also have two options. You can enable it at build time and at runtime. So if you want to enable it at runtime, all you need to do is specify BPL Jmix enabled set to true and BBL Jmix port. So the application will be available at local host.  

But what if you want to specify another host name? So for instance, the application is not running on your local machine, but it's running in production and the host is obviously different. In this case, you will have to specify a couple of additional options in your configuration file. So, you manually specify JMX remote. You specify the required host name, the port authentification and uh SSL configuration necessary. And then when you start your container image without any additional options, the JMIX will be enabled and you can access your application at the host that you specified.

And finally, a little tip, native memory tracking is enabled by default. So when you start application you will see a bunch of uh locks and when you stop your container image there uh is going to be statistics on the memory consumption of your application which is kind of cool right but in some cases we don't need uh native memory tracking running in production so you can disable it in this case you need to start your container image and specify the option ppl java nmt enabled set to false and that's it the native memory tracking will be  disabled. Today we optimized the performance of our container images, reduced their size and discovered that there is an SPOM that is created automatically and that resides in the container image. Cool. Well, now it's your turn. Test these tweaks on your application and don't forget to like and subscribe. Until next time.

Summary

In this video, the process of containerizing Spring Boot applications using build packs is explained as a faster and more secure alternative to writing Dockerfiles. Build packs analyze the application source code and automatically generate a production-ready container image, handling all dependencies and configurations under the hood. The video demonstrates how to fine-tune build packs by adjusting JVM settings, enabling profiling (JFR), monitoring (JMX), and reducing image size using JLink, CDS, and Spring AOT. It also covers how to generate a native image with GraalVM and how to customize build behavior using environment variables in Maven or Gradle. Additionally, build packs create an SBOM (Software Bill of Materials) automatically, helping with security and compliance. Overall, build packs simplify containerization while giving developers flexibility to optimize performance and image size.

About Catherine

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

Social Media

Videos
Apr 1, 2025
SBOMs & Java Security: Stay Compliant, Stay Protected!

A software bill of materials or SBOM is crucial for the modern development environment full of regulations, vulnerabilities, and hidden threats. In this video, we discuss what SBOMs are and how they help dealing with compliance audits and application security. We will explore the best open-source tools for generating SBOMs and provide a tutorial on creating an SBOM for your project and analyzing it for vulnerabilities.

Videos
card image
Mar 21, 2025
Async Profiler: Uncover Hidden Performance Issues in Java!

Let's discuss Async Profiler, the small and effective open-source low-overhead profiler for Hotspot JVM-based application. Itcan collect data on various JVM events like CPU usage, heap allocation, methods, and so on. It can monitor non-Java threads, native calls, and kernel functions. It is small, silent, and incredibly effective!

Further watching

Videos
card image
Jun 23, 2025
How to install Liberica Native Image Kit on Windows PC

Liberica Native Image Kit is a multilingual GraalVM-based set of utilities for creating native images. This guide will help you to install it on Windows PC.

Videos
card image
Apr 25, 2025
Java in 2025: Busting the Biggest Myths

Think Java is slow, outdated, or only for old-school enterprise apps? Think again. In this episode of Java Myth Busters, we debunk six common myths about Java, including performance issues, security concerns, and the myth that Java is dead. Discover how the JVM, Spring Boot, GraalVM, and modern JDK updates make Java one of the most powerful, scalable, and relevant languages in 2025.

Videos
card image
Apr 1, 2025
SBOMs & Java Security: Stay Compliant, Stay Protected!

A software bill of materials or SBOM is crucial for the modern development environment full of regulations, vulnerabilities, and hidden threats. In this video, we discuss what SBOMs are and how they help dealing with compliance audits and application security. We will explore the best open-source tools for generating SBOMs and provide a tutorial on creating an SBOM for your project and analyzing it for vulnerabilities.