aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Tools & DevOps

Docker image verkleinen: multi-stage en Alpine-gids

Een Docker image verkleinen heeft directe invloed op zowel de deploysnelheid als de veiligheid. Een image van meerdere gigabytes verlengt buildtijden, verhoogt registrykosten, vertraagt deploys en vergroot het aanvalsoppervlak doordat het pakketten meelevert die je nooit gebruikt. Het goede nieuws: in de meeste projecten kun je de imagegrootte met 70-80% verkleinen met een handvol kerntechnieken. In deze gids behandel ik multi-stage builds, op alpine gebaseerde images, .dockerignore en laagoptimalisatie met praktische voorbeelden.

Waarom is imagegrootte belangrijk?

Je betaalt voor een opgeblazen image in elke fase. Grote images kosten meer tijd om naar de registry te pushen, laten je langer wachten in de CI/CD-pipeline en blazen het verkeer op bij het pullen naar productie. In autoscaling-omgevingen downloadt elke nieuwe pod of container de image vanaf nul; het verschil tussen 50 MB en 1,2 GB wordt minuten over honderden containers.

  • Snelheid: een kleinere image betekent snellere pulls, pushes en cold starts.
  • Veiligheid: minder pakketten betekent minder CVE's en een kleiner aanvalsoppervlak.
  • Kosten: registryopslag en netwerkverkeer schalen recht evenredig met de grootte.

De juiste base image kiezen

De eenvoudigste winst komt uit de keuze van je base image. De meeste officiële taalimages zijn gebouwd op een volledige Debian en bevatten buildtools, documentatie en elke uitbreiding van de taal. In de meeste gevallen heb je alleen de runtime nodig.

  • alpine: een piepkleine distributie op basis van musl libc van ~5 MB. Varianten als node:20-alpine of python:3.12-alpine zijn een fractie van de volledige versie.
  • slim: varianten als python:3.12-slim zijn op Debian gebaseerd maar ontdaan van onnodige pakketten. Een goede tussenweg als je het musl-compatibiliteitsrisico van Alpine wilt vermijden.
  • distroless: Google's gcr.io/distroless-images bevatten alleen je app en de runtime; er is zelfs geen shell. Een van de kleinste en veiligste opties voor productie.

Een kanttekening: omdat Alpine musl libc gebruikt, kan het buildproblemen veroorzaken bij native extensies die afhankelijk zijn van glibc (vooral pakketten met C-extensies in Python, zoals numpy of pandas). In zulke gevallen verloopt slim soepeler.

Scheid build en runtime met multi-stage builds

De krachtigste techniek om images te verkleinen is de multi-stage build. Het idee is simpel: je compileert de app in een "builder"-fase met alle buildtools en kopieert vervolgens alleen de geproduceerde output naar een schone, kleine eindfase. Compilers, dev-afhankelijkheden en tussenbestanden komen nooit in de uiteindelijke image.

Een typisch voorbeeld voor een Node.js-app:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
ENV NODE_ENV=production
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
CMD ["node", "dist/index.js"]

Hier laat npm ci --omit=dev alleen de productieafhankelijkheden in de uiteindelijke image achter, terwijl COPY --from=builder enkel de gecompileerde dist-map verplaatst. Alles wat buildspecifiek is, blijft in de eerste fase en wordt weggegooid.

Voor gecompileerde talen is de winst nog groter. Het Go-voorbeeld toont de kracht van een statische binary die je op een scratch-image plaatst:

FROM golang:1.22 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app/server ./cmd/server

FROM gcr.io/distroless/static-debian12
COPY --from=build /app/server /server
ENTRYPOINT ["/server"]

Met deze aanpak slinkt een buildomgeving van honderden megabytes tot een enkele binary van enkele megabytes.

Optimaliseer het aantal lagen en hun inhoud

Elke RUN-, COPY- en ADD-opdracht maakt een nieuwe laag, en data die in één laag wordt toegevoegd, blijft in de imagegeschiedenis staan, zelfs als een latere laag die verwijdert. Daarom moet het opschonen binnen dezelfde RUN gebeuren:

RUN apt-get update \
 && apt-get install -y --no-install-recommends curl ca-certificates \
 && rm -rf /var/lib/apt/lists/*

Hier slaat --no-install-recommends aanbevolen maar onnodige pakketten over, terwijl rm -rf /var/lib/apt/lists/* de apt-cache in dezelfde laag wist. Als je dit over aparte regels verdeelt, blijft de cache in de image.

Een ander cruciaal punt voor laagcaching is de volgorde van opdrachten: kopieer eerst wat zelden verandert (afhankelijkheidsmanifesten) en daarna wat vaak verandert (broncode). Door COPY package*.json ./ vóór COPY . . te plaatsen, draait npm ci niet opnieuw telkens als de code verandert.

Sluit het overbodige uit met .dockerignore

Elk bestand dat naar de buildcontext wordt gestuurd kan in de image lekken en de build vertragen. Het .dockerignore-bestand werkt als .gitignore en levert vaak een van de snelste winsten op:

.git
node_modules
dist
*.log
.env
.env.*
coverage
Dockerfile
docker-compose.yml
README.md

Vooral het uitsluiten van node_modules en .git voorkomt zowel een opgeblazen buildcontext als het per ongeluk kopiëren van grote mappen. Het uitsluiten van .env-bestanden is ook essentieel voor de veiligheid.

Meet en analyseer de grootte

Meet tijdens het optimaliseren om te zien wat echt werkt. De opdracht docker images geeft een snelle vergelijking:

docker images myapp --format "{{.Repository}}:{{.Tag}} {{.Size}}"

Voor diepere analyse is de tool dive onmisbaar; hij toont de grootte van elke laag en welke bestanden zijn toegevoegd, zodat je de lagen kunt opsporen die de "efficiëntie" schaden. BuildKit aan houden (DOCKER_BUILDKIT=1, of standaard in modern Docker) zorgt ook voor parallelle en efficiëntere builds. Om de buildcache slimmer te benutten, kun je een persistente afhankelijkheidscache aanhouden met RUN --mount=type=cache.

Veelgestelde vragen

Is Alpine altijd de beste keuze?

Nee. Alpine is erg klein, maar omdat het musl libc gebruikt, kun je buildproblemen tegenkomen bij sommige native pakketten en, zelden, verschillen in DNS-gedrag. Heb je een Python/Node-project dat zwaar leunt op native afhankelijkheden, dan verloopt de slim-variant meestal soepeler en is hij nog altijd veel kleiner dan de volledige image.

Vertragen multi-stage builds de CI/CD?

In de praktijk niet. De builder-fase wordt gecachet en draait alleen opnieuw als afhankelijkheden of broncode veranderen. Dankzij de kleine resulterende image worden push- en pulltijden korter; in totaal wordt de pipeline meestal sneller.

Ik heb de image verkleind maar hij is nog steeds groot, wat kan ik nog doen?

Zoek de grootste lagen met dive. De boosdoener is meestal een niet-gewiste apt/npm-cache, onnodige dev-afhankelijkheden, node_modules die in de buildcontext lekt, of grote statische bestanden die worden gekopieerd. De eindfase omzetten naar distroless of scratch levert extra winst op.

Slokken je images nog steeds gigabytes op? Heb je hulp nodig om je Dockerfile naar multi-stage te migreren en je CI/CD-pipeline te versnellen, neem dan contact met me op.

Bu kategorideki tüm yazılar →

Devamı için