Beetje Coderen

Les 3 · Module 1 · Compose goed onder de knie

Images goed bouwen: tags, lagen en grootte

Maak de teller-image klein, snel te bouwen en veilig: laagvolgorde en cache, .dockerignore, multi-stage, niet als root, betekenisvolle tags en digests.

⏱ 70 minuten leestijd + oefenen

Illustratie: gestapelde dozen in lagen naast een schip, een image opgebouwd uit lagen

🎯 Na deze les kun je

  • uitleggen hoe lagen en de buildcache werken
  • een Dockerfile ordenen voor snelle herbuilds
  • de image kleiner en veiliger maken met slim, multi-stage en een gewone gebruiker
  • images taggen met versies en vastpinnen op een digest

Kubernetes haalt je image op elke node op waar een pod start. Een image van 1 GB betekent trage starts en trage updates; een image die als root draait, betekent meer schade bij een lek. In deze les maak je de teller-image productieklaar.

Lagen en cache

Elke RUN, COPY en ADD maakt een laag. Docker hergebruikt een laag uit de cache zolang de instructie én alles daarvóór ongewijzigd is. Verandert één laag, dan worden alle lagen erna opnieuw gebouwd.

# Slecht: elke codewijziging installeert alle pakketten opnieuw
COPY . .
RUN pip install -r requirements.txt

# Goed: pakketten alleen opnieuw als requirements.txt verandert
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .

Voeg een .dockerignore toe, zodat COPY en de build-context klein blijven:

.git
__pycache__
*.pyc
.env
compose*.yml

Klein en veilig

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS build
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=build /install /usr/local
COPY app.py .
RUN useradd --uid 10001 --no-create-home teller
USER 10001
EXPOSE 8080
CMD ["gunicorn", "-b", "0.0.0.0:8080", "-w", "2", "app:app"]
  • slim in plaats van de volledige python-image scheelt honderden MB's.
  • Een multi-stage build houdt buildgereedschap en caches uit de eindimage.
  • USER 10001: een numerieke gebruiker, zodat Kubernetes kan controleren dat het geen root is (runAsNonRoot, les 18).
docker build -t teller:1.1 .
docker image ls teller
docker history teller:1.1

Voorspel eerst: je doet RUN apt-get update && apt-get install -y gcc in de ene laag en RUN apt-get purge -y gcc in de volgende. Wordt de image kleiner?

Uitkomst

Nee. Een laag kan bestanden alleen verbergen voor latere lagen; de bytes van gcc zitten nog steeds in de eerdere laag en worden mee gedownload. Opruimen moet in dezelfde RUN (apt-get install … && … && apt-get purge …), of beter: bouw in een aparte stage en kopieer alleen het resultaat. Hetzelfde geldt voor geheimen: een bestand in een vroege laag verwijderen haalt het niet uit de image.

Tags en digests

docker tag teller:1.1 teller:1.1.0
docker tag teller:1.1 teller:$(git rev-parse --short HEAD)
  • Gebruik geen latest in productie: je weet niet welke versie draait, en een herstart kan stilletjes een nieuwe versie ophalen.
  • Een tag is verplaatsbaar; een digest (teller@sha256:3f4a…) niet. Voor maximale zekerheid pin je op een digest.
  • Een goede gewoonte: semver-tag voor mensen (1.1.0) én de git-commit voor herleidbaarheid.

Basisimages vastpinnen

FROM python:3.12-slim verandert als er een beveiligingsupdate is. Dat is meestal goed, maar maakt builds niet reproduceerbaar. Pin desgewenst op een digest en laat een tool als Renovate of Dependabot updates voorstellen.

Typische fouten

  • Image van 1 GB+: volledige basisimage, geen .dockerignore, of buildtools in de eindimage.
  • PermissionError na USER: de app schrijft naar een map die van root is. Schrijf naar /tmp of geef de map met chown aan de gebruiker.
  • Code-wijziging niet zichtbaar: je draait nog de oude tag, of Compose bouwde niet opnieuw (--build).

Oefenen

  1. Noteer de grootte van teller:1.0. Bouw de nieuwe Dockerfile als teller:1.1 en vergelijk.
  2. Wijzig alleen app.py en bouw opnieuw. Welke stappen komen uit de cache?
  3. Controleer met docker run --rm teller:1.1 id dat de container niet als root draait.
  4. Tag de image met je git-commit en zoek de digest op met docker inspect --format '{{.Id}}' teller:1.1.
Modelantwoord oefening 1

Typisch: rond de 200 MB voor 1.0 (één stage, pip-cache) tegen zo'n 150–170 MB voor 1.1. Het grootste deel is de Python-basis zelf.

Modelantwoord oefening 2

De pip install-laag komt uit de cache (CACHED); alleen COPY app.py . en de lagen daarna worden opnieuw gemaakt, in een paar seconden.

Modelantwoord oefening 3

uid=10001 gid=0(root) groups=0(root) of met eigen groep; in elk geval niet uid=0. De gebruiker heeft geen rootrechten.

Modelantwoord oefening 4

docker inspect toont het image-id (sha256:…). De repo-digest die je in Kubernetes gebruikt, krijg je pas na een push naar een registry (les 4).

Lokaal oefenen

Geen live lab. Met docker buildx du zie je hoeveel buildcache je hebt; docker builder prune ruimt hem op.

Samenvatting

  • Volgorde in je Dockerfile bepaalt de cache: eerst afhankelijkheden, dan code.
  • Klein en veilig: slim-basis, multi-stage, .dockerignore, numerieke niet-root-gebruiker.
  • Opruimen in een latere laag maakt niets kleiner.
  • Versietags en commits, geen latest; digests voor zekerheid.
  • Volgende les: een registry gebruiken.

📝 Quiz: Images goed bouwen: tags, lagen en grootte

5 vragen · 70% nodig om te slagen

1. Wanneer wordt een laag opnieuw gebouwd?
2. Wordt een image kleiner als je gcc in een latere laag verwijdert?
3. Waarom geen latest in productie?
4. Waarom een numerieke USER?
5. Wat is het verschil tussen een tag en een digest?