Un job de date în 2026 aproape sigur rulează într-un container. Executor-ul Spark pe Kubernetes, task-ul Airflow care face ridicatul greu, materializarea unui asset Dagster, rularea de dbt dintr-un pipeline CI: toate sunt containere, pornite din imagini, programate de ceva. Chiar și platformele gestionate (Databricks, Snowpark de la Snowflake, AWS Glue) sunt containere dedesubt; doar nu vezi cusăturile.
Lecția asta e fundamentele de containere pe care fiecare data engineer ar trebui să le știe. Modelul mental, pattern-urile de Dockerfile care contează, lucrurile care merg prost pe drumul către o imagine mică și reproductibilă, registry-urile unde locuiesc imaginile și use case-urile specifice datelor care leagă totul.
Containere și ce nu sunt
Un container nu e o mașină virtuală. Distincția contează pentru că modelează atât costurile, cât și modurile de eșec.
O mașină virtuală are propriul kernel, propriul proces de boot, propria alocare de memorie, adesea gigabyte de disk. Hypervisor-ul pretinde că VM-ul e un calculator real. Pornirea unui VM ia minute; rularea a zece VM-uri pe o mașină costă de zece ori overhead-ul de kernel.
Un container împarte kernel-ul host-ului. E un proces (sau un grup mic de procese) care rulează în namespace-uri izolate, cu un filesystem care e al lui și o limită CPU/memorie impusă de cgroups. Pornirea unui container ia milisecunde. Rularea a cincizeci de containere pe o mașină e normală. Izolarea e mai slabă decât un VM (un exploit de kernel scapă din orice container; rămâne în VM), dar pentru use case-ul „vreau să rulez codul meu într-un mediu reproductibil”, containerele sunt primitiva potrivită.
Cele două concepte cheie de ținut separate sunt imaginea și containerul. Imaginea e artefactul imutabil: un filesystem stratificat cu un fișier de metadata care descrie cum se pornește un proces în interiorul lui. Containerul e o instanță care rulează a unei imagini, cu propriul layer scriptibil deasupra. Construiești imagini. Rulezi containere. Un container nou din aceeași imagine pornește în același state curat.
Docker e implementarea originală a acestor idei, dominantă din 2013. Open Container Initiative a standardizat formatele astfel încât imaginile construite cu Docker să ruleze oriunde se vorbește OCI: containerd, podman, CRI-O, fiecare serviciu de containere al fiecărui cloud. Când data engineers spun „imagine Docker” de obicei vor să spună „imagine OCI”; formatele sunt aceleași.
Dockerfile-ul
Un Dockerfile e rețeta pentru construirea unei imagini. Fiecare instrucțiune creează un layer în filesystem-ul imaginii. Layer-ele sunt cache-uite, deci dacă schimbi o instrucțiune, doar layer-ele de la acel punct în jos sunt reconstruite.
Un Dockerfile naiv pentru un job Python de date:
FROM python:3.13
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "job.py"]
Asta funcționează și e greșit în trei feluri diferite. Imaginea de bază e mai mare decât trebuie să fie. Ordonarea layer-elor înseamnă că o modificare de cod invalidează layer-ul de instalare a dependențelor. Nu există pinning de versiuni. O să le reparăm pe toate.
Best practices care își câștigă pâinea
Folosește imagini de bază specifice și minime. python:3.13-slim în loc de python:latest. Variantele slim renunță la unelte de build și la majoritatea pachetelor de sistem, păstrând doar ce e necesar pentru a rula un interpreter Python. Tag-ul latest e o țintă mobilă; pin-uiește o versiune specifică ca un rebuild peste luni să producă același artefact. Pentru imagini și mai mici există variante distroless sau Alpine, cu compromisul că unele wheel-uri Python nu au binare prebuilt pentru ele și pici înapoi pe build din sursă.
Costul alegerii unei imagini de bază mai mici se cumulează. O imagine de 50 MB se trage mai repede, se scanează pentru vulnerabilități mai repede, are o suprafață de atac mai mică și costă mai puțin în storage de registry. Pentru un job care rulează pe o sută de executor-i Spark, cei 50 MB sunt înmulțiți cu o sută de fiecare dată când cluster-ul pornește.
Ordonează layer-ele după frecvența modificărilor. Lucrurile care se schimbă rar (pachete OS, dependențe de sistem, interpreter Python) merg aproape de începutul Dockerfile-ului. Lucrurile care se schimbă des (codul tău) merg aproape de final. Cache-ul de build Docker invalidează un layer și tot ce e dedesubt când input-urile lui se schimbă. Dacă copiezi codul înainte de a instala dependențele, fiecare modificare de cod invalidează instalarea dependențelor, iar build-ul tău ia zece minute când ar putea lua zece secunde.
Ordonarea convențională pentru Python:
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "job.py"]
Copierea requirements.txt e propriul ei pas; pip install e propriul lui pas. Copia de cod vine după. Editarea unui fișier Python reconstruiește doar ultimele două layer-e, în milisecunde.
Pin-uiește versiunile dependențelor. requirements.txt cu hash-uri, sau un lockfile uv, sau poetry.lock de la Poetry, sau pip-tools. Indiferent de unealtă, lockfile-ul e contractul: exact acest arbore de dependențe, de fiecare dată. Un requests ne-pin-uit în requirements-urile tale devine o versiune diferită luna viitoare, iar imaginea ta devine brusc diferită în feluri pe care nimeni nu le-a cerut.
Nu rula ca root. Default-ul în majoritatea imaginilor de bază e root, și e un footgun. Un proces compromis care rulează ca root într-un container are o șansă mult mai bună să scape sau să se miște lateral decât același proces care rulează ca utilizator neprivilegiat. Cele două linii în plus:
RUN useradd --create-home --shell /bin/bash appuser
USER appuser
aproape de finalul Dockerfile-ului mută procesul care rulează la un utilizator non-root. Pentru majoritatea job-urilor de date asta nu costă nimic și elimină o categorie de risc.
Folosește .dockerignore. Contextul de build e tot ce se află în directorul de unde ai rulat docker build; fără un .dockerignore, un director rătăcit node_modules sau .venv sau data/ poate balona contextul de build la gigabytes. Fișierul funcționează ca .gitignore. Adaugă intrările evidente: .git, __pycache__, *.pyc, .venv, node_modules, orice directoare locale de date.
Multi-stage builds
Cea mai mare îmbunătățire singulară pentru dimensiunea imaginii e multi-stage build-ul. Ideea: un singur Dockerfile definește două (sau mai multe) imagini, unde stage-urile ulterioare copiază artefacte de la cele anterioare, iar doar stage-ul final devine imaginea publicată.
Un pattern tipic pentru un job Python care are un pas de build (compilare extensii C, generare fișiere):
FROM python:3.13 AS builder
WORKDIR /build
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
FROM python:3.13-slim AS runtime
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*.whl
COPY . .
RUN useradd --create-home appuser
USER appuser
CMD ["python", "job.py"]
Stage-ul builder are imaginea Python completă cu compilatoare. Construiește wheel-urile. Stage-ul runtime pornește proaspăt din python:3.13-slim și doar copiază wheel-urile. Imaginea finală nu are compilatoare, nu are unelte de build, niciun cache. Mai mică, mai rapidă de tras, mai puțin de scanat.
Multi-stage strălucește pentru limbajele cu pași de build expliciți: Go, Java, Rust, TypeScript care compilează în JavaScript. Pentru muncă pură de Python economiile sunt mai mici, dar reale, pentru că pip install din wheel-uri prebuilt în stage-ul de runtime e mai rapid și mai ușor decât instalarea completă.
Registry-uri de imagini
O imagine trebuie să trăiască undeva pentru ca cluster-ul să o tragă. Un registry e stratul de stocare și distribuție: Docker Hub, GitHub Container Registry, GitLab Registry, AWS ECR, Google Artifact Registry, Azure ACR, JFrog, Harbor.
Alegerea depinde mai ales de unde locuiește restul infrastructurii. Workload-urile AWS trag din ECR. GCP din Artifact Registry. Echipele centrate pe GitHub folosesc GitHub Container Registry, care se autentifică cu aceleași token-uri ca restul GitHub. Docker Hub e ok pentru imagini open-source, dar are limite de rate care lovesc rapid cluster-ele de producție; pin-uiește un registry cloud specific pentru producție.
Strategia de tag-uri contează mai mult decât realizează oamenii. Tag-ul latest e mutabil; nu-l folosi în producție. Folosește tag-uri imutabile: un git commit SHA, un semver, un build number. Cluster-ul trage un tag specific, cluster-ul rulează o imagine specifică, deploy-ul e reproductibil. Dacă vreodată ai nevoie de rollback, redeploy-uiești tag-ul anterior și știi exact ce rulează.
Semnează-ți imaginile și scanează-le. Cosign pentru semnare, Trivy sau Grype pentru scanare. Semnătura îi dă cluster-ului un fel de a refuza imagini care n-au venit din CI-ul tău; scanner-ul îți dă o listă de vulnerabilități cunoscute în dependențele tale. Ambele ar trebui să ruleze în CI înainte ca imaginea să fie împinsă.
Ce înseamnă asta pentru job-urile de date
Docker apare în data engineering în trei locuri principale.
Spark pe Kubernetes e exemplul cel mai curat. Driver-ul și executor-ii Spark rulează ca pod-uri, pornite dintr-o imagine custom care conține codul tău PySpark, dependențele tale și distribuția Spark. Imaginea e construită în CI, împinsă într-un registry, referențiată în CR-ul SparkApplication sau în comanda spark-submit. Aceeași imagine rulează în dev, în CI și în prod, cu configurări diferite. Imaginea e unitatea de deploy. Lecția 55 acoperă partea de Kubernetes în detaliu.
Operatorii Airflow care rulează cod de utilizator în containere sunt al doilea loc. KubernetesPodOperator e exemplul canonic: scheduler-ul Airflow îi cere lui Kubernetes să ruleze un pod dintr-o imagine specificată, cu argumente specificate, și așteaptă să se termine. DockerOperator face același lucru pentru Docker simplu. Beneficiul e izolarea completă: dependențele unui task nu interferează cu dependențele altuia. Costul e overhead-ul mic per task de pornire a unui container.
Dezvoltarea locală reproductibilă e al treilea loc, și e cel mai subutilizat. Aceeași imagine pe care o rulezi în producție poate rula pe laptopul unui developer, cu aceeași versiune de Python, aceleași dependențe, același cod. Bug-urile care apar doar în producție nu se mai întâmplă, pentru că mediul de dev e mediul de producție. O comandă docker run care montează directorul de cod și pornește job-ul local e genul de chestie mică ce schimbă felul în care lucrează o echipă.
Ciclul build-and-deploy leagă totul:
flowchart TB
CODE[Dockerfile plus code] --> BUILD[docker build]
BUILD --> SCAN[Scan and sign]
SCAN --> PUSH[Push to registry]
PUSH --> PULL[Cluster pulls image]
PULL --> RUN[Job runs]
RUN --> METRICS[Metrics and logs]
O mică checklist de Dockerfile
Înainte de a face merge unui Dockerfile, lucrurile la care să te uiți:
- Imagine de bază specifică, slim, cu o versiune pin-uită.
- Multi-stage build dacă există pași de compilare.
- Layer-e ordonate de la modificări rare la modificări frecvente.
- Lockfile de dependențe copiat înainte de cod, instalat înainte ca codul să fie copiat.
- Utilizator non-root pentru procesul care rulează.
.dockerignorecare exclude gunoiul evident.- Tag imutabil în numele publicat.
- Health check sau entrypoint cunoscut bun.
- Niciun secret băgat în imagine (variabile de mediu la runtime, nu build args).
Fiecare item e mic. Împreună sunt diferența dintre o imagine de patruzeci de megabyte care se trage într-o secundă și o imagine de două gigabyte care ia un minut, înmulțită cu oricâte copii rulează cluster-ul tău.
Ce înseamnă asta în practică
Containerele sunt unitatea de deploy. Dockerfile-ul e contractul dintre codul pe care-l scrii și runtime-ul care-l rulează. Un Dockerfile mic și bine format e una dintre cele mai pârghioase piese de cod din repo-ul unei echipe de date: fiecare job moștenește proprietățile lui.
Vestea bună e că pattern-urile sunt stabile. Un Dockerfile scris azi pentru un job Python va continua să funcționeze peste cinci ani; cloud-ul de dedesubt se va schimba, orchestratorul se va schimba, registry-urile se vor schimba, dar Dockerfile-ul și imaginea OCI rămân aceleași. Dintre toate straturile din stack-ul modern de date, ăsta e unul dintre cele mai plictisitoare, în sensul cel mai bun.
Lecția 55 construiește pe asta cu Kubernetes, runtime-ul pe care majoritatea echipelor își rulează acum containerele. Lecția 56 închide modulul cu povestea de deployment de la Stripe, care leagă CI, CD, IaC și containerele într-o singură imagine operațională.
Citații
- Documentația Docker, „Best practices for writing Dockerfiles” (
https://docs.docker.com/build/building/best-practices/, consultat 2026-05-01). - Specificațiile Open Container Initiative (
https://opencontainers.org/, consultat 2026-05-01). - „Multi-stage builds” în documentația Docker (
https://docs.docker.com/build/building/multi-stage/, consultat 2026-05-01). - Documentația Spark on Kubernetes (
https://spark.apache.org/docs/latest/running-on-kubernetes.html, consultat 2026-05-01). - Proiectul Cosign (
https://github.com/sigstore/cosign, consultat 2026-05-01).