1. Practical boundary and baseline
When we hear the word “containers,” the brain immediately wants everything at once: Dockerfile, Compose, registry, Kubernetes, monitoring, secrets, memory limits... and suddenly you seem to have wanted to “just package the jar a little better,” but in fact you are building a small data center in the kitchen. That is why we need an honest boundary: the minimum set of knowledge and actions that already makes a Boot service container-friendly, but does not turn this course into an infrastructure course.
By this point, we already have the whole packaging chain at the Boot level: the jar is built, the layers are visible, and the image can be obtained without a manual Dockerfile. Now it is important to pin down what of this really belongs in the normal baseline and what already belongs to the neighboring infrastructure track.
In engineering terms, a baseline is a set of things that “good enough” solves the task in the current context. In our context, the task is not “learn Docker,” but “understand what Spring Boot already does for container packaging at the artifact level, and be able to use it.”
The problem is not that an executable jar is bad. The problem is that for repeated packaging it is too coarse a unit if the whole service is shipped as one monolith. So today’s baseline is very down to earth: make different parts of the artifact visible — dependencies, loader, and the application itself — so that repackaging does not repeat unnecessary work.
2. Artifact chain: code → image
If up to this day we thought “I have a project → I have a jar,” then now it is more correct to think “I have a project → I have several representations of the same artifact.” This is not philosophy for philosophy’s sake: it directly affects image rebuild speed and how easy it is to explain what is actually inside.
It is convenient to keep one chain in mind:
flowchart LR A["Source files catalog-service"] --> B["bootJar: executable jar"] B --> C["Layered jar: layers.idx + standard layers"] C --> D["jarmode=tools: list-layers / extract --layers"] C --> E["bootBuildImage: OCI image via buildpacks"]
The point of this chain is not that we suddenly have five different technologies. It is the same packaging line, just at different levels of detail. bootJar provides the runnable baseline. Layered jar makes the artifact structure explicit. jarmode=tools lets you check that structure manually. bootBuildImage uses the same artifact as input and turns it into an image.
The four standard groups — dependencies, spring-boot-loader, snapshot-dependencies, and application — are needed not for beauty, but so that what changes rarely does not travel every time together with your fresh fix.
3. Jar-first on Java 25: what does not change
Here is an idea that often surprises beginners: container-friendly packaging almost never requires changing application code. Today we are not rewriting the controller, not changing CatalogProperties, not touching Actuator, and not adding “container annotations” (fortunately, they do not exist). We are changing the shape of the artifact and the way build tools read it.
In Gradle terms, everything looks even a little boring: we still have bootJar, and it still places classes in BOOT-INF/classes, and dependencies in BOOT-INF/lib. Layered jar just makes this structure explicit for packaging tools.
It is also important separately that the Java 25 baseline here does not cancel the jar-first approach. We remain in the normal JVM world: the executable format is a jar, the application runs on the JVM, and packaging layers help this JVM life survive the later container packaging normally. Nice extras like a friendly layout for the AOT cache or CDS exist, but today the discipline of packaging itself is enough for us.
To put it more simply: we stay on the ground, we just learn how to pack a suitcase neatly instead of building a spaceship.
4. Minimal command map
When a topic becomes large, a small rule helps: I must be able to name four actions and explain why each one exists. For today, that is more than enough.
| What you need to know how to do | How we do it | Why this is needed at all |
|---|---|---|
| Build an executable jar | ./gradlew bootJar | get a basic runnable artifact |
| Inspect the layers | java -Djarmode=tools -jar <bootJar-jar> list-layers | make sure the jar is actually layered |
| Unpack the jar by layers | java -Djarmode=tools -jar <bootJar-jar> extract --layers --destination ... | see the contents of dependencies, application, and the other layers |
| Build an image using Boot tools | ./gradlew bootBuildImage | turn the same artifact into an OCI image via buildpacks |
This set is enough to avoid confusing today’s baseline with a separate Docker course. The only practical detail is this: you need to look specifically at the executable jar from bootJar, not *-plain.jar, and for bootBuildImage you need an available container runtime.
5. The boundary of the day: where we stop
At the level of the current Boot course, the container-friendly baseline is covered if three things are true:
- you understand why layered jar splits the artifact by change frequency;
- you can manually verify these layers through tools mode;
- you understand that bootBuildImage builds an image on top of the same jar through buildpacks.
What we consciously do not do right now: we do not design a Dockerfile as a separate engineering artifact, we do not discuss image publishing and registry, we do not touch Compose, networking, memory, volumes, secrets, and the rest of the operations kitchen, and we do not break down the internals of buildpacks step by step.
There is also a useful engineering caveat: buildpacks may normalize file metadata for reproducibility and caching. So the application is better off not relying on file timestamps as part of its logic.
At this level, the template is already mature enough: it can live outside the IDE, its structure can be checked, and if necessary it can be packaged into an image. From here on, the main question is no longer about another packaging method, but about the reliability of this template: does it start consistently, does it break when configuration or profiles are changed, and does wiring hold up. Compose, registry, and other operational concerns remain part of a separate Docker track; within the Boot approach, regression safety comes first.
6. Typical mistakes in container-friendly packaging
Mistake #1: assuming “the image was built” = “containerization is done”.
bootBuildImage really can build an OCI image, but after that a different world begins: how to run it, how to pass configuration in, how to set up networking, how to manage logs and security. Today’s baseline is about packaging and artifacts, not operations.
Mistake #2: trying to “fix container-friendliness” by editing the controller/service.
A very common beginner reaction: “Since this is about containers, I need to add some code.” No. The point of layered jar is that the split happens at the packaging level: dependencies and the application are arranged so that they are easy to cache and copy layer by layer. The application logic of catalog-service does not change because of that.
Mistake #3: confusing application mode and tool mode.
jarmode=tools is needed to inspect and extract layers. The service itself does not stop being a normal Boot application that lives as an executable jar and runs via java -jar or gets placed into an image.
Mistake #4: turning off the layered baseline without a reason.
In the current Boot baseline, layered jar is the default behavior of bootJar. Explicit configuration is needed when you want to consciously lock in or customize this behavior, not because “it feels safer” or “there are fewer files.”
Mistake #5: removing tools support from the archive too early.
If you remove tools from the jar, for example with includeTools = false, you cut off the most convenient way to manually verify layers. This only makes sense intentionally and for a clear reason, not just “to make it cleaner.”
GO TO FULL VERSION