Cache-Strategien fuer docker/build-push-action richtig waehlen
Ein Docker-Image-Build, der in GitHub Actions bei jedem Push wieder bei null anfaengt, verschwendet Minuten, die sich mit der richtigen Cache-Strategie fuer docker/build-push-action fast vollstaendig einsparen lassen, wenn man die Unterschiede zwischen GitHub Actions Cache, Registry-Cache und Inline-Cache versteht.
Inhaltsverzeichnis
- 1. Warum Docker-Builds in GitHub Actions ohne Cache langsam sind
- 2. Das gha-Cache-Backend: nativ fuer GitHub Actions
- 3. Registry-Cache als portable Alternative
- 4. Inline-Cache: der einfachste, aber begrenzte Ansatz
- 5. Der Unterschied zu GitLab CI: kein natives gha-Aequivalent
- 6. Multi-Platform-Builds in der Pipeline einrichten
- 7. Matrix-Build als schnellere Alternative zu QEMU
- 8. Cache-Wirksamkeit sichtbar machen statt zu vermuten
- 9. Cache-Invalidierung und Dockerfile-Reihenfolge
- 10. Zusammenfassung
- 11. FAQ
1. Warum Docker-Builds in GitHub Actions ohne Cache langsam sind
Jeder GitHub Actions Job laeuft standardmaessig auf einem frisch provisionierten, isolierten Runner ohne jegliche vorherige Docker-Layer-Historie. Anders als eine lokale Entwicklungsmaschine, auf der Docker-Layer ueber Wochen im lokalen Cache liegen bleiben, startet jeder Workflow-Lauf technisch bei null, es sei denn, man richtet explizit einen persistenten Cache-Mechanismus ein. Ohne diesen baut selbst ein unveraendertes npm install-Layer bei jedem Lauf komplett neu, was bei groesseren Projekten mehrere Minuten pro Build kostet.
Die offizielle docker/build-push-action von Docker selbst unterstuetzt mehrere Cache-Backends ueber die Parameter cache-from und cache-to, die auf BuildKit aufsetzen. Die Wahl des richtigen Backends ist keine Kleinigkeit: Ein falsch gewaehltes Cache-Backend kann den Build sogar verlangsamen, weil das Hoch- und Herunterladen des Caches selbst Zeit kostet, die den eigentlichen Geschwindigkeitsgewinn wieder auffrisst.
2. Das gha-Cache-Backend: nativ fuer GitHub Actions
Seit BuildKit 0.11 existiert ein dediziertes Cache-Backend namens gha, das direkt auf die GitHub Actions Cache API zugreift, dieselbe Infrastruktur, die auch actions/cache fuer andere Build-Artefakte nutzt. Der grosse Vorteil: Es ist kein externes Registry-Konto und kein zusaetzliches Secret noetig, der Cache liegt vollstaendig innerhalb von GitHub und wird automatisch nach ungefaehr sieben Tagen Inaktivitaet oder bei Erreichen des repository-weiten Cache-Limits (aktuell 10 GB) verworfen.
Die Einrichtung erfolgt ueber die Action docker/setup-buildx-action in Kombination mit den Parametern cache-from: type=gha und cache-to: type=gha,mode=max in docker/build-push-action. Der Parameter mode=max ist dabei entscheidend: Standardmaessig (mode=min) cached BuildKit nur die finalen Layer des Images, waehrend mode=max auch Zwischenlayer aus Multi-Stage-Builds cached, was bei mehrstufigen Dockerfiles den Unterschied zwischen einem fast leeren und einem wirklich wirksamen Cache ausmacht.
name: Build and push Docker image
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
3. Registry-Cache als portable Alternative
Das Registry-Cache-Backend (type=registry) speichert Cache-Layer als separates, speziell markiertes Image direkt in derselben Container-Registry wie das eigentliche Image. Der entscheidende Vorteil gegenueber gha: Dieser Cache ist nicht an GitHub Actions gebunden und funktioniert identisch in GitLab CI, Jenkins oder jeder anderen CI-Plattform, solange diese ebenfalls Zugriff auf dieselbe Registry hat. Fuer Teams, die zwischen CI-Systemen wechseln oder mehrere Pipelines parallel betreiben, ist das ein erheblicher Vorteil gegenueber dem GitHub-spezifischen gha-Backend.
Der Nachteil ist ein zusaetzlicher Netzwerk-Roundtrip: Der Cache muss bei jedem Build explizit in die Registry hochgeladen und beim naechsten Build wieder heruntergeladen werden, was bei langsamen Netzwerkverbindungen oder sehr grossen Images spuerbar Zeit kostet. Ausserdem erzeugt jeder Cache-Push einen zusaetzlichen Tag oder ein zusaetzliches Manifest in der Registry, was bei restriktiven Registry-Kontingenten oder Kostenmodellen pro gespeichertem Layer beruecksichtigt werden muss.
- name: Build and push with registry cache
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:latest
cache-from: type=registry,ref=ghcr.io/myorg/myapp:buildcache
cache-to: type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max
4. Inline-Cache: der einfachste, aber begrenzte Ansatz
Die dritte Option ist der Inline-Cache (type=inline), bei dem die Cache-Metadaten direkt in das gepushte Image selbst eingebettet werden, ohne separates Cache-Image oder externen Speicher. Das ist die einfachste Konfiguration ueberhaupt, weil kein zweites Manifest oder zusaetzliches Tag noetig ist, das eigentliche Image dient gleichzeitig als eigener Cache. Der entscheidende Nachteil: Inline-Cache unterstuetzt ausschliesslich mode=min, Zwischenlayer aus Multi-Stage-Builds werden also grundsaetzlich nicht mit gecached, was bei komplexeren Dockerfiles einen erheblichen Teil des moeglichen Cache-Nutzens verschenkt.
In der Praxis eignet sich Inline-Cache vor allem fuer sehr einfache, einstufige Dockerfiles oder als schneller Einstieg, wenn ein Team noch nicht bereit ist, ein separates Cache-Backend einzurichten. Fuer produktive Pipelines mit Multi-Stage-Builds ist gha oder Registry-Cache mit mode=max nahezu immer die bessere Wahl, weil der Geschwindigkeitsgewinn durch vollstaendiges Layer-Caching den zusaetzlichen Konfigurationsaufwand deutlich uebersteigt.
- name: Build and push with inline cache
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: myregistry/myapp:latest
cache-from: type=registry,ref=myregistry/myapp:latest
cache-to: type=inline
5. Der Unterschied zu GitLab CI: kein natives gha-Aequivalent
Wer von GitLab CI kommt, kennt dort primaer zwei Ansaetze: den GitLab Registry-Cache (funktional identisch zum type=registry-Backend, das auch in GitHub Actions funktioniert) und Docker-in-Docker mit einem gemounteten Cache-Volume auf einem selbst verwalteten Runner. Letzteres funktioniert in GitHub Actions nur eingeschraenkt, weil die von GitHub bereitgestellten gehosteten Runner bei jedem Job komplett neu provisioniert werden und kein persistentes Dateisystem zwischen Laeufen bieten, anders als ein selbst betriebener GitLab-Runner, der oft dauerhaft auf derselben Maschine laeuft.
Das gha-Cache-Backend existiert deshalb als GitHub-spezifische Antwort auf genau dieses Problem: Es simuliert einen persistenten Cache ueber die gehostete Cache-API, ohne dass ein eigener, dauerhaft laufender Runner noetig ist. Wer Workflows zwischen GitHub Actions und GitLab CI portabel halten will, sollte deshalb auf das Registry-Cache-Backend setzen, das auf beiden Plattformen identisch funktioniert, statt sich auf das GitHub-exklusive gha-Backend zu verlassen.
6. Multi-Platform-Builds in der Pipeline einrichten
Fuer Images, die sowohl auf linux/amd64-Servern als auch auf linux/arm64-Systemen wie Apple-Silicon-Entwicklermaschinen oder ARM-basierten Cloud-Instanzen laufen sollen, unterstuetzt docker/build-push-action den Parameter platforms direkt. Voraussetzung ist QEMU-Emulation ueber docker/setup-qemu-action, da GitHub-gehostete Runner selbst nur auf amd64-Architektur laufen und die ARM-Variante des Images per Emulation gebaut werden muss, was den Build spuerbar verlangsamt gegenueber einem nativen Build auf passender Hardware.
Fuer Projekte mit haeufigen Multi-Platform-Builds lohnt sich der Wechsel zu nativen ARM-Runnern, die GitHub inzwischen als gehostete Option anbietet, oder ein Matrix-Build-Ansatz, bei dem jede Plattform in einem eigenen, parallelen Job auf passender nativer Hardware gebaut und die Ergebnisse anschliessend per docker buildx imagetools create zu einem gemeinsamen Multi-Arch-Manifest zusammengefuehrt werden. Dieser Ansatz ist deutlich schneller als QEMU-Emulation, erfordert aber einen zusaetzlichen Zusammenfuehrungsschritt in der Pipeline.
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push multi-platform image
uses: docker/build-push-action@v6
with:
context: .
push: true
platforms: linux/amd64,linux/arm64
tags: ghcr.io/myorg/myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
7. Matrix-Build als schnellere Alternative zu QEMU
Ein Matrix-Build teilt den Multi-Platform-Build in mehrere unabhaengige Jobs auf, von denen jeder auf einem zur Zielarchitektur passenden Runner laeuft und nur ein Digest-File statt eines vollstaendigen Push erzeugt. Ein finaler Job sammelt alle Digests ein und erstellt daraus per buildx imagetools create ein gemeinsames Multi-Arch-Manifest, das unter einem einzigen Tag alle Plattformen buendelt. Dieser Ansatz vermeidet die QEMU-Emulation komplett und ist bei ARM-Builds oft drei- bis viermal schneller.
Der Mehraufwand liegt in der zusaetzlichen Pipeline-Komplexitaet: Statt eines einzelnen Build-Schritts braucht man eine Matrix-Strategie, einen Artefakt-Austausch zwischen Jobs fuer die Digest-Dateien und einen separaten Merge-Job. Fuer kleinere Projekte mit gelegentlichen ARM-Builds ist der einfachere QEMU-Ansatz meist die pragmatischere Wahl, fuer Projekte mit sehr haeufigen Releases und grossen Images zahlt sich die Matrix-Variante durch die eingesparte Build-Zeit schnell aus.
jobs:
build:
strategy:
matrix:
platform: [linux/amd64, linux/arm64]
runs-on: ${{ matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push by digest
id: build
uses: docker/build-push-action@v6
with:
context: .
platforms: ${{ matrix.platform }}
outputs: type=image,name=ghcr.io/myorg/myapp,push-by-digest=true,name-canonical=true,push=true
merge:
needs: build
runs-on: ubuntu-latest
steps:
- name: Create manifest list
run: |
docker buildx imagetools create \
-t ghcr.io/myorg/myapp:latest \
ghcr.io/myorg/myapp@sha256:AAA ghcr.io/myorg/myapp@sha256:BBB
8. Cache-Wirksamkeit sichtbar machen statt zu vermuten
Ohne konkrete Messung bleibt die Frage, ob ein Cache-Backend tatsaechlich etwas bringt, reine Vermutung. BuildKit gibt in seinem Build-Log fuer jeden Layer an, ob er aus dem Cache bedient wurde (CACHED) oder neu gebaut werden musste, was sich direkt im GitHub Actions Job-Log nachlesen laesst, ohne zusaetzliche Werkzeuge. Ein Blick auf die Gesamtbauzeit allein reicht nicht aus, weil sie stark schwankt, je nachdem wie ausgelastet die GitHub-Infrastruktur gerade ist. Aussagekraeftiger ist die Anzahl der als CACHED markierten Layer im Verhaeltnis zur Gesamtzahl der Layer im Dockerfile.
Ein einfacher, aber effektiver Praxistest ist, denselben Workflow zweimal hintereinander ohne Codeaenderung laufen zu lassen und die Bauzeiten zu vergleichen. Faellt die zweite Laufzeit nicht signifikant kuerzer aus als die erste, deutet das auf ein falsch konfiguriertes Cache-Backend hin, haeufig fehlendes mode=max, ein falscher ref-Wert beim Registry-Cache oder ein Dockerfile, dessen Layer-Reihenfolge den Cache staendig invalidiert. Dieser Zwei-Laeufe-Test gehoert in jede Einfuehrung eines neuen Cache-Backends, bevor man sich auf dessen Wirksamkeit verlaesst.
9. Cache-Invalidierung und Dockerfile-Reihenfolge
Unabhaengig vom gewaehlten Backend bleibt die Grundregel von Docker-Layer-Caching bestehen: Jede Aenderung an einem Layer invalidiert automatisch alle nachfolgenden Layer im Cache. Ein Dockerfile, das zuerst den gesamten Quellcode kopiert und erst danach npm install ausfuehrt, invalidiert bei jeder Codeaenderung auch den Dependency-Installations-Layer, selbst wenn sich die package.json gar nicht geaendert hat. Die richtige Reihenfolge, zuerst Abhaengigkeitsdateien kopieren und installieren, dann den restlichen Code kopieren, ist deshalb die Grundvoraussetzung, damit ein Cache-Backend ueberhaupt etwas bewirken kann.
Ein weiterer haeufiger Fehler ist ein Build-Argument wie ein Zeitstempel oder eine Commit-SHA, das frueh im Dockerfile per ARG gesetzt wird und dadurch alle nachfolgenden Layer bei jedem Build invalidiert, selbst wenn dieser Wert erst spaeter im Build tatsaechlich gebraucht wird. Solche volatilen Build-Argumente sollten so spaet wie moeglich im Dockerfile platziert werden, idealerweise direkt vor dem Layer, der sie tatsaechlich benoetigt, um moeglichst viele vorherige Layer weiterhin aus dem Cache bedienen zu koennen.
| Cache-Backend | Speicherort | Plattform-portabel | Unterstuetzt mode=max |
|---|---|---|---|
type=gha |
GitHub Actions Cache API | Nein, GitHub-exklusiv | Ja |
type=registry |
Separates Cache-Image in Registry | Ja, funktioniert ueberall | Ja |
type=inline |
Eingebettet im Image selbst | Ja, funktioniert ueberall | Nein, nur mode=min |
| Kein Cache | Kein Caching | N/A | N/A |
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
GitHub Actions Docker-Cache: Das Wichtigste auf einen Blick
gha-Backend
Nativ fuer GitHub Actions, kein Registry-Konto noetig, aber GitHub-exklusiv.
Registry-Cache
Plattform-portabel zwischen GitHub Actions und GitLab CI, kostet Netzwerk-Roundtrip.
mode=max
Zwingend fuer wirksames Caching von Zwischenlayern in Multi-Stage-Builds.
Multi-Platform
QEMU fuer einfache Setups, Matrix-Build mit nativen Runnern fuer maximale Geschwindigkeit.