Een goed geschreven Dockerfile is het hart van het recept dat je app overal op dezelfde manier laat draaien. Toch produceren de meeste Dockerfiles images die groter zijn dan nodig en bouwen ze alles opnieuw bij de kleinste wijziging. In deze gids lopen we stap voor stap door Dockers model van lagen (layers) en cache, en bouwen we images die kleiner, sneller en reproduceerbaar zijn.
Het lagenmodel: elke instructie is een laag
Een image bestaat uit gestapelde, alleen-lezen lagen. Elke instructie in een Dockerfile — FROM, RUN, COPY, ADD — produceert een nieuwe laag. Docker cachet deze lagen: als een instructie en de invoer ervan niet zijn veranderd, berekent Docker de laag niet opnieuw, maar hergebruikt de gecachte versie.
Het cruciale punt: als een laag verandert, wordt de cache van elke laag eronder ook ongeldig. Dingen die vaak veranderen achteraan in de Dockerfile zetten, en dingen die zelden veranderen vooraan, heeft dus direct invloed op de buildtijd. Volgorde is het halve werk.
Laten we beginnen met een eenvoudige Dockerfile
Een typisch maar gebrekkig voorbeeld voor een Node.js-app ziet er zo uit:
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Dit werkt, maar er is een probleem: omdat COPY . . het hele project kopieert, maakt het wijzigen van één regel broncode de cache van de RUN npm install-laag eronder ongeldig, zodat afhankelijkheden elke keer opnieuw worden geïnstalleerd. Terwijl als package.json niet is veranderd, de afhankelijkheden niet hoeven te veranderen.
De cache correct gebruiken: scheid afhankelijkheden
De oplossing is om eerst alleen de afhankelijkheidsmanifesten te kopiëren en te installeren, en de applicatiecode als laatste te laten:
FROM node:20
WORKDIR /app
# Eerst alleen de afhankelijkheidsbestanden
COPY package.json package-lock.json ./
RUN npm ci
# Daarna de applicatiecode
COPY . .
CMD ["node", "server.js"]
Nu draait alleen de laatste COPY . .-laag opnieuw wanneer de broncode verandert; de npm ci-laag komt uit de cache. Zolang package-lock.json niet is veranderd, worden afhankelijkheden niet opnieuw geïnstalleerd. Deze kleine herordening kan de buildtijd van minuten naar seconden brengen.
npm ci gebruiken in plaats van npm install is ook een bewuste keuze: ci volgt het lock-bestand exact, is deterministisch en betrouwbaarder voor CI-omgevingen.
Houd rommel buiten met .dockerignore
COPY . . kopieert alles — node_modules, .git, logbestanden, je lokale .env... Dit blaast de image op en verprutst onnodig de cache. Voeg een .dockerignore-bestand toe in de projectroot:
node_modules
.git
.gitignore
Dockerfile
*.log
.env
dist
coverage
Dit bestand werkt zoals .gitignore en somt paden op die nooit in de buildcontext (de bestanden die naar de Docker-daemon worden gestuurd) terecht mogen komen. Een kleinere context betekent snellere builds en een schonere image.
Multi-stage builds: gooi de buildtools eruit
De tools die nodig zijn om een app te bouwen (compilers, dev-afhankelijkheden) zijn niet nodig om hem te draaien. Een multi-stage build laat je in één fase bouwen en alleen het resultaat naar een kleine eindimage meenemen:
FROM node:20 AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Een dunne eindfase die alleen de productieoutput meeneemt
FROM node:20-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/server.js"]
Hier kopieert --from=build alleen de gecompileerde dist-map naar de tweede fase. De eindimage bevat geen buildtools of dev-afhankelijkheden, wat de imagegrootte vaak halveert of meer. Het kiezen van een dunne base-image zoals node:20-slim verkleint ook de grootte en het aanvalsoppervlak.
Praktische regels en veelgemaakte fouten
- Combineer RUN-commando's: elke
RUNis een laag. Koppel pakketinstallaties met&&en ruim op in dezelfde laag; bijvoorbeeld op een Debian-basisapt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*. - Zet versies vast:
FROM node:20is beter danFROM node:latest; gebruik een specifieke versie voor reproduceerbaarheid. - Schrijf CMD en ENTRYPOINT in exec-vorm:
CMD ["node", "server.js"]stuurt signalen (SIGTERM) correct door vergeleken met de shell-vorm, wat een nette afsluiting mogelijk maakt. - Draai niet als root: definieer in productie een
USERzodat de container als een gebruiker zonder rechten draait. - Gebruik WORKDIR: verkies
WORKDIRbovencd; het is leesbaar en blijft behouden tussen lagen.
De image bouwen en inspecteren
Zodra de Dockerfile klaar is, is de image bouwen en de grootte controleren eenvoudig:
docker build -t mijn-app:1.0 .
docker images mijn-app
docker history mijn-app:1.0
docker history toont, laag voor laag, welke instructie hoeveel ruimte inneemt; het is de meest praktische tool om te vinden wat je image opblaast. Met BuildKit (standaard in modern Docker) toont de build-uitvoer ook welke stappen CACHED waren.
Veelgestelde vragen
Moet ik een Dockerfile of Compose gebruiken?
Ze doen verschillende dingen. Een Dockerfile definieert hoe één image wordt gebouwd; docker compose definieert en draait meerdere containers samen (bijvoorbeeld een app plus een database). Compose verwijst meestal via build: naar je Dockerfile — de een vervangt dus de ander niet, ze worden samen gebruikt.
Wat is het verschil tussen COPY en ADD?
Beide kopiëren bestanden, maar ADD heeft twee extra mogelijkheden: downloaden van externe URL's en automatisch uitpakken van lokale tar-archieven. Omdat dat gedrag je kan verrassen, geef altijd de voorkeur aan COPY voor gewone bestandskopieën, en gebruik ADD alleen voor speciale behoeften zoals het uitpakken van een tarball.
Waarom is mijn image nog steeds zo groot?
De meest voorkomende oorzaken: een niet-dunne base-image (probeer slim of alpine), geen multi-stage build gebruiken, niet opruimen binnen dezelfde laag, en een ontbrekende .dockerignore. Inspecteer de lagen met docker history en richt je op de grootste.
Een efficiënte, kleine en snelle image nodig? Ik kan je bestaande Dockerfiles optimaliseren voor lagen en cache en je CI/CD-pipelines versnellen — neem contact op.