Blockpit
Building Docker Images
Description
Julian Handl von Blockpit zeigt in seinem devjobs.at TechTalk, warum manchmal Docker Images viel zu viel Speicherplatz brauchen und gibt Tipps was man dagegen tun kann.
Beim Videoaufruf stimmst Du der Datenübermittlung an YouTube und der Datenschutzerklärung zu.
Video Zusammenfassung
In Building Docker Images erklärt Julian Handl, wie Docker-Schichten und Caching funktionieren und warum naive Dockerfiles zu langsamen Builds und großen Images führen; er zeigt, wie man durch selektives COPY von package.json/package-lock.json und frühzeitiges npm install den Cache maximal nutzt. Er vergleicht Basis-Images (node:16 vs. node:16-alpine) und demonstriert Multi-Stage-Builds, die eine Angular-App von 1,3 GB über 670 MB auf 113 MB verkleinern, warnt vor Dateifallen (Move/Delete, doppelte Ebenen, COPY vs. ADD) und empfiehlt .dockerignore. Abschließend zeigt er from-scratch-Images mit statisch kompilierten Binaries (u. a. 130‑KB Hello‑World und ein 5,01‑MB Angular-Image mit DHCPT), sodass Zuschauer Build-Zeiten, Speicherbedarf und Traffic deutlich reduzieren können.
Building Docker Images: Schlanke und schnelle Container nach Julian Handl (Blockpit)
Kontext: Warum wir über effiziente Docker-Images sprechen müssen
Bei DevJobs.at haben wir „Building Docker Images“ von Julian Handl (Blockpit) verfolgt und eine sehr vertraute Ausgangslage wiedererkannt: Images werden mit der Zeit immer größer, Builds dauern ewig, und Speicher sowie Netzwerkbudget sind schneller verbraucht als uns lieb ist. Julian formuliert die Motivation pragmatisch:
„Ich will Zeit sparen … ich will Strom sparen … ich will Platz sparen … und ich will Traffic sparen.“
Das sind keine abstrakten Ideale, sondern alltägliche Zwänge. Wenn ein neu eingerichtetes Notebook nur ein paar hundert Images fasst oder ein Cloud-Server schon bei wenigen Dutzend an Grenzen stößt, wird Optimierung zur Pflicht. Genau hier setzt die Session an: Docker-Images sind leicht zu bauen – aber ebenso leicht ineffizient zu bauen. Die gute Nachricht: Mit ein paar strukturierten Prinzipien lässt sich Zeit, Strom, Speicher und Traffic messbar reduzieren.
Docker-Layer verstehen: Caching ja, aber jedes Layer zählt
Docker baut Images schichtweise. Jede Anweisung im Dockerfile erzeugt ein Layer; alle Layer landen im finalen Image. Der wesentliche Vorteil: Layer können gecacht werden. Der große Haken: Der Cache greift nur so lange, bis sich etwas Relevantes ändert – ab dem ersten geänderten Schritt verfällt Caching für alle nachfolgenden Schritte.
Julian illustriert das am klassischen Node-Beispiel. In der „naiven“ Variante kopiert man den kompletten App-Ordner in den Container, führt die Installation der Abhängigkeiten aus und startet den Build. Das Problem: Schon eine kleine Änderung am Anwendungscode invalidiert den Cache früher als nötig, weil die komplette Kopie der App sehr früh im Dockerfile erfolgt. Der Cache bricht an der ersten Stelle, an der sich etwas ändert – hier bereits bei der großen Kopieroperation. Ergebnis: npm install wird ständig neu ausgeführt, obwohl sich die package.json gar nicht verändert hat.
Die Abhilfe ist ebenso simpel wie wirkungsvoll: feinere, zweckmäßige Schritte. Statt die gesamte App früh zu kopieren, werden zuerst nur package.json und package-lock.json in den Container gebracht und sofort npm install ausgeführt. Erst danach folgt das Kopieren des eigentlichen Quellcodes (z. B. src). Der Clou: In der Praxis ändern sich Dependencies deutlich seltener als der Anwendungsquellcode. Dadurch kann der aufwendige Schritt der Paketinstallation über viele Builds hinweg aus dem Cache bedient werden. Das spart Zeit, CPU und Bandbreite.
Kerngedanke
- Je später sich „volatile“ Dateien ändern, desto länger greift der Cache.
- Dependencies zuerst verarbeiten, Applikationscode danach – nicht umgekehrt.
- Weniger große, unspezifische Kopieroperationen („copy all“) – mehr zielgenaue Schritte.
Platz und Traffic sparen: Das richtige Basis-Image
Bevor wir komplexer werden, lohnt sich ein Blick auf den größten Einzelhebel: die Basisschicht des Images. Julian zeigt den Unterschied zwischen zwei gut bekannten Node-Varianten:
node:16mit einer komprimierten Größe von rund 332 MB.node:16-alpinemit rund 39 MB.
Allein der Wechsel des Basis-Images spart bereits Hunderte Megabyte – bei jedem Pull, jedem Push und jedem Rollout. Das wirkt sich spürbar auf Bandbreitenkosten, Speichernutzung und Downloadzeiten aus. Natürlich muss die gewählte Variante zur Anwendung passen; hier zeigt die alpine-Variante, wie viel Luft nach oben bei schlankeren Images vorhanden ist.
Multi-Stage-Builds: Die Schichten, die man hinter sich lassen darf
Ein Schlüsselkonzept in Julians Vortrag sind Multi-Stage-Builds. Der Mechanismus: Wir trennen strikt zwischen Build-Umgebung („Builder“) und Laufzeit-Umgebung. Stärke und Komfortwerkzeuge gehören in die Build-Stage; die finale Laufzeit-Stage ist so klein wie möglich.
Julian setzt zunächst eine Builder-Stage auf Basis von node:16 auf. Dort werden Dependencies installiert und das Projekt gebaut. Danach beginnt eine zweite Stage auf Basis von node:16-alpine. In dieser leichten Stage liegt nur das, was wirklich im Betrieb benötigt wird – etwa die Artefakte aus dem Build (z. B. das Distribution-Verzeichnis). Wichtig: Alles, was vor der letzten FROM-Zeile im Dockerfile passiert, bleibt im finalen Image zurück. Diese Trennung sorgt dafür, dass Entwicklungswerkzeuge, temporäre Dateien und Build-Abhängigkeiten gar nicht erst in die Produktion wandern.
Zahlen, die haften bleiben
Julian demonstriert den Effekt anhand einer simplen Angular-Anwendung:
- Single-Stage-Build mit
node:16: ca. 1,3 GB. - Einziger Wechsel der Basis auf
node:16-alpine: ca. 670 MB. - Multi-Stage-Build (Builder
node:16, Runtimenode:16-alpine): ca. 113 MB.
Schon der Wechsel auf alpine halbiert die Größe. Die Multi-Stage-Variante reduziert weiter – auf einen Bruchteil der Ausgangsbasis. Für kontinuierliche Deployments, bei denen Images oft über Netzwerke bewegt werden, ist das ein massiver Hebel.
Umgang mit Dateien: Kopieren, Verschieben, Löschen – und die Tücken der Layer
Einer der lehrreichsten Abschnitte in „Building Docker Images“ ist der über Dateien und Layer. Dabei gibt es gleich mehrere Fallstricke:
- „Nur kopieren, was wirklich gebraucht wird“: Eine kleine .dockerignore und eine saubere .gitignore reduzieren den Build-Kontext – große, irrelevante Verzeichnisse werden nicht mitgeschleppt.
- Unbewusstes Duplizieren durch Verschieben: Wer Konfigurationsdateien von A nach B verschiebt, erzeugt in Docker nicht nur eine Umbenennung. Die Datei existiert in der alten Version weiterhin im vorherigen Layer und zusätzlich in neuer Position im aktuellen Layer. Ergebnis: mehr Platzbedarf.
- „Löschen“ löscht nicht wirklich: Das Entfernen z. B. großer Zip-Dateien am Ende der Build-Sequenz wirkt nur für das aktuelle Layer. Die Daten bleiben in den darunterliegenden Layern erhalten und landen somit trotzdem im finalen Image.
- Rechte- oder Attributänderungen duplizieren Dateien: Jede Änderung erzeugt eine neue Dateivariante im neuen Layer. Das summiert sich.
Der zentrale Lernpunkt: Jedes Layer ist Teil des finalen Images. Mit reinen „Aufräum“-Schritten lässt sich bereits in den Layern belegter Speicher nicht mehr wegzaubern. Um etwas „zurückzulassen“, braucht es Multi-Stage-Builds – nur so bleiben Build-Artefakte, temporäre Dateien und große Zwischenstände wirklich draußen.
Von „scratch“ starten: Ultraminimalistische Laufzeitumgebung
Julian weitet den Blick über alpine hinaus und zeigt „scratch“: eine extrem minimale Basis, die mit jeder Docker-Installation mitkommt. „scratch“ ist leer im Wortsinn – kein Dateisystem, nur Root – und setzt auf statisch kompilierte Linux-Binaries. Damit lassen sich Laufzeitumgebungen bauen, die im Wesentlichen nur aus dem ausführbaren Programm bestehen.
Zwei Aspekte sind dabei besonders interessant:
- Der Unterschied zwischen „copy“ und „add“: Beim „add“ einer Tar-Datei wird diese on the fly entpackt. Das kann in speziellen Szenarien nützlich sein, wenn man definierte Dateisysteminhalte in ein „scratch“-Image hineinbringen will.
- Ein einfaches „Hello World“-Beispiel: Julian beschreibt, wie er innerhalb einer Build-Stage (z. B. auf Alpine) ein kleines Programm kompiliert, um dann nur das statisch gelinkte Binary in ein finales „scratch“-Image zu übernehmen. Ergebnis: Ein funktionsfähiges Image mit rund 130 Kilobyte – im Wesentlichen so groß wie das executable selbst.
Diese Denkweise – Build mit Werkzeugen in einer schweren Stage, Runtime als puristisches „scratch“ mit statisch kompiliertem Binary – ist das Extrem der Multi-Stage-Strategie: maximal klein, minimaler Angriffsraum, maximal schneller Transfer.
Minimaler Webserver für Angular: Drei Stages, ein statisches Binary, 5,01 MB unkomprimiert
Im zweiten Highlight demonstriert Julian die Konstruktion seines „kleinsten Docker-Images“, das eine vollständige Angular-Anwendung ausliefert. Der Aufbau folgt drei Stufen:
- Build der Angular-Anwendung.
- Kompilierung eines sehr einfachen Webservers (DHCPT) zu einem statischen Binary.
- Finale „scratch“-Stage, in der das statisch gelinkte Webserver-Binary und die gebauten Angular-Artefakte zusammengeführt und gestartet werden.
Ein paar wichtige Details aus der Session:
- Diese spezielle Lösung funktioniert „fairerweise“ nur mit Hash-Routing in Angular, weil der minimalistische Webserver keine URL-Rewrites unterstützt.
- Die unkomprimierte Größe liegt bei etwa 5,01 MB – inklusive der gesamten Angular-Anwendung.
- Der Anteil der Angular-App daran liegt bei ca. 4,8 MB; die restliche Hülle ist extrem klein.
- Anerkennung geht an Florian Lippan, der die Idee, DHCPT zu einem statischen Binary zu kompilieren, eingebracht hat; Julian hat diese Idee mit der Angular-Pipeline verbunden.
Das Resultat ist ein in sich geschlossenes, sehr kleines Image ohne Volumes und ohne externe Abhängigkeiten. Für statische Frontends zeigt sich hier klar: Mehr „Produktion“ braucht es oft gar nicht.
Praktische Leitlinien aus „Building Docker Images“
Julian fasst seine Empfehlungen am Ende kurz zusammen. Für uns bei DevJobs.at sind es die zentralen Regeln, die man beim Containerbau immer im Kopf haben sollte:
- Nur kopieren, was wirklich gebraucht wird.
- Das richtige Basis-Image wählen – oft ist „alpine“ die beste Wahl, sofern kompatibel.
- Multi-Stage-Builds „wirklich, wirklich“ verwenden.
Diese Punkte hängen direkt zusammen: Was nicht kopiert wird, muss nicht wieder gelöscht werden (und bläht keine Layer auf). Was als Build-Tool benötigt wird, bleibt in der Builder-Stage. Was zur Laufzeit gebraucht wird, landet in einer leichten, fokussierten Runtime-Stage. Und die Basis entscheidet, wie viel Ballast man überhaupt mitbringt.
Was wir mitnehmen: Ein mentales Modell für effiziente Images
Die Session liefert kein exotisches Trickrepertoire, sondern ein robustes mentales Modell:
- Denke in Layern: Jede Änderung erzeugt neue Realität im nächsten Layer. Wer spät variierende Inhalte spät einbringt, gewinnt Cache-Hits.
- Trenne Rollen: Build-Umgebung und Laufzeitumgebung haben unterschiedliche Zwecke – und sollten unterschiedliche Images sein.
- Sei explizit statt generisch: Feingranulare COPY-Schritte, klare .dockerignore, präzise Auswahl der Artefakte.
- Baue statisch, wenn möglich: „scratch“ ist die logische Endstufe für minimalistische Images, sofern die Anwendung statisch kompiliert werden kann.
Diese Prinzipien übersetzen sich in konkrete, messbare Effekte: weniger Buildzeit, weniger CPU-Last, deutlich kleinere Images und geringerer Netzwerkverbrauch – genau die vier Ziele, die Julian zu Beginn als Motivation gesetzt hat.
Angewandt auf den typischen Frontend-Container
Auch ohne exakte Dockerfile-Snippets wird die Vorgehensweise klar:
- Base-Image bewusst wählen: Wo immer möglich, alpine einsetzen.
- Dependencies isolieren:
package.jsonundpackage-lock.jsonzuerst, dann Installation, erst danach den Quellcode. - Multi-Stage konsequent: Artefakte aus der Builder-Stage in eine kleine Runtime-Stage übernehmen; alles andere bleibt zurück.
- Dateikontext minimieren: Mit .dockerignore und diszipliniertem Kopieren verhindern, dass große oder irrelevante Ordner in den Kontext gelangen.
- Keine „späten“ Pseudo-Aufräumarbeiten: Löschen am Ende hilft nicht gegen bereits vorhandene Layer. Aufräumen heißt: in einer anderen Stage gar nicht erst mitnehmen.
- Für statische Sites: Ein minimaler HTTP-Server reicht oft. In der Extremform als statisches Binary auf „scratch“ ergibt sich eine Laufzeitumgebung im Kilobyte- bis niedrigen Megabyte-Bereich.
Zitatwürdige Gedanken aus der Session
Einige Aussagen bleiben hängen, weil sie die Essenz der Container-Optimierung auf den Punkt bringen:
- „Docker-Images sind leicht zu bauen – und leicht ineffizient zu bauen.“
- „Wir können nur bis zu dem Schritt cachen, an dem sich etwas ändert.“
- „Alles vor dem letzten FROM bleibt zurück.“
- „Um wirklich etwas zurückzulassen, braucht es Multi-Stage-Builds.“
Gerade der zweite Satz ist für den Alltag wichtig: Wann immer wir eine Änderung vorziehen (z. B. ein großes „COPY .“), verpufft der Cache-Effekt und teure Schritte wie npm install laufen unnötig oft. Umgekehrt lässt sich durch kleine Umstellungen eine erstaunliche Beschleunigung erreichen.
Fazit: Einfache Muster, großer Effekt
„Building Docker Images“ von Julian Handl (Blockpit) zeigt, wie viel man mit disziplinierten Grundlagen erreichen kann. Keine Magie, keine Spezialwerkzeuge – nur ein klares Verständnis von Layern, Caches und Stages.
- Wechsel auf leichtere Basis-Images bringt sofort gewaltige Einsparungen.
- Multi-Stage trennt Build von Runtime und hält das Endprodukt sauber.
- Sorgfältiger Umgang mit Dateien verhindert schleichende Duplikate und nutzlose Aufräumversuche.
- Für statische Anwendungen reicht oft ein winziger HTTP-Server – im Extremfall als statisches Binary auf „scratch“.
Am Ende steht ein schlankes, schnelleres, ressourcenschonendes Delivery-Artefakt. Genau die Art von Image, die wir ziehen, verschieben und in Produktion rollen wollen – ohne dabei Zeit, Strom, Speicher oder Traffic unnötig zu verschwenden.