Container Images Without a Dockerfile: Jib and Spring Boot
Container Images: The Underestimated Build Problem
At encircle360, we have been running our Java services on Kubernetes for years -- something we already described in our very first post about Kubernetes. Containerization is part of our daily routine. But a question that generates a surprising amount of friction is not the deployment itself, but the step before it: How do you actually build a good container image for a Spring Boot application?
For a long time, the answer was simply: you write a Dockerfile. But anyone who has done this for a production Java application knows that there are many pitfalls. And since we migrated our build scripts to Gradle Kotlin DSL, we want to consolidate as much build logic there as possible -- not scatter it across separate Dockerfiles.
In this post, we compare three approaches and explain why we ultimately chose Google Jib.
The Classic Way: Dockerfile
The traditional approach looks roughly like this:
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY build/libs/my-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
This works, but has several weaknesses. First, the build machine needs a running Docker daemon -- in CI/CD environments, this means either Docker-in-Docker or a privileged container, both of which are problematic. Second, the layer structure is suboptimal: with every code change, the entire JAR is copied again, including all unchanged dependencies. For a Spring Boot application with dozens of dependencies, this JAR can easily weigh 100 MB or more. Third, reproducibility is questionable -- identical source code can produce different images depending on when the build happens, because timestamps and metadata are included.
Of course, you can improve the Dockerfile: multi-stage builds, using Spring Boot's layered JAR feature, fixing timestamps. But then you quickly end up with a 30-line Dockerfile that needs to be maintained and understood. And you still need the Docker daemon.
Spring Boot Buildpacks: bootBuildImage
With Spring Boot 2.3, an integrated approach was added that was further improved with Spring Boot 3.0: Cloud Native Buildpacks via bootBuildImage. The idea behind it is that the framework itself knows how to build an optimal image.
tasks.named<BootBuildImage>("bootBuildImage") {
imageName.set("registry.encircle360.com/my-service:${project.version}")
environment.set(mapOf(
"BP_JVM_VERSION" to "17",
"BPE_JAVA_TOOL_OPTIONS" to "-XX:MaxRAMPercentage=75.0"
))
}
The invocation is simple: ./gradlew bootBuildImage. Spring Boot uses Paketo Buildpacks to produce an OCI-compliant image. The result is well-structured with clean layer separation, and you don't need to maintain a Dockerfile.
The catch: the task requires a running Docker daemon because Buildpacks internally start Docker containers to assemble the image. Additionally, the build process is comparatively slow because the Buildpack infrastructure is spun up each time. And the configuration options are limited -- it's sufficient for standard cases, but things get cumbersome with special requirements for base images or JVM flags.
Google Jib: Container Images from the Build Tool
Jib is Google's open-source solution for exactly this problem. The project has existed since 2018 and is now mature and stable. The central idea: Jib builds OCI-compliant container images directly from the build tool -- without a Docker daemon, without a Dockerfile, without a container runtime.
The Jib Gradle plugin integrates seamlessly into the build process:
plugins {
id("com.google.cloud.tools.jib") version "3.3.1"
}
jib {
from {
image = "eclipse-temurin:17-jre-alpine"
}
to {
image = "registry.encircle360.com/my-service"
tags = setOf("${project.version}", "latest")
auth {
username = System.getenv("REGISTRY_USER")
password = System.getenv("REGISTRY_PASSWORD")
}
}
container {
jvmFlags = listOf(
"-XX:MaxRAMPercentage=75.0",
"-XX:+UseG1GC"
)
ports = listOf("8080")
labels = mapOf(
"org.opencontainers.image.source" to "https://gitea.encircle360.com/encircle360/my-service",
"org.opencontainers.image.version" to "${project.version}"
)
creationTime.set("USE_CURRENT_TIMESTAMP")
}
}
With ./gradlew jib, the image is built and pushed directly to the registry. No Docker needed. With ./gradlew jibDockerBuild, you can alternatively build into the local Docker daemon if you want to test locally.
Why Jib Wins Us Over
The reasons we use Jib for our Spring Boot services are concrete and measurable.
No Docker daemon needed. Our CI pipelines run as unprivileged jobs. No Docker-in-Docker, no mounting the Docker socket, no security risks from privileged containers. Jib communicates directly with the container registry over HTTP -- that's all it needs.
Intelligent layer structure. Jib breaks the application into multiple layers: dependencies, snapshot dependencies, resources, and classes. Only layers that have actually changed are rebuilt and pushed. For a typical code change, this affects only the classes layer -- a few kilobytes instead of dozens of megabytes. This significantly speeds up both the build and the pull in the Kubernetes cluster.
Reproducible builds. Same input produces identical output. Jib eliminates the variability that arises in Dockerfile builds from timestamps, filesystem metadata, and non-deterministic package managers. For us, this matters because we need to be able to trace which code is in which image.
Fast incremental builds. Thanks to layer decomposition and the local cache, Jib builds incremental changes in seconds rather than minutes. During development, this makes a noticeable difference.
Full Gradle integration. The entire image configuration lives in build.gradle.kts -- no separate Dockerfile, no additional toolchain. This fits perfectly with our strategy of managing build logic centrally in Gradle, as we described in our post about Gradle Kotlin DSL.
Jib in Practice: CI/CD Integration
In our CI pipelines, the build step looks remarkably simple:
./gradlew jib \
-Djib.to.image=registry.encircle360.com/my-service:${CI_COMMIT_SHA} \
-Djib.to.auth.username=${REGISTRY_USER} \
-Djib.to.auth.password=${REGISTRY_PASSWORD}
The image is built and pushed directly to the registry. Kubernetes pulls it from there during the next deployment. No intermediate Docker build, no docker push -- a single Gradle task handles everything.
For multi-module projects, we configure Jib in the root project as a convention:
subprojects {
apply(plugin = "com.google.cloud.tools.jib")
jib {
from {
image = "eclipse-temurin:17-jre-alpine"
}
to {
image = "registry.encircle360.com/${project.name}"
}
}
}
Each submodule inherits the base configuration and can override it as needed. This keeps the configuration DRY, even for services with ten or more modules.
When Jib Isn't the Right Fit
To be fair, there are scenarios where Jib is not the best choice. If you need to include complex native dependencies in the image -- such as C libraries that need to be compiled -- you still need a Dockerfile or a multi-stage build. Jib is also not an option for non-Java projects, as it is specifically designed for Java. And if you already have a sophisticated Dockerfile-based pipeline that works well, there is no compelling reason to switch.
For the typical Spring Boot service -- and that's the vast majority of our applications -- Jib is the superior solution, however.
Conclusion
Building container images sounds trivial, but it isn't -- at least not if you take reproducibility, speed, and security seriously. Google Jib solves the essential problems of the traditional Dockerfile approach and integrates perfectly into the Gradle build process. Since we started using Jib, we no longer have Dockerfiles for our Java services, our CI builds are faster, and we have the confidence that the same code always produces the same image.
If you use Spring Boot and Gradle and want to simplify your container builds, give Jib a try. Setup takes five minutes, and the benefits are immediately apparent.
Written by
Patrick HütterFounder & Software Architect
Software architect, engineer and entrepreneur. Patrick has been building products and platforms for over a decade — from enterprise backends and cloud-native infrastructure to AI-powered applications. As founder of encircle360, he combines deep technical expertise with entrepreneurial vision, driving open source projects that create real impact.
You might also like
From Manifest to Production: How ADL, A2A and the Inference Gateway Are Revolutionizing Agent Infrastructure
Jul 5, 2026 · 12 min read
Agent Orchestration with Java: Bringing LLM Agents to Production on the JVM
Jul 4, 2026 · 6 min read
Spring AI: How Java Developers Can Finally Integrate AI Features the Right Way
Mar 25, 2026 · 5 min read