Van 1,2 GB naar 8 MB: de verandering in Docker die het uitrollen versnelt en risico’s verkleint

Een Docker-afbeelding van 1,2 GB om een kleine Go-service uit te voeren, lijkt misschien een verspilling van opslagruimte. Het echte probleem ontstaat echter wanneer Kubernetes nieuwe pods moet opstarten onder druk. Elke knoop die de afbeelding niet in de cache heeft, moet deze downloaden voordat de container kan starten. Het scheiden van de bouwomgeving en de runtime via een multi-stage build kan dat gewicht drastisch verminderen, hoewel de concrete sprong van 1,2 GB naar slechts 8 MB afhangt van de applicatie en niet als een universeel cijfer moet worden beschouwd.

De kern van kleine Docker-afbeeldingen in 30 seconden

  • Multi-stage builds maken het mogelijk een applicatie in een volledige afbeelding te bouwen en enkel het noodzakelijke resultaat voor productie over te nemen.
  • Docker raadt deze techniek aan om de grootte te verkleinen en het aanvalsvlak te verminderen.
  • scratch is een volledig lege afbeelding en geschikt voor statische binaire bestanden.
  • Een officiële golang:1.22-afbeelding neemt ongeveer 285 MB in beslag, gecomprimeerd op bepaalde Linux-varianten, nog vóór aanvullende code en lagen.
  • Hoe minder bytes, hoe sneller implementaties en schaaldiensten kunnen verlopen, vooral wanneer de knooppunten de lagen nog niet in cache hebben.

Het voorbeeld is eenvoudig. Een Go-applicatie kan perfect worden gebouwd met een Dockerfile als dit:

FROM golang:1.22

WORKDIR /app
COPY . .

RUN go build -o server .

CMD ["./server"]

Het werkt.

Het probleem is dat de afbeelding die wordt gebruikt voor het bouwen ook wordt gebruikt voor het uitvoeren van de service.

Dat betekent dat tijdens de productie tools worden meegenomen die misschien tijdens de docker build nodig waren, maar na het bouwen niet meer nodig zijn zodra het binair draait.

De officiële Docker-documentatie beschrijft dit probleem precies: bij een traditionele build worden alle instructies uitgevoerd in hetzelfde bouwproces, waardoor de lagen voor afhankelijkheden, compilatie en verpakking onderdeel kunnen worden van de uiteindelijke afbeelding. Docker beveelt multi-stage builds aan om deze werelden te scheiden.

De compiler hoeft niet mee te reizen naar productie

Het fundamentele verschil ligt in het behandelen van compilatie en uitvoering als twee aparte fasen.

Een eenvoudige Dockerfile voor een Go-applicatie zou kunnen zijn:

FROM golang:1.22 AS build

WORKDIR /app
COPY . .

RUN CGO_ENABLED=0 go build -o server .

FROM scratch

COPY --from=build /app/server /server

CMD ["/server"]

De eerste fase mag zo zwaar zijn als nodig.

Deze fase bevat Go, de benodigde bibliotheken, build-tools en andere dependencies die nodig zijn voor het genereren van het binair bestand.

Als die fase klaar is, wordt alleen /app/server overgenomen in de tweede afbeelding.

De compiler blijft achter in de eerste fase.

Het pakketbeheersysteem en de build-tools blijven in de eerste fase.

De aanpak wordt door Docker zelf vaak toegepast bij Go: een eerste laag gebaseerd op golang, gevolgd door een basis op scratch, waar alleen het gecompileerde uitvoerbare bestand wordt gekopieerd.

scratch heeft bovendien een bijzondere eigenschap: het bevat praktisch niets omdat het een lege basisafbeelding is.

Het bevat geen traditionele Linux-distributie, shell, pakketbeheerder of gebruiksklare tools.

Voor een statisch binaire bestand dat geen dependency op het systeem heeft, kan dit een extreem kleine basis vormen. Docker beveelt specifiek scratch aan voor volledig statische binaries.

Dat kan leiden tot enorme reducties.

Echter, het is belangrijk om een veelgebruikt voorbeeld uit social media voorzichtig te nuanceren: het is mogelijk om de grootte te reduceren van 1,2 GB naar 8 MB voor een specifieke applicatie, maar dit betekent niet dat golang:1.22 op zichzelf 1,2 GB zou nemen.

Docker Hub toont voor golang:1.22.12-alpine circa 69 MB en voor golang:1.22.12-bookworm ongeveer 285 MB, gecomprimeerd onder Linux/amd64. Extra lagen, afhankelijkheden, artefacten en caches kunnen de uiteindelijke grootte echter sterk verhogen.

De juiste vergelijking zou dus zijn:

OntwerpWat gaat naar productiePotentiële grootte
Enkele faseRuntime + compiler + tools + applicatieEnkele honderden MB of meer
Multi-stage + minimale afbeeldingMinimale runtime + applicatieEnkele tientallen MB
Multi-stage + scratchAlleen het binaire bestand en expliciet gekopieerde bestandenKan dalen tot enkele MB

De exacte waarden hangen af van de gecompileerde applicatie, architectuur, symbolen, bibliotheken en extra bestanden.

Grootte telt wanneer Kubernetes snel moet reageren

Een grote afbeelding betekent niet automatisch dat de opstarttijd in alle deploys traag is.

Docker en container-runtimes werken met lagen en caches. Als een knooppunt die lagen al in de cache heeft, hoeft het ze niet opnieuw te downloaden.

De situatie verandert echter bij een nieuw knooppunt.

Dit kan gebeuren tijdens automatische schaalvergroting, na vervanging van een machine, bij het verplaatsen van workloads tussen zones of wanneer Kubernetes een pod opstart op een host die de juiste afbeelding nog niet heeft.

Voor het starten van de container moeten de benodigde lagen uit de registry worden gehaald.

Daarom beïnvloedt de grootte van de afbeelding de zogenaamde cold start van de container, hoewel dit niet het enige component is.

Een afbeelding van 1,2 GB versus 8 MB vertegenwoordigt een verschil van 150 keer in bruto volume. De werkelijke tijdsbesparing hangt af van bandbreedte, registry-latentie, lokale lagen, disk-snelheid en parallelismemogelijkheden.

Wiskundig gezien zou het downloaden van 1,2 GB via een effectieve 100 Mbit/s-verbinding ongeveer 96 seconden duren, exclusief protocollen, compressie en andere bottlenecks. Voor 8 MB zou dat onder dezelfde omstandigheden minder dan een seconde kosten.

In productie zullen de cijfers verschillen, maar de verhoudingen verklaren waarom optimalisaties belangrijk kunnen zijn tijdens een piek in verkeer.

De Horizontal Pod Autoscaler van Kubernetes kan snel besluiten dat meer replicas nodig zijn. Als het laden van die replicas zelfs slechts enkele tientallen seconden kost door grote afbeeldingen, kan de capaciteit pas volledig terugkeren nadat de piek is afgenomen.

Deze verschil-unit geldt ook voor deploys over tientallen of honderden knooppunten.

Het gaat niet alleen om opslagruimte in een registry.

Het gaat om netwerkverkeer.

Het gaat om deployment-tijd.

En het beïnvloedt ook de hersteltijd bij fouten.

Minder inhoud betekent ook minder aanvalsvlakken, maar scratch heeft kosten

Een ander belangrijk voordeel is dat alle software die niet in een afbeelding wordt opgenomen, niet langer deel uitmaakt van het aanvalsvlak.

Als de uiteindelijke container geen compiler nodig heeft, is er weinig reden om die te installeren.

Dit geldt ook voor curl, wget, een pakketbeheerder of volledige verzameling systeemhulpmiddelen.

Docker benadrukt dat het scheiden van bouwomgeving en runtime met multi-stage builds kan leiden tot kleinere afbeeldingen en minder aanvalsvlakken.

Echter, scratch als universeel advies zou eveneens een vergissing zijn.

Een lege afbeelding bevat bijvoorbeeld geen elementen die sommige applicaties nodig hebben.

Certificaten van certificeerders (CA), tijdzones of systeembestanden kunnen vereist zijn. Een applicatie die CGO gebruikt, kan afhankelijk zijn van gedeelde bibliotheken en niet eenvoudig door het kopiëren van de binaire naar scratch werken.

Ook verdwijnen de diagnostische tools.

Binnen een scratch-container zitten meestal geen:

sh
bash
curl
ps
cat
ls

Proberen om:

docker exec -it mijn-container /bin/sh

zal niet werken, omdat /bin/sh simpelweg niet bestaat.

Vanuit een beveiligingsperspectief kan dit voordelig zijn. Vanuit operationeel oogpunt betekent het dat je andere technieken moet gebruiken voor debugging, korte levenscyclus-containers of images die specifiek voor diagnostiek zijn ontworpen.

Daarom is een minimale afbeelding niet altijd gelijk aan een lege afbeelding.

Afhankelijk van de applicatie kan het nuttiger zijn een kleinere basis te kiezen die wel de benodigde afhankelijkheden bevat, terwijl het multi-stage build wordt gebruikt om alle build-tools te verwijderen.

Het is ook verstandig om .dockerignore te reviewen. Docker raadt aan om items zoals .git, lokale artefacten of dependency-mappen die niet tijdens het buildproces hoeven te worden overgedragen, uit te sluiten.

Een simpele vraag bij het reviseren van een Dockerfile is:

Is dit bestand of pakket nodig voor het bouwen of voor het uitvoeren van de applicatie?

Als het alleen voor het bouwproces wordt gebruikt, zou het in de productie afbeelding meestal niet moeten staan.

Een kleinere afbeelding wordt niet automatisch een snellere, veiligere of efficiëntere service. Een service van 20 MB met een opstarttijd van twee minuten is mogelijk net zo snel als een van meerdere honderden megabytes die onmiddellijk reageert.

Maar het verminderen van honderden megabytes die nooit worden gebruikt, is een optimalisatie die voordelen heeft op meerdere vlakken: het vermindert dataoverdracht, opslag, distributietijd en de hoeveelheid software in de container.

Voor veel gecompileerde services begint de verbetering vaak al bij het toevoegen van een tweede FROM-laag.

Veelgestelde vragen

Wat is een multi-stage build in Docker?

Het is een Dockerfile dat meerdere FROM-instructies gebruikt om verschillende bouwfasen te scheiden. Hiermee wordt de applicatie in een bouwfase gecompileerd en slechts de benodigde bestanden overgenomen voor de uiteindelijke afbeelding.

Kan een Docker-afbeelding echt van 1,2 GB naar 8 MB gaan?

Ja, dat is mogelijk voor specifieke programma’s, vooral met kleine statische binaries, maar het is geen gegarandeerde reductie. De uiteindelijke grootte hangt af van de applicatie zelf, afhankelijkheden en benodigde runtime-bestanden.

Is het aan te raden om FROM scratch in productie te gebruiken?

Het kan geschikt zijn voor volledig statische binaries zonder systeemdependenties. Andere applicaties vereisen mogelijk certificaten, bibliotheken, tijdzones of systeemhulpmiddelen, die een robuustere basis nodig maken.

Maakt een kleinere Docker-afbeelding Kubernetes eenvoudiger sneller te schalen?

In some cases, yes. Wanneer knooppunten al de benodigde lagen in cache hebben, maakt grootte minder verschil. Maar bij nieuwe knooppunten die eerst moeten downloaden, kan het aanzienlijk schelen in opstarttijd en netwerkverkeer.

Scroll naar boven