Docker voor Data Engineers

Waarom Docker het probleem oplost dat een virtual environment niet kan (OS-niveau dependencies), images en layer-caching, een Dockerfile en docker-compose voor een pipeline, containers per taak in orchestrators, en multi-stage builds.

11 min leestijd Beginner Bijgewerkt: 2026-08-16 DevOps

Introductie

Python for Data Engineers loste het "werkt bij mij niet in productie"-probleem op voor Python-dependencies via virtual environments — maar een venv isoleert alleen Python-packages, niet de Python-versie zelf, niet systeembibliotheken, niet specifieke versies van command-line tools waar een pipeline mogelijk van afhangt. Docker lost dat bredere probleem op: een container legt de volledige runtime-omgeving vast — besturingssysteem-niveau dependencies, exacte taal- en toolversies, configuratiebestanden — zodat "het werkt bij mij" wordt vervangen door "het werkt overeal waar deze container draait", zonder uitzonderingen.

Containers versus Virtual Machines

Het onderscheid is de moeite waard om precies te begrijpen, omdat het verklaart waarom containers zo lichtgewicht zijn. Een virtuele machine virtualiseert volledige hardware, inclusief een eigen kernel — zwaar, traag om op te starten (minuten), maar volledig geïsoleerd. Een container deelt de kernel van het hostbesturingssysteem en isoleert alleen het proces zelf (bestandssysteem, netwerk, processen) via kernel-features (namespaces, cgroups) — aanzienlijk lichter, start in seconden, maar met iets minder isolatie dan een volledige VM. Voor de meeste data engineering-doeleinden (een reproduceerbare, geïsoleerde runtime voor een pipeline-stap) is die iets mindere isolatie een acceptabele afweging tegen de aanzienlijke snelheids- en resourcewinst.

Images en Layers: hoe een Container Wordt Opgebouwd

Een Docker image is een onveranderlijke blauwdruk, opgebouwd uit een reeks layers — elke instructie in een Dockerfile (RUN, COPY, FROM) creëert een nieuwe layer, die Docker cachet. Bij een volgende build wordt elke layer alleen opnieuw uitgevoerd als de instructie zelf óf de bestanden die hij gebruikt, zijn veranderd — een principe dat direct de bouwsnelheid van je CI/CD-pijplijn beïnvloedt, relevant voor de build-stap uit CI/CD for Data Engineering.

FROM python:3.12-slim

WORKDIR /app

# Dependencies EERST kopiëren en installeren — deze layer wordt gecached
# en alleen herbouwd als requirements.txt zelf verandert
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Pas DAARNA de broncode kopiëren — deze layer verandert het vaakst,
# maar de dure dependency-installatie hierboven blijft gecached
COPY . .

CMD ["python", "extract_orders.py"]

De volgorde hier is bewust, niet toevallig: requirements.txt kopiëren en installeren gebeurt vóór het kopiëren van de rest van de broncode, omdat broncode veel vaker wijzigt dan dependencies. Zou je de volgorde omdraaien, dan zou elke wijziging aan de broncode — ook een kleine, cosmetische — de volledige, tijdrovende pip install-layer opnieuw triggeren, omdat Docker's layer-cache elke wijziging vroeg in de keten alle daaropvolgende layers ongeldig maakt.

Docker Compose voor Lokale Ontwikkeling

Een enkele pipeline heeft vaak een lokale database of andere afhankelijkheid nodig om tegen te ontwikkelen en testen — Docker Compose definieert meerdere samenwerkende containers in één configuratiebestand, direct relevant voor de testdata-strategieën uit CI/CD for Data Engineering:

# docker-compose.yml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: orders_test
      POSTGRES_PASSWORD: localdev
    ports:
      - "5432:5432"

  pipeline:
    build: .
    depends_on:
      - postgres
    environment:
      DB_HOST: postgres   # containers verwijzen naar elkaar via de servicenaam
docker compose up   # start beide containers, met de juiste netwerkverbindingen ertussen

Dit geeft elke engineer een identieke, geïsoleerde lokale testomgeving — een lokale Postgres-instantie die niemand anders raakt, opgezet en afgebroken in seconden, zonder een gedeelde, mogelijk door anderen gebruikte database te hoeven benaderen.

Containers per Taak in Orchestratoren

Airflow (zie Introduction to Data Engineering) en vergelijkbare orchestratoren kunnen elke individuele taak in een eigen, geïsoleerde container draaien in plaats van alles in dezelfde Python-omgeving als de orchestrator zelf — relevant zodra verschillende pipeline-stappen conflicterende dependency-versies nodig hebben (een taak die een oudere versie van een ML-library vereist naast een taak die de nieuwste versie van een andere library nodig heeft, wat in één gedeelde omgeving simpelweg niet oplosbaar is):

from airflow.providers.cncf.kubernetes.operators.pod import KubernetesPodOperator

extract_task = KubernetesPodOperator(
    task_id="extract_orders",
    image="myregistry/extract-orders:1.4.0",   # eigen, geïsoleerde image per taak
    cmds=["python", "extract_orders.py"],
    namespace="data-pipelines",
)

Elke taak draait dan in zijn eigen container, met exact de dependencies die die specifieke taak nodig heeft — volledige isolatie tussen taken binnen dezelfde pipeline, zonder dat de orchestrator zelf enige kennis hoeft te hebben van de inhoud van elke container.

Volumes: Data Overleeft de Container

Containers zijn standaard ephemeral: alles wat binnen een container wordt weggeschreven, verdwijnt zodra die container stopt en wordt verwijderd — prima voor de stateloze verwerkingslogica van een pipeline zelf, problematisch voor de Postgres-container uit het Compose-voorbeeld hierboven, waar je juist wilt dat testdata blijft bestaan tussen sessies. Een volume koppelt een map buiten de container (op de hostmachine, of beheerd door Docker zelf) aan een pad binnen de container, zodat data die daar wordt geschreven, de levensduur van de container zelf overleeft:

services:
  postgres:
    image: postgres:16
    volumes:
      - pgdata:/var/lib/postgresql/data   # data overleeft 'docker compose down'
    environment:
      POSTGRES_PASSWORD: localdev

volumes:
  pgdata:

Zonder deze volume-declaratie zou elke docker compose down gevolgd door docker compose up een volledig lege database opleveren — met de volume blijft de data behouden, terwijl de container zelf nog steeds probleemloos vervangen, herstart of herbouwd kan worden. Dit onderscheid — code en configuratie in de image, data in een volume — is een van de meest fundamentele ontwerpprincipes van containerized systemen, en het verklaart waarom "gewoon de container verwijderen en opnieuw starten" een veilige, routinematige operatie is zolang state correct in volumes leeft.

Omgevingsvariabelen en Secrets in Containers

Net als bij de CI/CD-secrets uit CI/CD for Data Engineering horen wachtwoorden en API-sleutels nooit gebakken in een image zelf — ze worden tijdens het starten van de container meegegeven, niet tijdens het bouwen ervan:

# Fout: het geheim wordt onderdeel van de image-geschiedenis, permanent zichtbaar
RUN export DB_PASSWORD=supersecret

# Goed: wordt pas bij het starten van de container geïnjecteerd, nooit in de image zelf
docker run -e DB_PASSWORD="${DB_PASSWORD}" myregistry/extract-orders:1.4.0

Voor productieomgevingen is een expliciete docker run -e-vlag met een lokaal beschikbare waarde vaak niet praktisch of veilig genoeg — orchestratoren zoals Kubernetes bieden eigen secret-objecten die als omgevingsvariabelen of gemounte bestanden in een container worden geïnjecteerd, beheerd en versleuteld los van de container-definitie zelf, vergelijkbaar in geest met de secrets-store-aanpak uit CI/CD for Data Engineering maar dan toegepast op runtime-containers in plaats van CI-pijplijnen.

Docker in de Platformen die je Al Kent

De platformen uit eerdere hoofdstukken abstraheren containers doorgaans grotendeels weg — je schrijft een dbt-model of een notebook, niet een Dockerfile — maar containers duiken op specifieke, herkenbare plekken toch weer op. Snowpark Container Services (Snowflake, zie Snowflake) laat je eigen, custom containerized workloads binnen Snowflake's beheerde infrastructuur draaien, voor logica die noch pure SQL noch Snowpark's Python-ondersteuning comfortabel dekt. Databricks Container Services laat je clusters starten vanaf een custom Docker-image in plaats van Databricks' standaard runtime-image, nuttig wanneer een team zeer specifieke, van de standaard afwijkende systeemdependencies nodig heeft die niet via een reguliere library-installatie op te lossen zijn. Het patroon is in beide gevallen hetzelfde als bij de KubernetesPodOperator hierboven: het platform beheert de orchestratie en infrastructuur, jij levert alleen het containerized deel dat specifieke, afwijkende requirements heeft.

Multi-Stage Builds: Kleinere, Veiligere Images

Een naïeve Dockerfile installeert vaak build-tools (compilers, headerbestanden) die alleen nodig zijn tijdens het bouwen, maar niet tijdens het daadwerkelijk draaien van de applicatie — wat onnodig grote images oplevert met een groter aanvalsoppervlak. Multi-stage builds scheiden een build-fase van een runtime-fase, en nemen alleen het uiteindelijke resultaat mee naar het kleinere, uiteindelijke image:

# Stage 1: bouwen, met alle benodigde build-tools
FROM python:3.12 AS builder
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# Stage 2: alleen het resultaat meenemen, geen build-tools in het uiteindelijke image
FROM python:3.12-slim
COPY --from=builder /root/.local /root/.local
COPY . .
CMD ["python", "extract_orders.py"]

Het resultaat is een aanzienlijk kleiner uiteindelijk image — sneller te downloaden bij elke deploy, minder onnodige software die potentiële kwetsbaarheden kan bevatten, zonder dat de build-fase zelf minder krachtig hoeft te zijn.

Best Practices

  • Kopieer dependency-bestanden vóór de broncode in elke Dockerfile, om Docker's layer-cache maximaal te benutten en onnodige rebuilds te vermijden.
  • Gebruik multi-stage builds standaard voor elke image die build-tools nodig heeft die niet in de uiteindelijke runtime horen.
  • Gebruik specifieke, vastgepinde image-tags (python:3.12-slim, niet python:latest) — dezelfde reden als vastgepinde dependency-versies in Python for Data Engineers: reproduceerbaarheid boven gemak.
  • Gebruik Docker Compose voor lokale ontwikkelomgevingen die een database of andere afhankelijkheid nodig hebben, in plaats van een gedeelde, mogelijk instabiele testomgeving te benaderen.
  • Draai elke pipeline-taak met conflicterende dependencies in zijn eigen container, in plaats van te proberen alle mogelijke versiecombinaties in één gedeelde omgeving te verzoenen.
  • Declareer volumes expliciet voor elke container die state moet behouden (een lokale database, opgeslagen testresultaten) — behandel het ontbreken van een volume als een bewuste keuze voor ephemeral gedrag, niet als een vergeten detail.
  • Injecteer secrets altijd bij het starten van een container, nooit tijdens het bouwen — een RUN-instructie die een geheim gebruikt, laat dat geheim permanent in de image-laag-geschiedenis staan, ook na een latere "opruim"-instructie.

Veelgemaakte Fouten

  • Broncode vóór dependencies kopiëren in een Dockerfile, waardoor elke kleine codewijziging een volledige, trage dependency-herinstallatie triggert.
  • latest als image-tag gebruiken in plaats van een specifieke versie, waardoor een build op een willekeurig moment een andere, mogelijk incompatibele basisimage oppikt.
  • Build-tools meenemen in het uiteindelijke runtime-image zonder multi-stage builds te gebruiken, wat onnodig grote, minder veilige images oplevert.
  • Eén enorme, gedeelde container proberen te bouwen die alle mogelijke dependency-versies voor elke pipeline-taak tegelijk bevat, in plaats van taken met conflicterende behoeften te isoleren.
  • Geheimen (wachtwoorden, API-sleutels) direct in een Dockerfile hardcoderen — ze blijven dan permanent in de image-geschiedenis staan, ook als een latere layer ze "verwijdert".
  • Geen volume declareren voor een lokale databasecontainer, waardoor testdata bij elke docker compose down verdwijnt en teamleden onnodig steeds opnieuw hun testomgeving moeten opbouwen.
  • Aannemen dat containers een vervanging zijn voor alle platformspecifieke compute — de meeste dagelijkse dbt/Snowflake/Databricks-werk vereist geen Dockerfile; containers zijn specifiek waardevol voor de afwijkende, isolatie-vereisende gevallen die dit hoofdstuk beschrijft.

Performance Tips

  • Gebruik .dockerignore om onnodige bestanden (lokale virtual environments, .git, testdata) buiten de build-context te houden — een kleinere context betekent een snellere build.
  • Combineer gerelateerde RUN-instructies waar mogelijk tot één layer, om het totale aantal layers en de bijbehorende overhead te beperken.
  • Gebruik een -slim- of -alpine-basisimage waar compatibel, in plaats van een volledige, zware basisimage die functionaliteit bevat die een pipeline nooit gebruikt.
  • Cache dependency-layers expliciet in CI (via registry-caching of vergelijkbare mechanismen van je CI-platform) zodat niet elke CI-run vanaf nul dezelfde dependencies opnieuw hoeft te downloaden en installeren.
  • Vermijd onnodig veel, kleine volumes voor data die eigenlijk prima ephemeral kan zijn — elke volume is een stukje state dat iemand op enig moment moet begrijpen, beheren en opruimen.

Interviewvragen

"Wat is het verschil tussen een container en een virtuele machine?" Let op: een container deelt de kernel van het hostsysteem en isoleert alleen het proces (lichter, sneller); een VM virtualiseert volledige hardware inclusief een eigen kernel (zwaarder, meer geïsoleerd).

"Waarom is de volgorde van instructies in een Dockerfile belangrijk?" Let op: Docker's layer-cache maakt elke wijziging vroeg in de keten alle daaropvolgende layers ongeldig — dependencies vóór broncode kopiëren voorkomt onnodige, trage rebuilds bij elke kleine codewijziging.

"Waarom zou je Docker gebruiken als je al een Python virtual environment hebt?" Let op: een venv isoleert alleen Python-packages, niet OS-niveau dependencies, systeembibliotheken, of de Python-versie zelf — Docker legt de volledige runtime-omgeving vast, niet alleen een deel ervan.

"Wat is een multi-stage build, en welk probleem lost het op?" Let op: het scheidt een build-fase (met build-tools) van een runtime-fase, zodat alleen het uiteindelijke resultaat — niet de build-tools zelf — in het uiteindelijke, kleinere image terechtkomt.

"Waarom zou je in een orchestrator elke pipeline-taak in een eigen container laten draaien in plaats van in één gedeelde omgeving?" Let op: verschillende taken kunnen conflicterende dependency-versies nodig hebben die in één gedeelde omgeving simpelweg niet naast elkaar kunnen bestaan — eigen containers per taak lossen dat isolatieprobleem op.

"Wat is een Docker volume, en waarom heb je die nodig voor een lokale databasecontainer?" Let op: containers zijn standaard ephemeral — alles binnen de container verdwijnt bij verwijdering; een volume koppelt een pad buiten de container aan een pad erbinnen, zodat data (zoals databestanden) de levensduur van de container overleeft.

"Hoe zou je een wachtwoord veilig aan een container meegeven, zonder het in de image zelf te bakken?" Let op: injecteren tijdens het starten van de container (via -e of een orchestrator-specifiek secret-mechanisme), nooit via een RUN-instructie in de Dockerfile die het geheim permanent in de image-geschiedenis vastlegt.

"Waar duiken containers op binnen platformen als Snowflake en Databricks, ook al schrijf je meestal geen Dockerfile?" Let op: Snowpark Container Services voor custom workloads binnen Snowflake, Databricks Container Services voor clusters met een aangepaste basisimage — het platform beheert de orchestratie, jij levert alleen het containerized deel met afwijkende requirements.

Relevante Documentatie

Samenvatting

Docker lost een breder probleem op dan een Python virtual environment: het legt de volledige runtime-omgeving vast, niet alleen taalspecifieke dependencies, via lichte, kernel-gedeelde containers in plaats van zware, volledig gevirtualiseerde machines. Layer-caching maakt de volgorde van Dockerfile-instructies direct relevant voor buildsnelheid; Docker Compose geeft elke engineer een identieke, geïsoleerde lokale ontwikkelomgeving. In orchestratoren laat het draaien van elke taak in zijn eigen container conflicterende dependency-versies tussen pipeline-stappen naast elkaar bestaan, en multi-stage builds houden uiteindelijke images klein door build-tools buiten de runtime te houden. Volumes bewaren state waar dat nodig is, secrets horen bij het starten geïnjecteerd te worden, nooit gebakken in een image, en zelfs platformen als Snowflake en Databricks vallen voor hun meest afwijkende workloads terug op ditzelfde containerprincipe. Met DevOps-bouwstenen — CI/CD, Git, Docker — nu behandeld, verschuift het volgende hoofdstuk terug naar de data zelf: Data Quality & Testing.

Hulp nodig bij het bouwen van een modern dataplatform?

DataPartner365 helpt organisaties met Microsoft Fabric, Snowflake, Databricks, dbt, Azure, data-architectuur en CI/CD.

Neem contact op met DataPartner365