32.1 Konténer és virtuális környezet külön szerepe

32.1.1 Cél és konkrét helyzet

Az I. ellenőrző pont: megismételhető kutatói munkaasztal munkaasztalán a lab projekt lokálisan ugyanazt az eredményt adja: a regisztrált kernel a projekt .venv-jére mutat, a quickcheck.py failures=0 eredményt ír, a futási jegyzék pedig rögzíti a hash-ellenőrzést. A probléma akkor jelentkezik, amikor ezt a projektet egy kolléga gépére vagy CI-futásra adjuk át. A uv.lock és a pyproject.toml a Python-csomagok függőségeit rögzíti, de nem rögzíti sem az operációs rendszer verzióját, sem a rendszerkönyvtárakat, sem magát a Python-interpreter buildjét. A Másik gép, azonos projekt alfejezetben tárgyalt OS-különbségekből tudjuk: a másik gépen hiányozhat libgomp, más lehet a glibc vagy az időzóna-adatbázis, ezért ugyanaz a lock még nem garantálja ugyanazt a futási környezetet.

Ebben az alfejezetben a vizsgálati egység nem statisztikai minta, hanem egy környezet-build: egy Dockerfile-ból épülő image és az abból indított container. A bemenet a lab projekt forrása (pyproject.toml, uv.lock, src/, quickcheck.py, generate_data.py, starter_experiment.py), a kimenet pedig egy megismételhető futási környezet, amelyben a quickcheck.py a kívülről csatolt adatfájlokat ellenőrzi. Az ellenőrzési feltétel kettős: a Dockerfile ne tartalmazzon adatfájlokat és .venv-et; a futó konténer pedig a projekt lockjának megfelelő interpretert használja.

Induló becslésként érdemes feltenni, hogy a buildelési kontextus a .dockerignore nélkül több száz megabájt is lehet a .venv és az adatfájlok miatt; a szűréssel a forráskód néhány megabájtra csökken. A Dockerfile-t ebben a generálási környezetben nem futtatjuk, ezért az alábbi munkamenet specifikáció és szemléltető példa, nem mérési jegyzőkönyv.

32.1.2 Fogalmi magyarázat: image, container, base image, venv, volume, user, build context

A Docker-kép vagy image egy olvasható, rétegezett fájlrendszer és hozzá tartozó metaadatok halmaza. Minden RUN, COPY és FROM utasítás egy újabb fájlrendszer-réteget hoz létre. A konténer az image-ből indított futó példány: az image rétegeire egy írható réteget tesz, külön processzteret, hálózatot és fájlrendszer-nézetet kap. A konténer törlése után az írható rétege elveszik; ami megmarad, az a volume-on vagy a bind mounton kívülre kerül.

A base image az a kiinduló image, amelyre a Dockerfile épít. Példánkban python:3.12-slim egy Debian-alapú, kisméretű Python-kép. A slim azt jelzi, hogy a gyártó a felesleges fejlesztői eszközöket elhagyta. Az image-et a FROM sor határozza meg; ha a projekt .python-version fájlja 3.12-t ír, az csak kérés a helyi eszköz számára, a tényleges interpretert a konténerben a FROM python:3.12-slim adja.

A venv Python-szintű virtuális környezet: interpretert és a projekt Python-csomagjait izolálja. A uv sync --locked a uv.lock-ot oldja fel, tehát a Python-csomagok verzióját és ellenőrző adatait rögzíti. A venv nem rögzíti viszont az operációs rendszert, a rendszerkönyvtárakat és a natív függőségeket. A két réteg felelőssége:

Függőség Példa Rögzíti Nem rögzíti
Image glibc, libgomp, CA-tanúsítványok, tzdata, OS-verzió Dockerfile FROM és build A kernel pontos verzióját, CPU-utasításokat
Python-lock numpy, pandas, scipy, statsmodels verziója uv.lock, uv sync --locked A rendszerkönyvtárakat, az interpreter buildjét
Adat- és splitmanifest CSV-hash, cutoff manifest.json, mount Az adat jövőbeli frissítését

A volume és a bind mount a konténer tárterét köti a külvilághoz. A bind mount a host egy könyvtárát teszi elérhetővé, például -v "$(pwd)/data:/app/data:ro": a host data könyvtára a konténeren belül /app/data néven, csak olvashatóan jelenik meg. A volume ezzel szemben Docker által kezelt tároló, amelyet a konténer életciklusa után is megőriz. A Northstar-lab bemeneti CSV-ket bind mountként érdemes csatolni, hogy az adatfájlok ne az image részei legyenek, hanem a futás pillanatában érkezzenek.

A user a konténeren belüli processz UID-je. A konténer alapból gyakran root felhasználóval indul, ami adatkérdésben rossz gyakorlat: egy hibás kód a hostra is veszélyesebb lehet, és a későbbi CI-rendszerben a jogosultság-ellenőrzés is sérül. Ezért a Dockerfile-ban explicit USER app-et adunk meg. A bind mount fájljogosultsága a host UID-jétől függ; ha a konténer 10001 UID-je nem egyezik az adatfájl tulajdonosával, olvasási hiba léphet fel. A Másik gép, azonos projekt fájljog-tanulsága itt visszatér: a hiba forrása nem az algoritmus, hanem a fájlrendszer-hozzáférés.

A build context az a könyvtár, amelyet a docker build . parancs a Docker daemonnak átad. A build context tartalmazza az összes fájlt, amelyre a COPY hivatkozhat; a .dockerignore szűri ki belőle a felesleges elemeket. Ha a context nagy, a build már az elején lelassul, és a .venv, az adatfájlok vagy a .git könyvtár is bekerülhet a képbe. Ezért a .dockerignore része a környezeti szerződésnek.

32.1.3 A Python-lock és az image határa

A Kernelregisztráció és tiszta újrafuttatás alfejezetben a kernelspec argv[0] eleme mutatta a tényleges Python-útvonalat. A konténerben az interpreter útvonala /opt/venv/bin/python, tehát a uv sync a /opt/venv-be telepít; az image FROM rétege a rendszer-szintű részeket adja. A két réteg összefügg: ha a python:3.12-slim image új buildet kap, ugyanaz a uv.lock más glibc-vel futhat. Ha viszont a uv.lock változik, a Docker-réteg-cache a COPY pyproject.toml uv.lock sortól újraépül. Ez a rétegcache-logika a Cache, backfill és orchestrator input-hash alapú cache-elvéhez hasonlít: ha a bemenet hash-e nem változik, a korábbi rétegek felhasználhatók.

32.1.4 Teljes minimális munkapélda: Dockerfile a labhoz

A következő Dockerfile a lab projekt gyökerében áll, explicit munkakönyvtárral (/app) és explicit userrel (app). Az adatfájlokat és a .venv-et kizárjuk az image-ből. A UV_VERSION=0.5.11 szemléltető helyőrző, nem ellenőrzött aktuális verzió; a lab review-ban a ténylegesen telepített uv --version kimenettel kell rögzíteni. Ugyanez igaz a python:3.12-slim tagre: a példában a könnyen olvashatóság kedvéért tag van, a következő szakaszban a digeströgzítést külön tárgyaljuk.

# Dockerfile — szemléltető specifikáció, automatizált futtatás nélkül.
# Bázis kép: könnyen olvasható tag; éles rögzítéshez lásd a tag/digest szakaszt.
FROM python:3.12-slim

# Az uv verziója a lockkal együtt rögzítendő; az alábbi érték helyőrző.
ARG UV_VERSION=0.5.11
RUN python -m pip install --no-cache-dir uv==${UV_VERSION}

# Nem root felhasználó létrehozása.
RUN groupadd --gid 10001 app && \
    useradd --uid 10001 --gid app --create-home app

ENV UV_PROJECT_ENVIRONMENT=/opt/venv
ENV VIRTUAL_ENV=/opt/venv
RUN python -m venv "$VIRTUAL_ENV"
ENV PATH="$VIRTUAL_ENV/bin:$PATH"

WORKDIR /app

# A lab forrása; az adatfájlokat és a .venv-et szándékosan nem másoljuk.
COPY --chown=app:app pyproject.toml uv.lock ./
COPY --chown=app:app src ./src
COPY --chown=app:app quickcheck.py generate_data.py starter_experiment.py README.md ./

RUN uv sync --locked --no-dev --no-install-project

USER app
CMD ["python", "quickcheck.py"]

A .dockerignore fájl kizárja a projekt méretes és futás közben csatolt elemeit:

.git
.venv
__pycache__
*.pyc
.pytest_cache
.mypy_cache
.ruff_cache
data/
artifacts/
*.csv
*.parquet
.env

A build és a futás parancsa Linuxon, WSL2-ben vagy macOS-héjban (a lab/ projektgyökér mint munkakönyvtár):

# Linux, WSL2 vagy macOS héj; munkakönyvtár: a lab projekt gyökere
docker build --pull --progress plain -t northstar-lab:dev .

docker run --rm \
  -v "$(pwd)/data:/app/data:ro" \
  -v "$(pwd)/artifacts:/app/artifacts" \
  northstar-lab:dev

PowerShellben a sortörés backtick, a ${PWD} a környezeti változó; a bind mount útvonala a helyi lab tényleges elérési útjához igazítandó:

# PowerShell; munkakönyvtár: a lab projekt gyökere
docker build --pull --progress plain -t northstar-lab:dev .

docker run --rm `
  -v "${PWD}/data:/app/data:ro" `
  -v "${PWD}/artifacts:/app/artifacts" `
  northstar-lab:dev

Az ellenőrzéshez a konténerbe lépve le lehet kérdezni az interpreter útvonalát és a user azonosítóját:

# Szemléltető ellenőrző parancs; a konkrét kimenet a lab és a Docker verziójától függ
docker run --rm northstar-lab:dev python -c "import sys; print(sys.executable)"
# Elvárt: /opt/venv/bin/python

docker run --rm northstar-lab:dev id -u
# Elvárt: 10001

Az image tartalmának vizsgálata:

docker image inspect northstar-lab:dev --format '{{.Id}}'

A .Id az image tartalomazonosítója, nem feltétlenül rövid. A RepoDigests lista üres is lehet egy kizárólag helyben épített image-nél; ekkor az index .RepoDigests 0 kifejezés hibát adhat. A build ellenőrzéséhez külön docker image inspect northstar-lab:dev --format "{{.Id}}" parancsot használjunk; registry-digestet csak ténylegesen közzétett vagy letöltött image-nél várjunk.

32.1.5 Eredmény értelmezése

A build napló eleje mutatja a context méretét; a .dockerignore hatására a .venv és az adatfájlok nem kerülnek a contextbe, ezért a forrás-copy néhány megabájtos marad. A COPY --chown=app:app beállítja a fájlok tulajdonosát a konténeren belül, így a USER app után a kód elérhető. A uv sync --locked --no-dev a lockot kikényszeríti: ha a pyproject.toml és a uv.lock nincs összhangban, a build leáll, és ez kívánt hiba. A runtime --no-dev a fejlesztői csomagokat kihagyja; a quickcheck.py-hez nem szükséges pytest, de ha a lab teljes tesztfuttatást kér, külön teszt-image vagy build stage indokolt.

A futás során a data mount :ro jelzése megakadályozza, hogy a vizsgált bemeneti fájlokat a konténer módosítsa; az artifacts mount írható, így a futás eredménye a konténer törlése után is megmarad. A --rm a leállás után törli a konténert, de a bind mountok tartalma a hoston továbbra is elérhető. Az invariáns: ha a Dockerfile, a uv.lock és a forrás ugyanaz, akkor ugyanaz a quickcheck.py fut; a failures=0 eredmény csak a csatolt adat sértetlenségét igazolja, ahogy I. ellenőrző pont: megismételhető kutatói munkaasztal alfejezetben is külön réteg volt.

32.1.6 Feladatok

  1. Értelmezés. A kolléga gépe rendelkezik a uv.lock másolatával, de nincs Docker. Magyarázd el, miért nem elegendő a lock egyedül ahhoz, hogy a lab modellképzése bitazonos környezetben fusson. Nevezz meg legalább két függőséget, amit a lock nem rögzít, és mondd meg, melyik réteg fedi le őket.

  2. Implementáció. Egészítsd ki a fenti .dockerignore-t a következő elemekkel, és indokold meg, miért fontos mindegyik: uv.lock biztonsági másolata, .ipynb_checkpoints, *.log, artifacts/split.json. (A uv.lock maga a Dockerfile-ban másolódik, de a kérdés azt vizsgálja, mi kerüljön a build contextbe.)

  3. Hibadiagnózis. A következő futási hibák közül válaszd ki, melyik utal konténer-konfigurációs hibára, és mi a javítás:

  4. Megváltozott feltétel. A lab CI-futás mostantól nem bind mountot használ, hanem az adatokat egy Docker named volume-ba másolja a futás előtt. Hogyan változik a :ro szemantikája, és miért lehet mégis előnyösebb a bind mount a helyi fejlesztésben? Adj egy konkrét docker volume create és docker run -v példát.

32.1.7 Megoldások és indoklás

  1. A uv.lock a Python-csomagok verzióját és ellenőrző adatait rögzíti, de a Python-interpretert magát az image FROM python:3.12-slim adja; a lock nem tartalmazza a glibc, a libgomp vagy a CA-tanúsítványok verzióját. A kolléga gépe lehet más Debian-verzió, más időzóna-adatbázis, vagy akár Windows, ahol a natív wheel-ek más fordítási beállításokkal futnak. Az ellenőrzés a megfelelő réteg: a lock a venv-függőségeket, az image a system-réteget. A kernel és a CPU-utasítás szintén nem rögzíthető a Docker image-szel; ez a Tensor, device és autograd device-kérdésének konténeres megfelelője.

  2. uv.lock biztonsági másolata: maga a lock szükséges a Dockerfile-ban, ezért a .dockerignore akkor kapná el, ha a COPY . .-t írnánk; explicit COPY pyproject.toml uv.lock ./ mellett a kettős bekerülés elkerülhető. A másolatot nem a contextből, hanem a copy-lépésből kell kihagyni; a .dockerignore-ban a uv.lock szerepeltetése hibás lenne, mert akkor a COPY sem éri el. .ipynb_checkpoints: notebook-mentések nem részei a futtatandó kódnak, és nagy, nem reprodukálható tartalmat vihetnek be. *.log: futási naplók a host feladata; a build contextbe kerülve félrevezető állapotot rögzítenek. artifacts/split.json: a futás kimenete, nem a build bemenete; a Futásazonosító és kísérleti táblázat run-manifest logikája szerint a split a futáshoz tartozik, nem a környezethez. A context így csak a ténylegesen szükséges, forrás-verziózott fájlokat tartalmazza.

  3. FileNotFoundError: data/customers.csv: a bind mount hiányzik vagy rossz az útvonal; a -v "$(pwd)/data:/app/data:ro" után a quickcheck relatív elérése igazítandó. PermissionError az artifacts mounton: a host könyvtár tulajdonosa nem a 10001 UID; írható host jogot vagy named volume-ot kell adni. ModuleNotFoundError: No module named 'numpy': a uv sync nem futott le vagy a VIRTUAL_ENV PATH nem érvényes; a RUN uv sync --locked --no-dev --no-install-project réteg hibája. uv sync: lockfile not found: a COPY pyproject.toml uv.lock ./ hiányzik, vagy a .dockerignore kizárja a lockot — a build ilyenkor joggal áll le, és a hibát a COPY és a .dockerignore együttes vizsgálatával kell javítani.

  4. A named volume a Docker által kezelt tároló; a :ro itt azt jelenti, hogy a konténer olvashatja, de a futás alatt nem módosíthatja a volume-ot. A bind mount előnye fejlesztés közben, hogy azonnal látja a host fájljait; a named volume ehhez képest leválik a host elérési útról, így a bejárás nehezebb. Példa:

docker volume create northstar-data

docker run --rm \
  -v northstar-data:/app/data:ro \
  -v "$(pwd)/artifacts:/app/artifacts" \
  northstar-lab:dev

A named volume használata előtt az adatokat egyszer fel kell tölteni egy ideiglenes konténerrel vagy a Docker Desktop eszközével; ez a CI-ban megbízhatóbb, mert nem függ a host könyvtárstruktúrájától.

32.1.8 Gyakori tévedés ellenpéldával

„Az image tag azonosítja a tartalmat.” Ez téves. A python:3.12-slim tag egy mutató, amelyet a karbantartó újra kiadhat. Tegnap a docker pull még a sha256:aaaa... digestet adhatta, ma ugyanaz a tag a sha256:bbbb... tartalomra mutathat; a két gép buildje ezért eltérhet. Az ellenőrzéshez a tag mellé a digestet kell rögzíteni: FROM python:3.12-slim@sha256:.... Ha egy rögzített digesttel megadott bázisimage új tartalmat kap a tag alatt, a digest továbbra is a régi tartalomhoz köt; az image letöltése akkor hibázik, ha a digest már nem érhető el. A frissítéshez új digestet kell választani és review-ban kezelni; ez a Modelljelölt, jóváhagyás és registry modellregistry-jéhez hasonló: a champion címke szintén mutálható, csak a tartalom-hash az azonosító.

Önellenőrzés: a futó konténerben ellenőrizd, hogy id -u értéke 10001, a sys.executable /opt/venv/bin/python, és a docker build context-méret nem tartalmazza a .venv-et. Ha a context nagy, a .dockerignore hibás vagy hiányzik. A bind mount jogosultsági hibája esetén ne a Python-kódot javítsd először: előbb a host UID és a mount jogosultságát vizsgáld.

32.1.9 Rövid összefoglaló

A konténer-image az operációs rendszer, a rendszerkönyvtárak és a Python-interpreter rétegét rögzíti; a uv.lock ugyanazon belül a Python-csomagok verzióját. A Dockerfile explicit munkakönyvtárral, nem root userrel, kizárt adatokkal és .dockerignore-dzsal a lab futását hordozhatóvá teszi, de a kernel és a CPU-utasítások továbbra is változhatnak. A bind mount adatfájlként, a volume pedig tartós tárolóként szolgál; a tag helyett digest azonosítja a buildet. A következő alfejezetben Távoli interpreter és notebook biztonságos elérése a távoli interpretert és a notebook biztonságos elérését vizsgáljuk, ahol ezek a környezeti rétegek a távoli gépen is érvényesek maradnak.