mehrere Images deklarativ und parallel bauen
Wer in einem Monorepo fünf, zehn oder zwanzig Images pflegt, kennt das Problem: eine wachsende Liste einzelner docker build-Aufrufe in einem Shell-Skript, die nacheinander laufen und bei jedem neuen Image manuell ergänzt werden müssen. docker buildx bake ersetzt diese Skripte durch eine deklarative Datei und baut alle Images parallel.
Inhaltsverzeichnis
- 1. Das Problem mit vielen einzelnen docker build-Aufrufen
- 2. Die Grundstruktur einer docker-bake.hcl-Datei
- 3. Wie BuildKit die parallele Ausführung umsetzt
- 4. Gruppen und Vererbung mit target inherits
- 5. Variablen und Matrix-Builds für mehrere Plattformen
- 6. Named Contexts: Abhängigkeiten zwischen Targets
- 7. JSON als Alternative zu HCL
- 8. Integration in CI/CD-Pipelines
- 9. Praktische Tipps für den Umstieg von Shell-Skripten
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem mit vielen einzelnen docker build-Aufrufen
In vielen Projekten wächst die Anzahl der Docker-Images mit der Zeit organisch: ein Image für den Webserver, eines für den Worker, eines für den Cron-Runner, eines für die Test-Umgebung, jeweils mit leicht unterschiedlichen Build-Args, Tags und Ziel-Plattformen. Der naheliegende erste Schritt ist ein Shell-Skript mit mehreren docker build-Zeilen hintereinander, was schnell unübersichtlich wird und, entscheidender, jedes Image strikt sequenziell baut, selbst wenn die zugrunde liegende Hardware problemlos mehrere Builds gleichzeitig verarbeiten könnte.
docker buildx bake, seit BuildKit fester Bestandteil der Docker-CLI, löst genau dieses Problem. Statt vieler einzelner Befehle wird eine deklarative Konfigurationsdatei definiert, die alle Images, ihre jeweiligen Build-Kontexte, Build-Args und Tags beschreibt. Ein einziger Aufruf von docker buildx bake baut dann alle oder eine ausgewählte Teilmenge dieser Images, wobei BuildKit unabhängige Builds automatisch parallelisiert und dabei sogar Layer-Caches zwischen verwandten Images wiederverwendet.
2. Die Grundstruktur einer docker-bake.hcl-Datei
Die Standard-Konfigurationsdatei heißt docker-bake.hcl und nutzt HCL, die Hashicorp Configuration Language, dieselbe Sprache, die auch Terraform verwendet. Eine Bake-Datei besteht im Kern aus sogenannten Targets, jedes davon entspricht einem einzelnen docker build-Aufruf mit eigenem Kontext, Dockerfile, Build-Args und Tags. Wird docker buildx bake ohne weitere Argumente aufgerufen, sucht es automatisch nach docker-bake.hcl im aktuellen Verzeichnis und baut standardmäßig alle darin definierten Targets.
Jedes Target kann eigene Werte für context, dockerfile, tags, args und platforms definieren, wodurch sich pro Image individuelle Einstellungen treffen lassen, ohne dass ein zentrales Skript unterschiedliche Fallunterscheidungen für jedes Image enthalten muss. Diese Deklarativität macht Bake-Dateien deutlich wartungsfreundlicher als gewachsene Shell-Skripte, weil neue Images einfach als weiteres target-Block hinzugefügt werden, ohne bestehende Logik anzufassen.
// docker-bake.hcl
group "default" {
targets = ["web", "worker", "cron"]
}
target "web" {
context = "./services/web"
dockerfile = "Dockerfile"
tags = ["mironsoft/web:latest"]
}
target "worker" {
context = "./services/worker"
dockerfile = "Dockerfile"
tags = ["mironsoft/worker:latest"]
}
target "cron" {
context = "./services/cron"
dockerfile = "Dockerfile"
tags = ["mironsoft/cron:latest"]
}
3. Wie BuildKit die parallele Ausführung umsetzt
Ruft man docker buildx bake ohne Einschränkung auf, ermittelt BuildKit zunächst den Abhängigkeitsgraphen zwischen den Targets. Targets ohne gegenseitige Abhängigkeit, etwa weil keines das Ergebnis eines anderen per contexts referenziert, werden parallel gebaut, begrenzt durch die verfügbaren CPU-Kerne und den konfigurierten Builder. In der Praxis bedeutet das für ein Projekt mit fünf unabhängigen Images eine deutlich kürzere Gesamt-Build-Zeit gegenüber fünf sequenziellen docker build-Aufrufen, besonders auf Multi-Core-CI-Runnern.
Zusätzlich teilen sich parallele Builds denselben BuildKit-Cache, sodass identische Basis-Layer, etwa ein gemeinsames FROM node:20-alpine oder ein geteilter apt-get-Layer, nur einmal gebaut und für alle betroffenen Targets wiederverwendet werden, statt redundant mehrfach ausgeführt zu werden. Dieser Effekt verstärkt sich zusätzlich, wenn mehrere Images auf einem gemeinsamen Basis-Image oder einer gemeinsamen Build-Stage in einem Multi-Stage-Dockerfile aufbauen.
# Alle Targets der default-Gruppe parallel bauen
docker buildx bake
# Nur ein bestimmtes Target bauen
docker buildx bake worker
# Mehrere spezifische Targets bauen
docker buildx bake web worker
# Geplanten Build-Plan anzeigen, ohne tatsaechlich zu bauen
docker buildx bake --print
4. Gruppen und Vererbung mit target inherits
Bake-Dateien unterstützen group-Blöcke, mit denen sich mehrere Targets zu benannten Sammlungen bündeln lassen, etwa eine Gruppe production mit allen produktiven Images und eine separate Gruppe ci mit zusätzlichen Test-Images. Wird docker buildx bake production aufgerufen, baut BuildKit ausschließlich die in dieser Gruppe gelisteten Targets, was gezielte Teilbuilds ohne separate Konfigurationsdateien ermöglicht.
Noch mächtiger ist die Vererbung über inherits: Ein Basis-Target kann gemeinsame Einstellungen wie Plattformen, gemeinsame Build-Args oder Labels definieren, und konkrete Image-Targets erben diese Werte, überschreiben aber gezielt einzelne Felder wie context oder tags. Das reduziert Duplikation erheblich, besonders in Projekten mit vielen strukturell ähnlichen Images, etwa einer Reihe von Microservices, die alle dieselbe Node-Version und dieselben Standard-Labels verwenden sollen.
// docker-bake.hcl mit Gruppen und Vererbung
group "production" {
targets = ["web", "worker"]
}
group "ci" {
targets = ["web", "worker", "test"]
}
target "_common" {
args = {
NODE_VERSION = "20"
}
labels = {
"org.opencontainers.image.vendor" = "Mironsoft"
}
}
target "web" {
inherits = ["_common"]
context = "./services/web"
tags = ["mironsoft/web:latest"]
}
target "worker" {
inherits = ["_common"]
context = "./services/worker"
tags = ["mironsoft/worker:latest"]
}
target "test" {
inherits = ["_common"]
context = "./services/web"
target = "test-stage"
}
5. Variablen und Matrix-Builds für mehrere Plattformen
HCL-Bake-Dateien unterstützen eigene variable-Deklarationen mit Default-Werten, die sich über Umgebungsvariablen zur Build-Zeit überschreiben lassen, ähnlich wie ARG im Dockerfile, aber auf Ebene der gesamten Bake-Datei statt eines einzelnen Images. Damit lassen sich zum Beispiel Tags dynamisch mit der aktuellen Git-Commit-SHA oder einer Versionsnummer versehen, ohne die Bake-Datei bei jedem Release manuell zu editieren.
Für Multi-Plattform-Builds unterstützt Bake außerdem eine Matrix-Syntax, mit der ein einziges Target-Template automatisch in mehrere konkrete Targets expandiert wird, etwa für die Kombination aus mehreren Node-Versionen und mehreren Zielarchitekturen. Das erspart das manuelle Duplizieren nahezu identischer Target-Blöcke und hält die Bake-Datei auch bei vielen Build-Varianten übersichtlich.
// docker-bake.hcl mit Variablen und Matrix
variable "TAG" {
default = "latest"
}
variable "REGISTRY" {
default = "docker.io/mironsoft"
}
target "web" {
context = "./services/web"
tags = ["${REGISTRY}/web:${TAG}"]
platforms = ["linux/amd64", "linux/arm64"]
}
target "matrix-test" {
matrix = {
node_version = ["18", "20", "22"]
}
name = "test-node-${node_version}"
context = "./services/web"
target = "test-stage"
args = {
NODE_VERSION = node_version
}
}
// Aufruf mit ueberschriebenem TAG:
// TAG=2.4.1 docker buildx bake web
6. Named Contexts: Abhängigkeiten zwischen Targets
In manchen Projekten baut ein Image auf dem Ergebnis eines anderen Images auf, etwa wenn ein gemeinsames Basis-Image mit vorinstallierten Abhängigkeiten zuerst gebaut und danach von mehreren Anwendungs-Images referenziert werden soll, statt jedes Mal von Grund auf neu zu bauen. Bake unterstützt dafür sogenannte Named Contexts über das Feld contexts innerhalb eines Targets, mit denen ein anderes Target als Build-Kontext referenziert werden kann, etwa contexts = { base = "target:base-image" }.
BuildKit erkennt diese Abhängigkeit automatisch und baut das referenzierte Basis-Target zuerst, bevor es die davon abhängigen Targets startet, wodurch der zuvor rein manuelle Zwischenschritt eines separaten docker build für das Basis-Image entfällt. Innerhalb des abhängigen Dockerfiles wird der Named Context dann wie ein normaler Build-Stage-Name referenziert, etwa über FROM base AS runtime, wodurch sich komplexe Abhängigkeitsketten zwischen mehreren eigenständigen Dockerfiles sauber in einer einzigen Bake-Datei abbilden lassen.
// docker-bake.hcl mit Named Context zwischen zwei Targets
target "base-image" {
context = "./base"
dockerfile = "Dockerfile"
tags = ["mironsoft/base:latest"]
}
target "app" {
context = "./services/app"
dockerfile = "Dockerfile"
contexts = {
base = "target:base-image"
}
tags = ["mironsoft/app:latest"]
}
// Im Dockerfile von ./services/app:
// FROM base AS runtime
7. JSON als Alternative zu HCL
Wer HCL nicht einführen möchte oder bereits Build-Metadaten aus einem anderen System generiert, kann Bake-Dateien auch als reines JSON schreiben, üblicherweise unter dem Namen docker-bake.json. Die Struktur entspricht dabei exakt der HCL-Variante mit group- und target-Objekten, nur eben in JSON-Syntax, was sich besonders gut eignet, wenn die Datei programmatisch von einem Build-Skript oder einer CI-Pipeline generiert wird, statt von Hand gepflegt zu werden.
Ein praktischer Vorteil von JSON ist die einfache Integration mit bestehenden Tools: Ein Node.js- oder Python-Skript kann die Liste der zu bauenden Images beispielsweise aus einer package.json oder einer zentralen Service-Registry ableiten und daraus automatisch eine gültige docker-bake.json generieren, bevor docker buildx bake -f docker-bake.json aufgerufen wird. Für rein manuell gepflegte Konfigurationen bleibt HCL wegen seiner besseren Lesbarkeit und Kommentar-Unterstützung meist die angenehmere Wahl.
# docker-bake.json aequivalent zur HCL-Datei, als JSON
# {
# "group": { "default": { "targets": ["web", "worker"] } },
# "target": {
# "web": { "context": "./services/web", "tags": ["mironsoft/web:latest"] },
# "worker": { "context": "./services/worker", "tags": ["mironsoft/worker:latest"] }
# }
# }
docker buildx bake -f docker-bake.json
8. Integration in CI/CD-Pipelines
In CI/CD-Pipelines zahlt sich Bake besonders aus, weil ein einziger Build-Schritt genügt, um alle relevanten Images für einen Commit zu bauen, statt einer Matrix aus mehreren parallelen Jobs, die jeweils eigene Runner-Ressourcen beanspruchen. Kombiniert mit der --push-Option lassen sich alle gebauten Images direkt im selben Aufruf in eine Registry veröffentlichen, ohne separate docker push-Befehle für jedes einzelne Image zu benötigen.
Für Pull-Request-Builds, bei denen nur geänderte Services neu gebaut werden sollen, lässt sich die Zielauswahl dynamisch anhand geänderter Verzeichnisse steuern, etwa indem ein CI-Skript anhand von git diff ermittelt, welche Targets betroffen sind, und nur diese an docker buildx bake übergibt. Das reduziert unnötige Build-Zeit erheblich in Monorepos, in denen ein einzelner Commit meist nur einen kleinen Teil der insgesamt vorhandenen Images betrifft.
# Alle Images bauen UND direkt in die Registry pushen
docker buildx bake --push
# Nur veraenderte Targets bauen (Beispiel-Logik in CI)
CHANGED=$(git diff --name-only origin/main... | grep -oP '^services/\K[a-z]+' | sort -u)
docker buildx bake $CHANGED --push
9. Praktische Tipps für den Umstieg von Shell-Skripten
Der Umstieg von einem gewachsenen Shell-Skript auf Bake lohnt sich meist schon ab drei bis vier eigenständigen Images, weil ab dieser Größe die Wartung des Skripts spürbar aufwendiger wird als die einer deklarativen Datei. Ein guter erster Schritt ist, jedes bestehende docker build-Kommando eins zu eins in ein Target zu übersetzen, ohne sofort Gruppen oder Vererbung einzuführen, und erst danach schrittweise Gemeinsamkeiten in ein gemeinsames _common-Target zu extrahieren.
Mit docker buildx bake --print lässt sich vor jedem echten Build der resultierende Build-Plan als JSON ausgeben, was besonders beim Debuggen von Vererbung und Variablen-Auflösung hilfreich ist, da Fehler in der HCL-Struktur so sichtbar werden, bevor überhaupt ein Build-Prozess gestartet wird. Diese Trockenlauf-Option sollte fester Bestandteil jeder Änderung an der Bake-Datei sein, bevor sie in eine CI-Pipeline übernommen wird.
| Aspekt | Mehrere docker build-Aufrufe | docker buildx bake |
|---|---|---|
| Ausführung | Sequenziell, ein Befehl nach dem anderen | Parallel, BuildKit ermittelt den Abhängigkeitsgraphen |
| Konfiguration | Verteilt über Shell-Skript-Zeilen | Zentral in docker-bake.hcl oder .json |
| Wiederverwendung gemeinsamer Werte | Manuell per Skript-Variablen | Eingebaut über inherits und _common-Targets |
| Multi-Plattform/Matrix-Builds | Aufwendig, viele einzelne Befehle nötig | Eingebaute matrix-Syntax |
| CI-Integration mit Registry-Push | Separater docker push je Image | Ein Aufruf mit --push für alle Targets |
Mironsoft
Container-Infrastruktur, CI-Pipelines und Deployment-Automatisierung
Docker-Setups, die im Team und in Produktion tragfähig bleiben?
Wir prüfen bestehende Dockerfiles und Compose-Stacks auf Sicherheitslücken, aufgeblähte Images und fragile Build-Pipelines und bauen daraus eine Container-Infrastruktur, die schnell baut, sicher läuft und im Team nachvollziehbar bleibt.
Dockerfile-Review
Multi-Stage-Builds, Layer-Caching und Image-Größe systematisch optimieren.
Security-Audit
Container-Isolation, Secrets-Handling und Image-Scanning gegen echte Angriffsflächen absichern.
CI/CD-Integration
Build-Pipelines, Registries und Deployment-Strategien für reproduzierbare Releases aufbauen.
10. Zusammenfassung
Docker Bake: Das Wichtigste auf einen Blick
Deklarativ statt Skript
Eine docker-bake.hcl beschreibt alle Images zentral statt vieler Shell-Zeilen.
Automatische Parallelisierung
BuildKit baut unabhängige Targets gleichzeitig und teilt sich den Layer-Cache.
Gruppen und Vererbung
group- und inherits-Blöcke bündeln Targets und reduzieren Duplikation erheblich.
CI-freundlich
Ein Aufruf mit --push baut und veröffentlicht alle relevanten Images gleichzeitig.