docker/build-push-action richtig konfigurieren: Cache-Strategien in GitHub Actions
AI generated
FROM
RUN
Docker · CI/CD · GitHub Actions
Docker Builds in GitHub Actions
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.

17 Min. Lesezeit docker/build-push-action BuildKit Cache Multi-Platform-Builds

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.

11. FAQ: GitHub Actions Docker-Cache: Das Wichtigste auf einen Blick

1Was ist der Vorteil des gha-Cache-Backends gegenueber Registry-Cache?
Das gha-Backend braucht kein separates Registry-Konto und kein zusaetzliches Secret, der Cache liegt vollstaendig innerhalb der GitHub Actions Infrastruktur und wird automatisch verwaltet.
2Warum sollte ich mode=max statt mode=min verwenden?
mode=min cached nur die finalen Layer des Images, mode=max cached zusaetzlich Zwischenlayer aus Multi-Stage-Builds. Bei mehrstufigen Dockerfiles macht das einen erheblichen Unterschied fuer die Cache-Trefferquote.
3Funktioniert das gha-Cache-Backend auch in GitLab CI?
Nein, das gha-Backend ist exklusiv an die GitHub Actions Cache API gebunden. Fuer plattform-uebergreifende Portabilitaet zwischen GitHub Actions und GitLab CI eignet sich stattdessen das Registry-Cache-Backend.
4Was ist die groesste Einschraenkung von Inline-Cache?
Inline-Cache unterstuetzt ausschliesslich mode=min, Zwischenlayer aus Multi-Stage-Builds werden also nicht gecached, was bei komplexeren Dockerfiles einen erheblichen Teil des moeglichen Nutzens verschenkt.
5Wie gross ist das Cache-Limit fuer das gha-Backend?
Das repository-weite Cache-Limit liegt aktuell bei 10 GB, und Cache-Eintraege werden nach ungefaehr sieben Tagen Inaktivitaet automatisch verworfen.
6Warum ist Multi-Platform-Build mit QEMU langsamer als ein nativer Build?
GitHub-gehostete Runner laufen selbst nur auf amd64-Architektur. Die ARM-Variante muss deshalb per QEMU-Emulation gebaut werden, was spuerbar langsamer ist als ein nativer Build auf passender Hardware.
7Was ist der Vorteil eines Matrix-Builds gegenueber QEMU-Emulation?
Ein Matrix-Build laesst jede Zielplattform auf nativer, passender Hardware bauen statt per Emulation, was bei ARM-Builds oft drei- bis viermal schneller ist, erfordert aber einen zusaetzlichen Merge-Schritt fuer das finale Multi-Arch-Manifest.
8Warum invalidiert eine Codeaenderung manchmal auch den Dependency-Layer?
Wenn ein Dockerfile zuerst den gesamten Quellcode kopiert und erst danach die Abhaengigkeiten installiert, invalidiert jede Codeaenderung auch den Installations-Layer. Die richtige Reihenfolge ist, zuerst Abhaengigkeitsdateien zu kopieren und zu installieren, dann den restlichen Code.
9Sollten volatile Build-Argumente frueh oder spaet im Dockerfile stehen?
So spaet wie moeglich, idealerweise direkt vor dem Layer, der sie tatsaechlich benoetigt, damit moeglichst viele vorherige Layer weiterhin aus dem Cache bedient werden koennen.
10Kann ich mehrere Cache-Quellen gleichzeitig fuer cache-from angeben?
Ja, cache-from akzeptiert eine Liste mehrerer Quellen, etwa gha und registry gleichzeitig, wobei BuildKit die erste passende Quelle fuer jeden Layer verwendet.