dev@laptop:~/sessie$ cat docker-101.md
Docker
van image → registry
Eén uur. Mentaal model + handen op de bak.
Doel: snappen waarom een image draait, niet alleen dat je 'm kunt pushen.
De casus: lokaal bouwen en pushen lukt — totdat de CI/CD het overneemt en niets meer bouwt.
— gebruik → / spatie om verder te gaan, F voor fullscreen.
dev@laptop:~/sessie$ why --are-we-here
De casus
Een team bouwt en pusht lokaal probleemloos images. Tóch loopt het vast zodra de CI/CD het overneemt: de pipeline weigert te bouwen.
"Een image kunnen pushen" is niet hetzelfde als "snappen hoe een container runtime werkt". Ad-hoc lokale flows werken prima — totdat je reproduceerbaarheid nodig hebt.
En dat moment is precies dit: CI / CD. De gap dook niet toevallig op tijdens de migratie — dáár breekt een wankel mentaal model. Dat repareren we hier.
dev@laptop:~/model$ whatis container
Een container is geen VM
Een VM draait een eigen kernel bovenop een hypervisor. Zwaar, traag te starten, volledige OS.
Een container deelt de host-kernel en draait gewoon native processen, geïsoleerd met namespaces + cgroups.
Geen bytecode. Geen interpreter ertussen. Het proces in de container draait rechtstreeks op de kernel van de host.
Gevolg: een image is gebonden aan een CPU-architectuur. amd64 ≠ arm64. Onthoud dat — slide 5.
dev@laptop:~/model$ whatis image vs container
Image ≠ container
IMAGE = recept
- Stapel read-only filesystem-layers
- + config-metadata: ENTRYPOINT/CMD, env, poorten, workdir
- Bevroren. Wordt nooit aangepast.
CONTAINER = gerecht
- Een draaiende instance van een image
- + een dunne writable layer bovenop
- Wijzigingen leven daar — en zijn weg bij rm.
Image = "hoe ziet het eruit + hoe start ik het". Container = "het draait nu, met een kladlaag erbovenop".
dev@laptop:~/model$ debunk bytecode-mythe
De twee misverstanden
exit 1
"Image = bytecode"
- Er is geen VM die het interpreteert.
- Een image is een filesystem + startinstructie, geen draaibare bytecode.
exit 1
"Een image draait overal"
- Een image is architectuur-gebonden.
- amd64-image op een arm64-host = crash of trage emulatie.
- Oplossing: multi-arch manifest (één tag, meerdere platforms).
Dit is waarom een build die lokaal werkt soms in CI op een andere node faalt.
dev@laptop:~/model$ explain layers
Layers stapelen
Een union filesystem (overlayfs) presenteert losse read-only layers als één geheel.
Elke Dockerfile-instructie ≈ een nieuwe layer. Layers worden gecachet en hergebruikt.
Daarom: kopieer eerst je dependency-manifest en installeer, dan pas je broncode. Wijzig je code, dan blijft de install-layer uit cache.
writable layer (container)
COPY . . — broncode
RUN install deps
COPY manifest
FROM base-image
Onder → boven. Alleen de bovenste laag is schrijfbaar, en hoort bij de container — niet bij de image.
dev@laptop:~/basics$ docker --help | head
De lifecycle in commands
docker pull nginx:1.27 # image ophalen
docker run -d --name web nginx:1.27 # container starten uit image
docker ps # wat draait er
docker exec -it web sh # shell ín de draaiende container
docker logs web # stdout/stderr
docker stop web && docker rm web # stoppen + opruimen
run = create + start. exec opent een proces in een container die al draait — niet hetzelfde als run.
dev@laptop:~/basics$ docker commit # demo — let op
demo · 1× laten zien, daarna nooit meer
Container → image, met de hand
docker run -it --name kladje ubuntu:24.04 bash
root@a1b2c3:/# apt-get update && apt-get install -y cowsay
root@a1b2c3:/# exit
docker commit kladje mijn-cowsay:eigenbouw
sha256:9f8e... # de writable layer is nu bevroren tot een image
Dit werkt. Je voelt nu wat die writable layer is: je hebt 'm bevroren tot een nieuwe image-layer. Onthoud dit gevoel — en op de volgende slide leg ik uit waarom je dit nooit in CI moet doen.
dev@laptop:~/basics$ docker commit; echo $?
Waarom commit faalt in CI
exit 1
Wat er mis is
- Niet reproduceerbaar: niemand weet wat je intypte.
- Niet auditeerbaar: geen broncode, geen diff, geen review.
- Niet in de repo: CI heeft niets om te bouwen.
Dít is precies waar de pipeline op breekt. Bestaat een image alleen omdat iemand 'm lokaal commit-te, dan vindt de CI geen Dockerfile — of bouwt iets anders dan er lokaal draaide.
"Boring & auditable" begint hier: een build die je in een ISO 27001-audit kunt uitleggen, niet "Henk heeft 'm ooit op z'n laptop gemaakt".
dev@laptop:~/build$ cat Dockerfile
Dockerfile = de juiste manier
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
USER 1000
CMD ["python", "app.py"]
Declaratief · in versiebeheer · reproduceerbaar. Iedereen bouwt dezelfde image uit dezelfde regels.
En dit is exact wat CI bouwt — of het nu GitHub Actions of Forgejo Actions is. Geen Dockerfile in de repo = geen werkende pipeline.
dev@laptop:~/build$ build && tag && push
Bouwen → taggen → pushen → pullen
docker build -t mijn-app:0.1.0 . # bouw uit Dockerfile
docker tag mijn-app:0.1.0 \
docker.io/gongoeloe/mijn-app:0.1.0 # registry-pad + versie
docker login
docker push docker.io/gongoeloe/mijn-app:0.1.0
docker pull docker.io/gongoeloe/mijn-app:0.1.0 # elders, of in CI
Een tag is registry / repository : versie. Geen versie opgeven → Docker vult :latest in. Daar gaan we het op de volgende slide over hebben.
dev@laptop:~/best-practices$ lint Dockerfile
Do's & don'ts
exit 0
- Pin versies: :1.27 of digest, nooit :latest in prod.
- Multi-stage build: bouw-tools in stage 1, alleen de binary in de runtime-image.
- Non-root: USER 1000.
- .dockerignore: geen .git, secrets of node_modules in de build-context.
exit 1
- :latest in prod — je weet nooit wat draait.
- Secrets in een layer. Een layer is voor altijd: ook al "verwijder" je 'm later, hij zit nog in de history. (→ SOPS/age, geen ENV PASSWORD=.)
- Dikke base-image met half Ubuntu erin als je een statische binary hebt.
dev@laptop:~/tips$ docker volume ls
Tips & tricks
Named volumes ≠ bind mounts
- Named volume: door Docker beheerd, voor data (db). Overleeft de container.
- Bind mount: host-pad, voor je broncode tijdens dev.
compose & data
docker compose down # containers + netwerk weg
# named volumes BLIJVEN
docker compose down -v # -v gooit óók de volumes weg
Data is dus losgekoppeld van de container-lifecycle. Een down wist je database niet — pas -v doet dat.
dev@laptop:~/lab$ ./hands-on.sh # 30 min
live · samen doen
Aan de slag
Maak een account op hub.docker.com (of gebruik een zelf-gehoste registry).
Schrijf een mini Dockerfile (slide 10 als basis). Hou 'm klein.
docker build → tag → login → push → pull op een andere machine.
Bonus: doe de commit-flow van slide 8 één keer — en voel waarom je 'm daarna nooit meer wilt.
Loop je vast: error eerst lezen, dan vragen. docker logs en docker inspect zijn je vrienden.
dev@laptop:~/sessie$ recap && exit 0
De kern
- Image = read-only layers + startinstructie. Container = draaiende instance + kladlaag.
- Native op de host-kernel — geen bytecode, en architectuur-gebonden.
- docker commit = anti-pattern. Dockerfile = reproduceerbaar = wat CI bouwt.
- Geen Dockerfile in de repo → geen werkende CI-pipeline. Dat was de hele puzzel.
Boring & auditable: een build die je in een audit kunt uitleggen. Niet "Henk's laptop".
Vragen?