Container Images ohne Dockerfile: Jib und Spring Boot
Container Images: Das unterschätzte Build-Problem
Wir bei encircle360 betreiben unsere Java-Services seit Jahren auf Kubernetes -- das haben wir schon in unserem allerersten Beitrag zu Kubernetes beschrieben. Containerisierung ist für uns Alltag. Aber eine Frage, die überraschend viel Reibung erzeugt, ist nicht das Deployment, sondern der Schritt davor: Wie baut man eigentlich ein gutes Container Image für eine Spring-Boot-Anwendung?
Lange war die Antwort schlicht: Man schreibt ein Dockerfile. Aber wer das schon einmal für eine produktive Java-Anwendung gemacht hat, weiß, dass es dabei viele Stolperfallen gibt. Und seit wir unsere Build-Skripte auf Gradle Kotlin DSL migriert haben, wollen wir möglichst viel Build-Logik dort konsolidieren -- nicht in separaten Dockerfiles verstreuen.
In diesem Beitrag vergleichen wir drei Ansätze und erklären, warum wir letztlich bei Google Jib gelandet sind.
Der klassische Weg: Dockerfile
Der traditionelle Ansatz sieht in etwa so aus:
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
COPY build/libs/my-service.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
Das funktioniert, hat aber mehrere Schwächen. Erstens muss auf der Build-Maschine ein Docker Daemon laufen -- in CI/CD-Umgebungen bedeutet das entweder Docker-in-Docker oder einen privilegierten Container, beides problematisch. Zweitens ist die Layer-Struktur suboptimal: Bei jeder Code-Änderung wird das gesamte JAR neu kopiert, inklusive aller unveränderten Dependencies. Bei einer Spring-Boot-Anwendung mit dutzenden Abhängigkeiten kann dieses JAR leicht 100 MB oder mehr wiegen. Drittens ist die Reproduzierbarkeit fragwürdig -- identischer Quellcode kann je nach Zeitpunkt des Builds unterschiedliche Images erzeugen, weil Timestamps und Metadaten einfließen.
Natürlich kann man das Dockerfile verbessern: Multi-Stage-Builds, Spring Boots Layered-JAR-Feature nutzen, Zeitstempel fixieren. Aber dann landet man schnell bei einem 30-Zeilen-Dockerfile, das gepflegt und verstanden werden muss. Und man braucht immer noch den Docker Daemon.
Spring Boot Buildpacks: bootBuildImage
Mit Spring Boot 2.3 kam ein integrierter Ansatz hinzu, der mit Spring Boot 3.0 nochmals verbessert wurde: Cloud Native Buildpacks via bootBuildImage. Die Idee dahinter ist, dass das Framework selbst weiß, wie man ein optimales Image baut.
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"
))
}
Der Aufruf ist simpel: ./gradlew bootBuildImage. Spring Boot nutzt Paketo Buildpacks, um ein OCI-konformes Image zu erzeugen. Das Ergebnis ist gut strukturiert mit sauberer Layer-Trennung, und man muss kein Dockerfile pflegen.
Der Haken: Der Task benötigt einen laufenden Docker Daemon, weil Buildpacks intern Docker-Container starten, um das Image zu assemblieren. Außerdem ist der Build-Prozess vergleichsweise langsam, weil jedes Mal die Buildpack-Infrastruktur hochgefahren wird. Und die Konfigurationsmöglichkeiten sind begrenzt -- für Standardfälle reicht es, aber bei speziellen Anforderungen an Base Images oder JVM-Flags wird es umständlich.
Google Jib: Container Images aus dem Build-Tool heraus
Jib ist Googles Open-Source-Lösung für genau dieses Problem. Das Projekt existiert seit 2018 und ist mittlerweile ausgereift und stabil. Die zentrale Idee: Jib baut OCI-konforme Container Images direkt aus dem Build-Tool heraus -- ohne Docker Daemon, ohne Dockerfile, ohne Container-Runtime.
Das Jib-Gradle-Plugin integriert sich nahtlos in den Build-Prozess:
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")
}
}
Mit ./gradlew jib wird das Image gebaut und direkt in die Registry gepusht. Kein Docker nötig. Mit ./gradlew jibDockerBuild kann man alternativ in den lokalen Docker Daemon bauen, falls man lokal testen will.
Warum Jib überzeugt
Die Gründe, warum wir Jib für unsere Spring-Boot-Services einsetzen, sind konkret und messbar.
Kein Docker Daemon nötig. Unsere CI-Pipelines laufen als unprivilegierte Jobs. Kein Docker-in-Docker, kein Mount des Docker Sockets, keine Sicherheitsrisiken durch privilegierte Container. Jib kommuniziert direkt mit der Container Registry über HTTP -- das ist alles, was es braucht.
Intelligente Layer-Struktur. Jib zerlegt die Anwendung in mehrere Layers: Dependencies, Snapshot-Dependencies, Ressourcen und Klassen. Nur Layers, die sich tatsächlich geändert haben, werden neu gebaut und gepusht. Bei einer typischen Code-Änderung betrifft das nur den Klassen-Layer -- wenige Kilobyte statt dutzender Megabyte. Das beschleunigt sowohl den Build als auch den Pull im Kubernetes-Cluster erheblich.
Reproduzierbare Builds. Gleicher Input erzeugt identischen Output. Jib eliminiert die Variabilität, die bei Dockerfile-Builds durch Timestamps, Dateisystem-Metadaten und nicht-deterministische Paketmanager entsteht. Für uns ist das wichtig, weil wir nachvollziehen können müssen, welcher Code in welchem Image steckt.
Schnelle inkrementelle Builds. Durch die Layer-Zerlegung und den lokalen Cache baut Jib inkrementelle Änderungen in Sekunden statt Minuten. In der Entwicklung macht das einen spürbaren Unterschied.
Volle Integration in Gradle. Die gesamte Image-Konfiguration lebt in build.gradle.kts -- kein separates Dockerfile, keine zusätzliche Toolchain. Das passt perfekt zu unserer Strategie, Build-Logik zentral in Gradle zu verwalten, wie wir es in unserem Beitrag zur Gradle Kotlin DSL beschrieben haben.
Jib in der Praxis: CI/CD-Integration
In unseren CI-Pipelines sieht der Build-Schritt denkbar einfach aus:
./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}
Das Image wird gebaut und direkt in die Registry gepusht. Kubernetes zieht es beim nächsten Deployment von dort. Kein zwischengeschalteter Docker-Build, kein docker push -- ein einziger Gradle-Task erledigt alles.
Für Multi-Modul-Projekte konfigurieren wir Jib im Root-Projekt als Konvention:
subprojects {
apply(plugin = "com.google.cloud.tools.jib")
jib {
from {
image = "eclipse-temurin:17-jre-alpine"
}
to {
image = "registry.encircle360.com/${project.name}"
}
}
}
Jedes Submodul erbt die Basiskonfiguration und kann sie bei Bedarf überschreiben. Damit halten wir die Konfiguration DRY, auch bei Services mit zehn oder mehr Modulen.
Wann Jib nicht passt
Fairerweise gibt es Szenarien, in denen Jib nicht die beste Wahl ist. Wer komplexe native Abhängigkeiten in das Image einbinden muss -- etwa C-Bibliotheken, die kompiliert werden müssen -- braucht weiterhin ein Dockerfile oder einen Multi-Stage-Build. Auch für Nicht-Java-Projekte ist Jib keine Option, da es spezifisch für Java konzipiert ist. Und wer bereits eine ausgefeilte Dockerfile-basierte Pipeline hat, die gut funktioniert, hat nicht zwingend Grund zum Wechsel.
Für den typischen Spring-Boot-Service -- und das sind die allermeisten unserer Anwendungen -- ist Jib jedoch die überlegene Lösung.
Fazit
Container Images zu bauen klingt trivial, ist es aber nicht -- zumindest nicht, wenn man Reproduzierbarkeit, Geschwindigkeit und Sicherheit ernst nimmt. Google Jib löst die wesentlichen Probleme des traditionellen Dockerfile-Ansatzes und integriert sich perfekt in den Gradle-Build-Prozess. Seit wir Jib einsetzen, haben wir keine Dockerfiles mehr für unsere Java-Services, schnellere CI-Builds und die Gewissheit, dass gleicher Code immer zum gleichen Image führt.
Wer Spring Boot und Gradle nutzt und seine Container-Builds vereinfachen will, sollte Jib ausprobieren. Die Einrichtung dauert fünf Minuten, und die Vorteile zeigen sich sofort.
Geschrieben von
Patrick HütterGründer & Software-Architekt
Softwarearchitekt, Ingenieur und Unternehmer. Patrick baut seit über einem Jahrzehnt Produkte und Plattformen — von Enterprise-Backends über Cloud-Native-Infrastruktur bis hin zu KI-gestützten Anwendungen. Als Gründer von encircle360 verbindet er technische Tiefe mit unternehmerischem Denken und treibt Open-Source-Projekte voran, die echten Impact haben.
Das könnte dich auch interessieren
Vom Manifest zur Produktion: Wie ADL, A2A und das Inference Gateway die Agenten-Infrastruktur revolutionieren
05.07.2026 · 12 Min. Lesezeit
Agent-Orchestrierung mit Java: Wie Sie LLM-Agenten produktionsreif in die JVM bringen
04.07.2026 · 6 Min. Lesezeit
Spring AI: Wie Java-Entwickler endlich KI-Features ohne Umwege integrieren
25.03.2026 · 5 Min. Lesezeit