Docker Bake: Mehrere Images deklarativ und parallel bauen
AI generated
FROM
RUN
Docker · BuildKit · CI/CD
Docker Bake
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.

17 Min. Lesezeit buildx bake HCL Multi-Image-Build Parallele Builds Monorepo

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.

11. FAQ: Docker Bake: Das Wichtigste auf einen Blick

1Was ist der Hauptvorteil von docker buildx bake gegenüber mehreren docker build-Befehlen?
Der Hauptvorteil ist parallele Ausführung unabhängiger Builds durch BuildKit sowie eine zentrale, deklarative Konfiguration statt einer wachsenden Sammlung einzelner Shell-Kommandos. Das verkürzt die Gesamt-Build-Zeit und erleichtert die Wartung deutlich.
2Muss ich HCL lernen, um docker buildx bake zu nutzen?
Nicht zwingend, Bake-Dateien lassen sich auch als reines JSON unter dem Namen docker-bake.json schreiben. Für manuell gepflegte Konfigurationen ist HCL wegen Kommentar-Unterstützung und besserer Lesbarkeit aber meist angenehmer.
3Wie baue ich nur ein einzelnes Target statt aller definierten Images?
Mit docker buildx bake , zum Beispiel docker buildx bake worker. Es werden dann nur dieses Target und seine eventuellen Abhängigkeiten gebaut, alle anderen Targets in der Datei bleiben unberührt.
4Was bewirkt der Befehl docker buildx bake --print?
Er gibt den resultierenden Build-Plan als JSON aus, inklusive aufgelöster Variablen und Vererbung, ohne tatsächlich einen Build zu starten. Das eignet sich hervorragend zum Debuggen komplexer Bake-Dateien vor dem echten Build.
5Kann ich mit Bake Images direkt in eine Registry pushen?
Ja, mit der Option --push, etwa docker buildx bake --push, werden alle gebauten Images im selben Durchlauf in die jeweils in tags konfigurierte Registry veröffentlicht, ohne separate docker push-Befehle je Image.
6Wie funktioniert Vererbung zwischen Targets in einer Bake-Datei?
Über das Feld inherits kann ein Target die Einstellungen eines anderen Targets, etwa eines gemeinsamen _common-Targets, übernehmen und gezielt einzelne Felder wie context oder tags überschreiben. Das reduziert Duplikation bei strukturell ähnlichen Images erheblich.
7Was ist eine group in einer docker-bake.hcl-Datei?
Eine group bündelt mehrere Targets unter einem gemeinsamen Namen, etwa production oder ci. Wird docker buildx bake mit dem Gruppennamen aufgerufen, werden ausschließlich die in dieser Gruppe gelisteten Targets gebaut.
8Lassen sich Bake-Variablen zur Build-Zeit überschreiben?
Ja, über Umgebungsvariablen mit demselben Namen wie die in variable definierten Werte, zum Beispiel TAG=2.4.1 docker buildx bake web, um den Default-Tag für diesen einen Aufruf zu überschreiben.
9Was ist ein Matrix-Build in docker buildx bake?
Ein Matrix-Build expandiert automatisch ein einziges Target-Template in mehrere konkrete Targets anhand kombinierter Werte, etwa mehrerer Node-Versionen oder Zielarchitekturen, statt jede Kombination manuell als eigenes Target zu duplizieren.
10Teilen sich parallel gebaute Targets tatsächlich denselben Build-Cache?
Ja, sofern sie identische oder ähnliche Layer verwenden, etwa dasselbe Basis-Image oder dieselbe frühe Build-Stage in einem Multi-Stage-Dockerfile, werden diese Layer von BuildKit nur einmal gebaut und für alle betroffenen Targets wiederverwendet.