Mundarija (25)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Konteyner va virtual mashina
- 2.2. Image, konteyner, registry
- 2.3. Dockerfile ko'rsatmalari
- 2.4. Qatlamlar va kesh
- 2.5. Multi-stage build
- 2.6. Base image tanlash
- 2.7. Xavfsizlik: root, sirlar, .dockerignore
- 2.8. 12-factor konfiguratsiya
- 2.9. docker-compose: API + ma'lumotlar bazasi
- 2.10. ML ga xos savollar
- 2.11. Tuzoqlar
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Dockerfile linteri noldan
- Misol 2 — Qatlam keshi simulyatsiyasi
- Misol 3 — .dockerignore ta'siri: haqiqiy papkada kontekst hajmi
- Misol 4 — 12-factor konfiguratsiya: env, pydantic va sirni niqoblash
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
27.9-dars: Docker bilan konteynerlash
27-QISM — MLOPS VA DEPLOY · 9-dars
1. Kirish va motivatsiya
Oldingi darsda 27.8-bob model API sini mustahkamladik: kirishni validatsiya qildik, xatolarni to'g'ri kodlar bilan qaytardik va hammasini TestClient bilan jarayon ichida sinadik. Testlar yashil. Endi savol: bu API boshqa mashinada — serverda, hamkasbingiz noutbukida, bulutda — xuddi shunday ishlaydimi?
Tajriba "yo'q" deydi. Sizning noutbukingizda Python 3.12, scikit-learn ning ma'lum bir versiyasi, libgomp tizim kutubxonasi va o'nlab muhit o'zgaruvchilari bor. Serverda esa ularning har biri boshqacha bo'lishi mumkin. 27.2-darsda muhitni requirements.txt va lock fayl bilan qayd qildik — bu Python paketlari uchun yetarli. Lekin Python ning o'zi, operatsion tizim kutubxonalari, fayl tuzilishi va ishga tushirish buyrug'i hali ham "mashinaga bog'liq".
Konteyner bu muammoni hal qiladi: ilova, uning Python i, paketlari va tizim kutubxonalari bitta image ga yig'iladi. Image bir marta quriladi va istalgan joyda aynan bir xil ishga tushadi. Docker — shu g'oyaning eng mashhur amalga oshirilishi.
Real vaziyat. Jamoa kredit skoring modelini serverga qo'ydi va birinchi so'rovda ModuleNotFoundError emas, balki undan ham yomoni — jim noto'g'ri natija oldi: serverdagi scikit-learn boshqa kichik versiya edi va joblib bilan saqlangan model ogohlantirish berib yuklandi, lekin bir transformatsiya boshqacha ishladi. Ikki hafta davomida skorlar biroz siljigan holda ketdi. Model image ichida, pinlangan versiyalar bilan yetkazilganida bu savol umuman tug'ilmasdi: sinovdan o'tgan image — ishlab chiqarishdagi image.
Lekin Docker o'zi yangi xatolar manbai ham bo'lishi mumkin: 2 GB li image lar, har kichik o'zgarishda 10 daqiqalik qayta qurish, root huquqi bilan ishlayotgan konteyner va image tarixida qolib ketgan parol. Bu darsda Dockerfile ni to'g'ri yozishni o'rganamiz va har qoidani Python bilan tekshiramiz.
Bu darsda model API sini kichik, tez quriladigan va xavfsiz image ga joylaymiz.
Bu darsda:
- Konteyner va virtual mashina farqi
- Image, konteyner, registry
- Dockerfile ko'rsatmalari
- Qatlamlar va kesh: nima uchun tartib muhim
- Multi-stage build va slim base image
- Xavfsizlik: root emas foydalanuvchi, sirlar,
.dockerignore - 12-factor konfiguratsiya
- docker-compose: API + ma'lumotlar bazasi
- Amaliy: Dockerfile linteri, kesh simulyatsiyasi, kontekst hajmi, konfiguratsiya validatsiyasi
ℹ Misollarda
dockerbuyrug'i chaqirilmaydi — Dockerfile va loyiha papkasi Python bilan tahlil qilinadi, shuning uchun natija istalgan mashinada bir xil. Dockerfile, compose vabashbuyruqlari alohida bloklarda — ularni o'z mashinangizda sinab ko'ring.
2. Nazariya — chuqur tushuntirish
2.1. Konteyner va virtual mashina
VIRTUAL MASHINA (VM) KONTEYNER
+-----------+ +-----------+ +---------+ +---------+ +---------+
| ilova A | | ilova B | | ilova A | | ilova B | | ilova C |
| kutubxona | | kutubxona | | kutubx. | | kutubx. | | kutubx. |
| MEHMON OT | | MEHMON OT | +---------+ +---------+ +---------+
+-----------+ +-----------+ | konteyner runtime (Docker) |
| GIPERVIZOR | +-----------------------------------+
+--------------------------+ | XOST OT YADROSI (Linux) |
| XOST OT / apparatura | +-----------------------------------+
+--------------------------+ | apparatura |
VM: har biri TO'LIQ operatsion tizim (yadro bilan)
ishga tushish - o'nlab soniya/daqiqa, hajm - GB lar
izolyatsiya kuchli (alohida yadro)
KONTEYNER: yadro UMUMIY, faqat foydalanuvchi qismi alohida
ishga tushish - soniyadan kam, hajm - o'nlab/yuzlab MB
izolyatsiya: Linux namespaces (ko'rinish) + cgroups (resurs)Konteyner — bu "kichik VM" emas, balki izolyatsiya qilingan jarayon. U xostning yadrosidan foydalanadi, lekin o'z fayl tizimini, jarayonlar ro'yxatini, tarmog'ini "ko'radi". Shuning uchun Linux konteyneri Windows yoki macOS da ishlashi uchun Docker Desktop ichkarida kichik Linux VM ishga tushiradi.
| VM | Konteyner | |
|---|---|---|
| Ishga tushish | soniyalar-daqiqalar | soniyadan kam |
| Hajm | GB | MB |
| Yadro | o'ziniki | xostniki |
| Izolyatsiya | kuchli | yetarli, lekin yadro umumiy |
| Tipik qo'llash | turli OT, kuchli ajratish | ilovani yetkazish, masshtablash |
Konteyner — izolyatsiya qilingan jarayon, to'liq OT emas: yengil, tez va takrorlanadigan, lekin yadroni xost bilan bo'lishadi.
2.2. Image, konteyner, registry
Dockerfile --(docker build)--> IMAGE --(docker run)--> KONTEYNER
(retsept) (o'zgarmas (ishlayotgan nusxa,
qatlamlar to'plami) ustida yoziladigan qatlam)
|
(docker push/pull)
v
REGISTRY
(Docker Hub, kompaniya ichki registri)
IMAGE NOMI: registry.misol.uz/ml/churn-api:1.4.2
\_______________/ \___________/ \___/
registry repozitoriy teg
TEG vs DIGEST:
churn-api:1.4.2 - teg, KO'CHIRILISHI mumkin (boshqa image ga)
churn-api@sha256:3f9a... - digest, mazmun xeshi, O'ZGARMASImage — o'zgarmas shablon; konteyner — uning ishlayotgan nusxasi. Bitta image dan o'nta konteyner ishga tushirish mumkin (masshtablash shunday qilinadi, 27.10). Konteyner ichida yozilgan fayllar uning yuqori yoziladigan qatlamida qoladi va konteyner o'chirilganda yo'qoladi — shuning uchun ma'lumotlar bazasi fayllari volume da saqlanadi.
Teg — yorliq, digest — kafolat. Ishlab chiqarishda aniq qaysi image ishlayotganini bilish uchun digest yoki hech qachon qayta ishlatilmaydigan teg (masalan, git xesh) ishlating.
2.3. Dockerfile ko'rsatmalari
FROM python:3.12.7-slim # asos image - har doim aniq versiya
WORKDIR /app # ishchi papka (yo'q bo'lsa yaratiladi)
ENV PYTHONUNBUFFERED=1 # muhit o'zgaruvchisi (build va run da)
COPY requirements.txt . # kontekstdan image ga nusxa
RUN pip install --no-cache-dir -r requirements.txt # build vaqtida buyruq
COPY ilova/ ./ilova/
RUN useradd --create-home --uid 10001 ilova
USER ilova # bundan keyingi hamma narsa shu foydalanuvchi
EXPOSE 8000 # HUJJAT: ilova shu portni tinglaydi
HEALTHCHECK --interval=30s CMD python -c "import urllib.request as u; u.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "ilova.server:app", "--host", "0.0.0.0", "--port", "8000"]| Ko'rsatma | Nima qiladi | Muhim nozikliklar |
|---|---|---|
FROM |
asos image | latest emas, aniq teg; AS nom — multi-stage |
WORKDIR |
ishchi papka | RUN cd o'rniga shu |
COPY |
fayl nusxalash | --chown, --from=bosqich; mahalliy fayl uchun ADD emas |
RUN |
build vaqtida buyruq | har RUN — yangi qatlam; keshni SHU RUN ichida tozalang |
ENV |
muhit o'zgaruvchisi | image ga yoziladi — sir uchun EMAS |
ARG |
faqat build vaqtidagi o'zgaruvchi | image tarixida ko'rinadi — sir uchun EMAS |
EXPOSE |
portni hujjatlash | portni o'zi ochmaydi (-p ochadi) |
USER |
foydalanuvchini almashtirish | root emas foydalanuvchi |
CMD |
sukut buyruq | JSON shakli: ["python", "..."] |
ENTRYPOINT |
doimiy bajariladigan fayl | CMD unga argument bo'ladi |
HEALTHCHECK |
sog'liq tekshiruvi | orkestrator nosog'lom konteynerni almashtiradi |
CMD va ENTRYPOINT:
ENTRYPOINT ["python", "-m", "ilova"] CMD ["serve"]
docker run image -> python -m ilova serve
docker run image baholash -> python -m ilova baholash (CMD almashdi)
SHELL shakli: CMD python -m ilova.server
-> /bin/sh -c "python -m ilova.server"
-> PID 1 = sh, Python emas -> SIGTERM Python ga YETMAYDI
-> docker stop 10 soniya kutadi va SIGKILL: so'rovlar yarim yo'lda uziladi
JSON (exec) shakli: CMD ["python", "-m", "ilova.server"]
-> PID 1 = python, SIGTERM ni o'zi oladi -> toza to'xtash CMD va ENTRYPOINT — JSON shaklida, aks holda ilova to'xtash signalini olmaydi va har deploy da so'rovlar uziladi.
2.4. Qatlamlar va kesh
Har ko'rsatma -> bitta QATLAM. Image = qatlamlar to'plami.
KESH KALITI (soddalashtirilgan):
kalit(qatlam) = xesh( kalit(ota qatlam) + ko'rsatma matni
+ COPY/ADD bo'lsa: nusxalangan fayllar mazmuni )
-> bitta qatlam o'zgarsa, undan PASTDAGI hammasi qayta quriladi
-> RUN kaliti buyruq MATNIGA bog'liq, natijasiga emas
YOMON TARTIB: YAXSHI TARTIB:
FROM python:3.12.7-slim FROM python:3.12.7-slim
WORKDIR /app WORKDIR /app
COPY . . <- kod COPY requirements.txt . <- kam o'zgaradi
RUN pip install ... <- 95 s RUN pip install ... <- keshdan
CMD [...] COPY ilova/ ./ilova/ <- kod
CMD [...]
kod o'zgardi -> pip QAYTA kod o'zgardi -> faqat oxirgi COPYQoida oddiy: kam o'zgaradigan narsa tepada, tez o'zgaradigani pastda. Bog'liqliklar haftada bir o'zgaradi, kod — kuniga o'n marta. Agar kod bog'liqliklardan oldin nusxalansa, har commit da pip install qayta ishlaydi, yangi yuzlab MB li qatlam registryga yuklanadi va har serverga qayta tortiladi. 2-misol buni raqamlarda ko'rsatadi.
Ikkinchi nozik joy: RUN keshi natijaga emas, matnga bog'liq. RUN pip install pandas bir marta keshga tushsa, PyPI da yangi versiya chiqsa ham kesh eski qatlamni beradi — lekin keshsiz CI mashinasi yangi versiyani o'rnatadi. Natijada bir xil Dockerfile dan ikki xil image. Yechim — versiyalarni pinlash 27.2-bob: shunda matn natijani to'liq belgilaydi.
Kesh kaliti zanjir: bitta qatlam o'zgarishi pastdagi hammasini buzadi — shuning uchun requirements.txt koddan oldin.
2.5. Multi-stage build
# 1-bosqich: qurish - kompilyator va sarlavha fayllari shu yerda qoladi
FROM python:3.12.7-slim AS quruvchi
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 2-bosqich: ishga tushirish - faqat tayyor paketlar
FROM python:3.12.7-slim
COPY --from=quruvchi /install /usr/local
COPY ilova/ /app/ilova/NIMA BERADI:
yakuniy image da gcc, sarlavhalar, build keshi YO'Q
-> kichikroq (ko'pincha yuzlab MB farq)
-> hujum yuzasi kichik (kompilyator yo'q)
-> faqat OXIRGI bosqich yetkaziladi; oraliq bosqichlar keshda qoladi
ML DA TIPIK:
quruvchi: paketlarni kompilyatsiya, modelni eksport/tekshirish
yakuniy: runtime + model artefakti + ilova kodiMulti-stage — qurish asboblari yakuniy image ga tushmaydi; faqat oxirgi bosqich ishga tushadi va yetkaziladi.
2.6. Base image tanlash
python:3.12.7 ~1 GB atrofida to'liq Debian, gcc bor, qulay, katta
python:3.12.7-slim ~50 MB atrofida minimal Debian - ML uchun odatda eng yaxshi
python:3.12.7-alpine eng kichik musl libc: ko'p ML g'ildiraklari (wheel)
yo'q -> numpy/scipy manbadan kompilyatsiya,
sekin va xatoga moyil
distroless / chiseled juda kichik shell ham yo'q - xavfsiz, lekin
nosozlikni tuzatish qiyin
(hajmlar taxminiy, versiyaga qarab o'zgaradi - rasmiy sahifaga qarang)
ML UCHUN TIPIK TANLOV: -slim + multi-stage
TORCH: CPU uchun alohida g'ildirak (index-url .../cpu) - CUDA versiyasi GB lab ML uchun -slim — oqilona sukut; alpine ML paketlari bilan ko'pincha kichik emas, balki sekin va muammoli.
2.7. Xavfsizlik: root, sirlar, .dockerignore
1. ROOT EMAS FOYDALANUVCHI
Sukut bo'yicha konteyner ichida jarayon root (UID 0).
Ilovada zaiflik bo'lsa -> hujumchi konteyner ichida root;
yadro umumiy bo'lgani uchun qochish xavfi oshadi.
RUN useradd --uid 10001 ilova + USER ilova
2. SIRLAR IMAGE GA YOZILMASIN
ENV DB_PAROL=... -> image metadata sida, `docker history` da ko'rinadi
ARG TOKEN=... -> build tarixida qoladi
COPY .env . -> fayl qatlamda qoladi (keyin RUN rm ham yordam bermaydi:
oldingi qatlam baribir image ichida!)
TO'G'RI:
runtime sir: docker run -e / orkestrator secret / secret manager
build sir: RUN --mount=type=secret,id=pip_token ... (qatlamga tushmaydi)
3. .dockerignore
docker build . -> butun papka (KONTEKST) builder ga yuboriladi
.git, .venv, data/, .env -> sekin build + sir oqishiQATLAMLAR "O'CHIRISHNI" BILMAYDI:
COPY .env /app/.env qatlam 5: .env bor
RUN rm /app/.env qatlam 6: ".env o'chirildi" belgisi
-> yakuniy fayl tizimida .env ko'rinmaydi,
lekin qatlam 5 image ichida: `docker save` bilan ochib olish mumkin Sir image ga bir marta tushsa, u yerda qoladi — keyingi RUN rm uni o'chirmaydi. Sirlar faqat ishga tushishda, muhit orqali beriladi.
2.8. 12-factor konfiguratsiya
12-FACTOR APP (III. Config): konfiguratsiya MUHITDA, kodda emas
BITTA IMAGE ---> dev ILOVA_MUHIT=dev ILOVA_LOG_DARAJASI=DEBUG
---> staging ILOVA_MUHIT=staging
---> prod ILOVA_MUHIT=prod ILOVA_ISHCHILAR=8
QOIDALAR:
image muhitga bog'liq EMAS - sinovdan o'tgan image aynan prod ga boradi
konfiguratsiya ishga tushishda VALIDATSIYA qilinadi -> xato darhol,
birinchi so'rovda emas ("fail fast")
noma'lum o'zgaruvchi (imlo xatosi) - xato, jim e'tiborsiz emas
sirlar SecretStr - repr, log va JSON da niqoblanganPydantic bilan buni BaseModel va kichik yuklovchi funksiya bilan qilish mumkin (4-misol). Katta loyihalarda pydantic-settings paketi xuddi shu ishni tayyor holda qiladi:
from pydantic import SecretStr
from pydantic_settings import BaseSettings, SettingsConfigDict
class Sozlamalar(BaseSettings):
model_config = SettingsConfigDict(env_prefix="ILOVA_", extra="forbid")
muhit: str
port: int = 8000
db_url: SecretStr
sozlamalar = Sozlamalar() # os.environ dan o'qiydi va tekshiradiBitta image, ko'p muhit — farq faqat muhit o'zgaruvchilarida; ular ishga tushishda tekshiriladi.
2.9. docker-compose: API + ma'lumotlar bazasi
# docker-compose.yml - lokal ishlab chiqish va sinov uchun
services:
api:
build: .
image: churn-api:dev
ports:
- "8000:8000"
environment:
ILOVA_MUHIT: dev
ILOVA_MODEL_YOLI: /models/model.joblib
env_file:
- .env # sirlar: git ga va image ga TUSHMAYDI
volumes:
- ./artefakt:/models:ro # model faqat o'qish uchun
depends_on:
db:
condition: service_healthy
restart: unless-stopped
db:
image: postgres:17.2
environment:
POSTGRES_DB: ml
POSTGRES_USER: ilova
POSTGRES_PASSWORD_FILE: /run/secrets/db_parol
secrets:
- db_parol
volumes:
- pgdata:/var/lib/postgresql/data # konteyner o'chsa ham ma'lumot qoladi
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ilova -d ml"]
interval: 5s
retries: 10
volumes:
pgdata:
secrets:
db_parol:
file: ./secrets/db_parol.txtMUHIM JOYLAR:
api -> db ga "db:5432" nomi bilan ulanadi (compose ichki DNS)
depends_on + healthcheck: DB TAYYOR bo'lgandan keyin API boshlanadi
volume: DB fayllari konteyner qatlamida emas
compose - bitta mashina uchun; ko'p mashina - Kubernetes (27.10)Compose — ko'p konteynerli tizimni bitta faylda tasvirlash; ma'lumot volume da, sirlar fayl/secret orqali.
2.10. ML ga xos savollar
MODEL ARTEFAKTI QAYERDA?
A) image ichida (COPY model.joblib)
+ image = kod + model, to'liq takrorlanadi, bitta digest
- har yangi model -> yangi image; katta modelda image katta
B) ishga tushishda registrdan yuklash 27.5-bob
+ image kichik, model mustaqil yangilanadi
- ishga tushish sekinroq, registr mavjudligiga bog'liq,
"qaysi model ishlayapti" - alohida kuzatish kerak
Kichik tabular modellar uchun A odatda soddaroq.
GPU:
xostda drayver + NVIDIA Container Toolkit, image da CUDA runtime
docker run --gpus all ...
CUDA image lar GB lab - CPU inference uchun CPU g'ildiraklari
ISHCHILAR SONI:
konteyner ichida ishchilar soni env dan (ILOVA_ISHCHILAR);
masshtablash - odatda ko'proq KONTEYNER, bitta katta konteyner emas (27.10)Kichik model — image ichida, katta model — registrdan; ikkala holatda ham qaysi model versiyasi ishlayotgani qayd qilinadi.
2.11. Tuzoqlar
Asosiy tuzoqlar: FROM python:latest (bugun va ertaga turli image); COPY . . ni pip install dan oldin yozish (har commit da to'liq qayta qurish); apt-get keshini boshqa RUN da tozalash (oldingi qatlamda qoladi); pip install ni --no-cache-dir siz; sirni ENV/ARG/COPY bilan image ga yozish; .dockerignore siz build (.git, .env, data/ kontekstga tushadi); root sifatida ishlash; CMD ni shell shaklida yozish (SIGTERM yetmaydi); konteyner ichida ma'lumot saqlash (volume siz); alpine ni ML uchun "kichik" deb tanlash; ishlab chiqarishda o'zgaruvchan teg (:latest, :prod) bilan deploy.
3. Tez ma'lumotnoma
# ML API uchun namunaviy Dockerfile
FROM python:3.12.7-slim AS quruvchi
WORKDIR /build
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12.7-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN useradd --create-home --uid 10001 ilova
WORKDIR /app
COPY --from=quruvchi /install /usr/local
COPY --chown=ilova:ilova ilova/ ./ilova/
COPY --chown=ilova:ilova artefakt/model.joblib ./artefakt/
USER ilova
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request as u; u.urlopen('http://localhost:8000/health')"
CMD ["uvicorn", "ilova.server:app", "--host", "0.0.0.0", "--port", "8000"]# .dockerignore
.git
.venv
**/__pycache__
**/*.pyc
.env
.env.*
data/
!data/schema.json
notebooks/
tests/docker build -t churn-api:1.4.2 . # qurish
docker build --progress=plain -t churn-api:dev . # har qadam va CACHED belgilari
docker images churn-api # hajmlar
docker history churn-api:1.4.2 # qatlamlar va ularning hajmi
docker run --rm -p 8000:8000 --env-file .env churn-api:1.4.2
docker run --rm churn-api:1.4.2 id # uid=10001 bo'lishi kerak
docker exec -it <konteyner> sh # ichkariga kirish (tuzatish)
docker compose up --build # API + DB
docker compose down # to'xtatish (volume qoladi)
docker tag churn-api:1.4.2 registry.misol.uz/ml/churn-api:1.4.2
docker push registry.misol.uz/ml/churn-api:1.4.2Tuzilma xulosasi
aniq teg (3.12.7-slim) -> requirements -> pip (--no-cache-dir) -> kod
multi-stage: gcc faqat quruvchi bosqichda
USER ilova, CMD JSON shaklida, HEALTHCHECK
.dockerignore: .git .venv .env data/ notebooks/
sirlar: faqat runtime env / secret, hech qachon ENV/ARG/COPY
konfiguratsiya: env -> pydantic -> ishga tushishda tekshiruv4. Batafsil misollar
Misollar real Python bilan (Python 3.14, pydantic 2.13);
dockerchaqirilmaydi. Qatlam hajmlari va qurish vaqtlari 2-misolda faraziy deb belgilangan.
Misol 1 — Dockerfile linteri noldan
Uchta Dockerfile — yomon, o'rtacha va yaxshi (multi-stage) — matn sifatida tahlil qilinadi: davom qatorlari (\) birlashtiriladi, izohlar tashlanadi, har ko'rsatma qoidalar bo'yicha tekshiriladi. Oxirida linterning o'zi sinaladi: yaxshi faylga bittadan buzilish kiritib, har biri ushlanishini tekshiramiz.
"""Dockerfile lint noldan: qoidalar, ogohlantirishlar va jadval."""
import re
from dataclasses import dataclass
YOMON = """\
FROM python:latest
ENV DB_PAROL=supermaxfiy123
ADD . /app
WORKDIR /app
RUN apt-get update && apt-get install -y gcc
RUN pip install -r requirements.txt
RUN pip install pandas
EXPOSE 8000
CMD python -m ilova.server
"""
ORTA = """\
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN apt-get update && \\
apt-get install -y --no-install-recommends gcc && \\
rm -rf /var/lib/apt/lists/*
RUN pip install --no-cache-dir -r requirements.txt
EXPOSE 8000
CMD ["python", "-m", "ilova.server"]
"""
YAXSHI = """\
# 1-bosqich: qurish (kompilyator shu yerda qoladi)
FROM python:3.12.7-slim AS quruvchi
WORKDIR /build
RUN apt-get update && \\
apt-get install -y --no-install-recommends gcc && \\
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
# 2-bosqich: ishga tushirish (faqat kerakli narsa)
FROM python:3.12.7-slim
ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1
RUN useradd --create-home --uid 10001 ilova
WORKDIR /app
COPY --from=quruvchi /install /usr/local
COPY --chown=ilova:ilova ilova/ ./ilova/
USER ilova
EXPOSE 8000
HEALTHCHECK --interval=30s --timeout=3s CMD python -c "import urllib.request as u; u.urlopen('http://localhost:8000/health')"
CMD ["python", "-m", "ilova.server"]
"""
SIR_SOZLAR = ("PAROL", "PASSWORD", "SECRET", "TOKEN", "API_KEY", "KALIT")
DARAJA_TARTIBI = {"XATO": 0, "OGOH": 1, "MASLAHAT": 2}
@dataclass(frozen=True)
class Korsatma:
qator: int
buyruq: str
argument: str
@dataclass(frozen=True)
class Topilma:
qoida: str
daraja: str
qator: int
xabar: str
def tahlil(matn):
"""Izohlarni tashlaydi, '\\' bilan davom etgan qatorlarni birlashtiradi."""
natija, bufer, boshi = [], "", 0
for i, xom in enumerate(matn.splitlines(), start=1):
qator = xom.strip()
if not bufer and (not qator or qator.startswith("#")):
continue
if not bufer:
boshi = i
if qator.endswith("\\"):
bufer += qator[:-1] + " "
continue
toliq = (bufer + qator).strip()
bufer = ""
buyruq, _, argument = toliq.partition(" ")
natija.append(Korsatma(boshi, buyruq.upper(), argument.strip()))
return natija
def bosqichlar(korsatmalar):
"""Multi-stage: har FROM yangi bosqich boshlaydi."""
royxat = []
for k in korsatmalar:
if k.buyruq == "FROM":
royxat.append([])
if royxat:
royxat[-1].append(k)
return royxat
def lint(matn):
k_lar = tahlil(matn)
topilmalar = []
def qosh(qoida, daraja, qator, xabar):
topilmalar.append(Topilma(qoida, daraja, qator, xabar))
for k in k_lar:
if k.buyruq == "FROM":
image = k.argument.split()[0]
teg = image.split(":")[1] if ":" in image else ""
if teg in ("", "latest"):
qosh("DL01", "XATO", k.qator,
f"pinlanmagan base image: {image}")
elif not re.fullmatch(r"\d+\.\d+\.\d+.*", teg):
qosh("DL02", "MASLAHAT", k.qator,
f"teg to'liq emas (patch yo'q): {image}")
if k.buyruq in ("ENV", "ARG"):
for juft in k.argument.split():
nom, _, qiymat = juft.partition("=")
if any(s in nom.upper() for s in SIR_SOZLAR) and qiymat:
qosh("DL03", "XATO", k.qator,
f"sir {k.buyruq} da: {nom}=*** (image tarixida qoladi)")
if k.buyruq == "ADD" and "://" not in k.argument:
qosh("DL04", "OGOH", k.qator, "mahalliy fayl uchun ADD emas, COPY")
if k.buyruq == "RUN" and "apt-get install" in k.argument:
if "rm -rf /var/lib/apt/lists" not in k.argument:
qosh("DL05", "OGOH", k.qator,
"apt keshi shu RUN ichida tozalanmagan")
if "--no-install-recommends" not in k.argument:
qosh("DL06", "MASLAHAT", k.qator,
"--no-install-recommends yo'q (ortiqcha paketlar)")
if k.buyruq == "RUN" and "pip install" in k.argument:
if "--no-cache-dir" not in k.argument:
qosh("DL07", "OGOH", k.qator,
"pip keshi image ga yoziladi (--no-cache-dir yo'q)")
if "-r " not in k.argument:
paketlar = [s for s in k.argument.split()[2:]
if not s.startswith("-")]
pinsiz = [p for p in paketlar if "==" not in p]
if pinsiz:
qosh("DL08", "OGOH", k.qator,
f"versiyasiz paket: {', '.join(pinsiz)}")
if k.buyruq == "CMD" and not k.argument.startswith("["):
qosh("DL09", "MASLAHAT", k.qator,
"CMD shell shaklida: SIGTERM ilovaga yetmaydi")
# qatlam tartibi: kod butunlay nusxalanib, keyin bog'liqliklar o'rnatilsa
for bosqich in bosqichlar(k_lar):
hammasi_nusxa = None
for k in bosqich:
birinchi = k.argument.split()[0] if k.argument else ""
if (k.buyruq in ("COPY", "ADD") and birinchi == "."
and hammasi_nusxa is None):
hammasi_nusxa = k
if (k.buyruq == "RUN" and "pip install" in k.argument
and hammasi_nusxa is not None):
qosh("DL10", "XATO", hammasi_nusxa.qator,
"butun kod pip install dan OLDIN nusxalangan - "
"har kod o'zgarishida bog'liqliklar qayta o'rnatiladi")
break
# root: faqat OXIRGI bosqich ishga tushadi
oxirgi = bosqichlar(k_lar)[-1]
userlar = [k for k in oxirgi if k.buyruq == "USER"]
if not userlar or userlar[-1].argument in ("root", "0"):
qosh("DL11", "XATO", oxirgi[0].qator,
"USER yo'q - konteyner root sifatida ishlaydi")
if not any(k.buyruq == "HEALTHCHECK" for k in oxirgi):
qosh("DL12", "MASLAHAT", oxirgi[0].qator, "HEALTHCHECK yo'q")
return k_lar, sorted(topilmalar,
key=lambda t: (DARAJA_TARTIBI[t.daraja], t.qator))
def main() -> None:
fayllar = {"yomon": YOMON, "orta": ORTA, "yaxshi": YAXSHI}
print("=== 1. Tahlil: qatorlar -> ko'rsatmalar ===")
k_lar = tahlil(ORTA)
for k in k_lar:
print(f" {k.qator:>2} {k.buyruq:<8} {k.argument[:52]}")
print(f" fayl qatorlari: {len(ORTA.splitlines())}, "
f"ko'rsatmalar: {len(k_lar)} (davom qatorlari birlashdi)")
jadval = {}
for nom, matn in fayllar.items():
print(f"\n=== 2. Lint: {nom} ===")
k_lar, topilmalar = lint(matn)
for t in topilmalar:
print(f" {t.qoida} {t.daraja:<8} q{t.qator:<3} {t.xabar[:58]}")
if not topilmalar:
print(" topilma yo'q")
jadval[nom] = (len(bosqichlar(k_lar)), topilmalar)
print("\n=== 3. Umumiy jadval ===")
print(f" {'fayl':<8} {'bosqich':>7} {'XATO':>5} {'OGOH':>5} "
f"{'MASLAHAT':>9} qaror")
for nom, (n_bosqich, topilmalar) in jadval.items():
son = {d: sum(t.daraja == d for t in topilmalar)
for d in DARAJA_TARTIBI}
qaror = "RAD (CI to'xtaydi)" if son["XATO"] else "o'tdi"
print(f" {nom:<8} {n_bosqich:>7} {son['XATO']:>5} {son['OGOH']:>5} "
f"{son['MASLAHAT']:>9} {qaror}")
print("\n=== 4. Linterning o'zini sinash: yaxshi faylga bitta buzilish ===")
mutatsiyalar = {
"USER qatori o'chirildi": YAXSHI.replace("USER ilova\n", ""),
"teg -> latest": YAXSHI.replace("3.12.7-slim\nENV",
"latest\nENV"),
"ENV ga token": YAXSHI.replace("PYTHONUNBUFFERED=1",
"PYTHONUNBUFFERED=1 HF_TOKEN=abc"),
"COPY . . pip dan oldin": YAXSHI.replace(
"COPY requirements.txt .", "COPY . ."),
}
kutilgan = {"USER qatori o'chirildi": "DL11", "teg -> latest": "DL01",
"ENV ga token": "DL03", "COPY . . pip dan oldin": "DL10"}
otdi = 0
for nom, matn in mutatsiyalar.items():
_, tl = lint(matn)
qoidalar = sorted({t.qoida for t in tl})
ushladi = kutilgan[nom] in qoidalar
otdi += ushladi
holat = "ushlandi" if ushladi else "o'tkazib yuborildi"
print(f" {nom:<24} -> {', '.join(qoidalar) or '-':<10} {holat}")
print(f" {otdi}/{len(mutatsiyalar)} buzilish ushlandi")
print("\n=== 5. Qoidalar qamrovi ===")
barcha = sorted({t.qoida for _, tl in jadval.values() for t in tl})
print(f" ishlagan qoidalar: {len(barcha)} ta: {', '.join(barcha)}")
yaxshi_xato = [t for t in jadval["yaxshi"][1] if t.daraja == "XATO"]
if not yaxshi_xato:
print(" yaxshi Dockerfile da XATO yo'q - qoidalar ortiqcha qattiq emas")
print(" ⭐ Lint - CI da har Dockerfile o'zgarishida arzon tekshiruv")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Tahlil: qatorlar -> ko'rsatmalar ===
1 FROM python:3.12-slim
2 WORKDIR /app
3 COPY . .
4 RUN apt-get update && apt-get install -y --no-install-r
7 RUN pip install --no-cache-dir -r requirements.txt
8 EXPOSE 8000
9 CMD ["python", "-m", "ilova.server"]
fayl qatorlari: 9, ko'rsatmalar: 7 (davom qatorlari birlashdi)
=== 2. Lint: yomon ===
DL01 XATO q1 pinlanmagan base image: python:latest
DL11 XATO q1 USER yo'q - konteyner root sifatida ishlaydi
DL03 XATO q2 sir ENV da: DB_PAROL=*** (image tarixida qoladi)
DL10 XATO q3 butun kod pip install dan OLDIN nusxalangan - har kod o'zg
DL04 OGOH q3 mahalliy fayl uchun ADD emas, COPY
DL05 OGOH q5 apt keshi shu RUN ichida tozalanmagan
DL07 OGOH q6 pip keshi image ga yoziladi (--no-cache-dir yo'q)
DL07 OGOH q7 pip keshi image ga yoziladi (--no-cache-dir yo'q)
DL08 OGOH q7 versiyasiz paket: pandas
DL12 MASLAHAT q1 HEALTHCHECK yo'q
DL06 MASLAHAT q5 --no-install-recommends yo'q (ortiqcha paketlar)
DL09 MASLAHAT q9 CMD shell shaklida: SIGTERM ilovaga yetmaydi
=== 2. Lint: orta ===
DL11 XATO q1 USER yo'q - konteyner root sifatida ishlaydi
DL10 XATO q3 butun kod pip install dan OLDIN nusxalangan - har kod o'zg
DL02 MASLAHAT q1 teg to'liq emas (patch yo'q): python:3.12-slim
DL12 MASLAHAT q1 HEALTHCHECK yo'q
=== 2. Lint: yaxshi ===
topilma yo'q
=== 3. Umumiy jadval ===
fayl bosqich XATO OGOH MASLAHAT qaror
yomon 1 4 5 3 RAD (CI to'xtaydi)
orta 1 2 0 2 RAD (CI to'xtaydi)
yaxshi 2 0 0 0 o'tdi
=== 4. Linterning o'zini sinash: yaxshi faylga bitta buzilish ===
USER qatori o'chirildi -> DL11 ushlandi
teg -> latest -> DL01 ushlandi
ENV ga token -> DL03 ushlandi
COPY . . pip dan oldin -> DL10 ushlandi
4/4 buzilish ushlandi
=== 5. Qoidalar qamrovi ===
ishlagan qoidalar: 12 ta: DL01, DL02, DL03, DL04, DL05, DL06, DL07, DL08, DL09, DL10, DL11, DL12
yaxshi Dockerfile da XATO yo'q - qoidalar ortiqcha qattiq emas
⭐ Lint - CI da har Dockerfile o'zgarishida arzon tekshiruvNatija tahlili.
- 1-bo'lim: o'rtacha Dockerfile ning 9 qatori 7 ta ko'rsatmaga aylandi — uch qatorli
RUN apt-get ... \bitta ko'rsatma bo'ldi va 4-qator raqami bilan qoldi. Linter xabarlaridagi qator raqamlari shuning uchun ko'rsatma boshlangan qatorni bildiradi. - 2-bo'lim, yomon fayl: 12 ta topilma, shulardan 4 tasi XATO:
python:latest, root,ENV DB_PAROLvaADD . /appningpip installdan oldin kelishi. Diqqat qiling: sirning qiymati lint xabarida ham***bilan yashirilgan — linter chiqishi CI loglariga tushadi, u yerda ham sir bo'lmasligi kerak. - O'rtacha fayl ko'rinishidan yaxshi:
-slim,--no-cache-dir, apt keshi tozalangan,CMDJSON shaklida. Lekin ikki XATO qoldi — root vaCOPY . .ningpip installdan oldin kelishi. Bu "deyarli to'g'ri" Dockerfile lar eng keng tarqalgani: ishlaydi, lekin sekin va xavfli. - Yaxshi fayl — ikki bosqichli (
bosqichustunida 2), topilma yo'q. Root qoidasi faqat oxirgi bosqichni tekshiradi: quruvchi bosqichda root bo'lishi normal, chunki u ishga tushmaydi. - 4-bo'lim — linterni sinash: to'rt buzilishning to'rttasi ham ushlandi (4/4). Bu mutatsion test g'oyasi: tekshiruvchi vosita ham tekshirilishi kerak, aks holda "topilma yo'q" ning ma'nosi noma'lum — balki qoida umuman ishlamayotgandir.
Haqiqiy loyihada hadolint shu ishni yuzlab qoidalar bilan qiladi (u RUN ichidagi shell kodni ham tahlil qiladi). G'oya bir xil: Dockerfile — kod, u ham CI da tekshiriladi.
Misol 2 — Qatlam keshi simulyatsiyasi
Har qatlam kaliti ota kalit, ko'rsatma matni va nusxalangan fayllar mazmunidan xeshlanadi — xuddi 2.4-bo'limdagi sxema. Ikki tartib (yomon va yaxshi) bir xil o'zgarishlar ostida taqqoslanadi: nechta qatlam qayta quriladi, necha soniya ketadi va serverga necha MB yuklab olinadi. Qatlam hajmlari (pip install — 380 MB) va vaqtlari (95 s) faraziy, lekin tipik ML image tartibida.
"""Qatlam keshi simulyatsiyasi: kalit zanjiri, ikki tartib, qayta qurish narxi.
Qatlam hajmlari va qurish vaqtlari FARAZIY (tipik ML image ga yaqin
tartibda tanlangan), haqiqiy o'lchov emas.
"""
import hashlib
import random
from dataclasses import dataclass
import numpy as np
# faraziy: (hajm MB, qurish soniyasi)
FARAZIY = {"FROM": (45.0, 0.0), "WORKDIR": (0.0, 0.0), "ENV": (0.0, 0.0),
"COPY": (None, 1.0), "RUN": (380.0, 95.0), "CMD": (0.0, 0.0)}
@dataclass(frozen=True)
class Qadam:
buyruq: str
argument: str
@dataclass(frozen=True)
class Qatlam:
qadam: Qadam
kalit: str
hajm_mb: float
soniya: float
YOMON_TARTIB = [
Qadam("FROM", "python:3.12.7-slim"),
Qadam("WORKDIR", "/app"),
Qadam("COPY", "."),
Qadam("RUN", "pip install --no-cache-dir -r requirements.txt"),
Qadam("CMD", '["python", "-m", "ilova.server"]'),
]
YAXSHI_TARTIB = [
Qadam("FROM", "python:3.12.7-slim"),
Qadam("WORKDIR", "/app"),
Qadam("COPY", "requirements.txt"),
Qadam("RUN", "pip install --no-cache-dir -r requirements.txt"),
Qadam("COPY", "ilova/"),
Qadam("CMD", '["python", "-m", "ilova.server"]'),
]
def boshlangich_fayllar():
return {
"requirements.txt": b"numpy==2.3.1\npandas==3.0.0\n"
b"scikit-learn==1.9.0\nfastapi==0.141.0\n",
"ilova/server.py": b"# API kodi\n" + b"x = 1\n" * 400,
"ilova/model.py": b"# model yuklash\n" + b"y = 2\n" * 300,
"README.md": b"# Loyiha\n",
"tests/test_api.py": b"def test_ok():\n assert True\n",
}
def mos_fayllar(fayllar, manba):
if manba == ".":
return sorted(fayllar)
if manba.endswith("/"):
return sorted(f for f in fayllar if f.startswith(manba))
return [manba]
def qur(qadamlar, fayllar):
"""Har qatlam kaliti = xesh(ota kalit + ko'rsatma + nusxalangan fayllar)."""
qatlamlar, ota = [], ""
for q in qadamlar:
h = hashlib.sha256((ota + q.buyruq + " " + q.argument).encode())
hajm, soniya = FARAZIY[q.buyruq]
if q.buyruq == "COPY":
nomlar = mos_fayllar(fayllar, q.argument)
for nom in nomlar:
h.update(nom.encode() + hashlib.sha256(fayllar[nom]).digest())
hajm = sum(len(fayllar[n]) for n in nomlar) / 1e6
kalit = h.hexdigest()[:12]
qatlamlar.append(Qatlam(q, kalit, hajm, soniya))
ota = kalit
return qatlamlar
def natija(qatlamlar, kesh, serverda):
"""Keshda yo'q qatlamlar quriladi; serverda yo'q qatlamlar yuklanadi."""
qayta = [q for q in qatlamlar if q.kalit not in kesh]
soniya = sum(q.soniya for q in qayta)
yuklab = sum(q.hajm_mb for q in qatlamlar if q.kalit not in serverda)
return qayta, soniya, yuklab
def main() -> None:
tartiblar = {"yomon (COPY . . oldin)": YOMON_TARTIB,
"yaxshi (requirements oldin)": YAXSHI_TARTIB}
print("=== 1. Kalit zanjiri (yaxshi tartib, birinchi qurish) ===")
fayllar = boshlangich_fayllar()
for q in qur(YAXSHI_TARTIB, fayllar):
print(f" {q.kalit} {q.qadam.buyruq:<8} {q.qadam.argument[:34]:<34} "
f"{q.hajm_mb:>8.3f} MB")
print(" FROM qatlami mashinada bor (bir marta tortilgan) deb olamiz")
ssenariylar = [
("hech narsa o'zgarmadi", lambda f: None),
("ilova/model.py o'zgardi",
lambda f: f.__setitem__("ilova/model.py", f["ilova/model.py"] + b"#")),
("README.md o'zgardi",
lambda f: f.__setitem__("README.md", b"# Loyiha v2\n")),
("requirements.txt o'zgardi",
lambda f: f.__setitem__("requirements.txt",
f["requirements.txt"] + b"joblib==1.5.1\n")),
]
print("\n=== 2. Bitta o'zgarish: nima qayta quriladi ===")
print(f" {'ssenariy':<26} {'tartib':<28} {'qayta':>5} "
f"{'soniya':>6} {'yuklab MB':>9}")
for nom, ozgartir in ssenariylar:
for t_nom, qadamlar in tartiblar.items():
asl = qur(qadamlar, boshlangich_fayllar())
kesh = {q.kalit for q in asl}
yangi_fayllar = boshlangich_fayllar()
ozgartir(yangi_fayllar)
qayta, soniya, yuklab = natija(qur(qadamlar, yangi_fayllar),
kesh, kesh)
belgilar = ",".join(q.qadam.buyruq for q in qayta) or "-"
print(f" {nom:<26} {t_nom:<28} {len(qayta):>5} {soniya:>6.0f} "
f"{yuklab:>9.3f} [{belgilar}]")
print("\n=== 3. 40 ta commit tarixi (urug' 7) ===")
rng = random.Random(7)
turlar = rng.choices(["kod", "hujjat", "bogliqlik"],
weights=[0.80, 0.12, 0.08], k=40)
print(f" commitlar: kod {turlar.count('kod')}, hujjat "
f"{turlar.count('hujjat')}, bogliqlik {turlar.count('bogliqlik')}")
jami = {}
for t_nom, qadamlar in tartiblar.items():
f = boshlangich_fayllar()
oldingi = qur(qadamlar, f)
kesh = {q.kalit for q in oldingi}
soniyalar, yuklamalar = [], []
for i, tur in enumerate(turlar):
if tur == "kod":
f["ilova/server.py"] += f"# o'zgarish {i}\n".encode()
elif tur == "hujjat":
f["README.md"] += f"qator {i}\n".encode()
else:
f["requirements.txt"] += f"paket{i}==1.0\n".encode()
qatlamlar = qur(qadamlar, f)
_, soniya, yuklab = natija(qatlamlar, kesh, kesh)
kesh |= {q.kalit for q in qatlamlar}
soniyalar.append(soniya)
yuklamalar.append(yuklab)
jami[t_nom] = (np.array(soniyalar), np.array(yuklamalar))
print(f" {t_nom:<28} qurish {sum(soniyalar):>6.0f} s, "
f"serverga yuklab {sum(yuklamalar):>8.1f} MB")
print("\n=== 4. Juftlashgan farq (bir xil commitlar, yomon - yaxshi) ===")
(s_y, m_y), (s_x, m_x) = jami.values()
for nom, a, b in [("soniya", s_y, s_x), ("MB", m_y, m_x)]:
d = a - b
se = d.std(ddof=1) / np.sqrt(len(d))
belgi = "sezilarli" if abs(d.mean()) > 2 * se else "sezilarsiz"
print(f" {nom:<7} o'rtacha farq {d.mean():>8.2f} +- {se:.2f} (SE) "
f"-> {belgi}")
ulush = (m_y.sum() - m_x.sum()) / m_y.sum()
print(f" yaxshi tartib yuklamani {ulush:.1%} ga kamaytirdi")
print("\n=== 5. Tuzoq: RUN kaliti natijaga emas, MATNGA bog'liq ===")
pinsiz = [Qadam("FROM", "python:3.12.7-slim"),
Qadam("RUN", "pip install --no-cache-dir pandas")]
k1 = qur(pinsiz, {})[-1].kalit
k2 = qur(pinsiz, {})[-1].kalit
print(f" dushanba kaliti: {k1}, juma kaliti: {k2}, teng: {k1 == k2}")
print(" PyPI da yangi pandas chiqsa ham kesh ESKI qatlamni beradi;")
print(" keshsiz mashinada esa YANGI versiya o'rnatiladi -> ikki xil image")
print(" yechim: versiyani pinlash (pandas==3.0.0) - matn = natija")
print(" ⭐ Kam o'zgaradigan narsa tepada, tez o'zgaradigani pastda")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Kalit zanjiri (yaxshi tartib, birinchi qurish) ===
bd146a98f1e6 FROM python:3.12.7-slim 45.000 MB
53d9795bc465 WORKDIR /app 0.000 MB
526fe4126d81 COPY requirements.txt 0.000 MB
7c223b978278 RUN pip install --no-cache-dir -r requ 380.000 MB
907aea1d3374 COPY ilova/ 0.004 MB
7e4900179101 CMD ["python", "-m", "ilova.server"] 0.000 MB
FROM qatlami mashinada bor (bir marta tortilgan) deb olamiz
=== 2. Bitta o'zgarish: nima qayta quriladi ===
ssenariy tartib qayta soniya yuklab MB
hech narsa o'zgarmadi yomon (COPY . . oldin) 0 0 0.000 [-]
hech narsa o'zgarmadi yaxshi (requirements oldin) 0 0 0.000 [-]
ilova/model.py o'zgardi yomon (COPY . . oldin) 3 96 380.004 [COPY,RUN,CMD]
ilova/model.py o'zgardi yaxshi (requirements oldin) 2 1 0.004 [COPY,CMD]
README.md o'zgardi yomon (COPY . . oldin) 3 96 380.004 [COPY,RUN,CMD]
README.md o'zgardi yaxshi (requirements oldin) 0 0 0.000 [-]
requirements.txt o'zgardi yomon (COPY . . oldin) 3 96 380.004 [COPY,RUN,CMD]
requirements.txt o'zgardi yaxshi (requirements oldin) 4 97 380.004 [COPY,RUN,COPY,CMD]
=== 3. 40 ta commit tarixi (urug' 7) ===
commitlar: kod 35, hujjat 3, bogliqlik 2
yomon (COPY . . oldin) qurish 3840 s, serverga yuklab 15200.2 MB
yaxshi (requirements oldin) qurish 229 s, serverga yuklab 760.2 MB
=== 4. Juftlashgan farq (bir xil commitlar, yomon - yaxshi) ===
soniya o'rtacha farq 90.28 +- 3.35 (SE) -> sezilarli
MB o'rtacha farq 361.00 +- 13.26 (SE) -> sezilarli
yaxshi tartib yuklamani 95.0% ga kamaytirdi
=== 5. Tuzoq: RUN kaliti natijaga emas, MATNGA bog'liq ===
dushanba kaliti: a156ecc6a623, juma kaliti: a156ecc6a623, teng: True
PyPI da yangi pandas chiqsa ham kesh ESKI qatlamni beradi;
keshsiz mashinada esa YANGI versiya o'rnatiladi -> ikki xil image
yechim: versiyani pinlash (pandas==3.0.0) - matn = natija
⭐ Kam o'zgaradigan narsa tepada, tez o'zgaradigani pastdaNatija tahlili.
- 1-bo'lim: har kalit oldingi kalitdan hosil bo'ladi — bu zanjir. Shuning uchun bitta qatlam o'zgarsa, undan pastdagi hamma kalitlar o'zgaradi.
- 2-bo'lim — asosiy natija:
ilova/model.pyo'zgarganda yomon tartibda 3 qatlam (shu jumladan 95 soniyalikpip install) qayta quriladi va serverga 380 MB tortiladi; yaxshi tartibda — 2 qatlam, 1 soniya va 0.004 MB. README o'zgarishi yanada ibratli: yomon tartibda u ham to'liq qayta qurishga olib keladi, yaxshi tartibda esa hech narsa qayta qurilmaydi — chunki README image ga umuman nusxalanmaydi. - Kutilgandan farqli nuqta (halol):
requirements.txto'zgarganda yaxshi tartib biroz yomonroq — 4 qatlam va 97 soniya, yomon tartibda 3 qatlam va 96 soniya, chunki kod qatlami ham qayta quriladi. Farq 1 soniya — bog'liqliklar kam o'zgargani uchun bu narx arzimas. - 3-4-bo'limlar — 40 commit: tarixda 35 ta kod, 3 ta hujjat va 2 ta bog'liqlik o'zgarishi. Jami qurish vaqti 3840 soniyadan 229 soniyaga, yuklab olinadigan hajm 15200.2 MB dan 760.2 MB ga tushdi (95.0% kamaydi). Commit bo'yicha juftlashgan farq
90.28 +- 3.35soniya — SE dan yigirma martadan ortiq, ya'ni tasodif emas. - 5-bo'lim — tuzoq:
pip install pandasning kaliti "dushanba" va "juma" kuni bir xil — kesh PyPI da nima o'zgarganini bilmaydi. Keshli mashina eski, keshsiz CI yangi versiyani o'rnatadi. Pinlangan versiya bilan buyruq matni natijani to'liq belgilaydi.
Haqiqiy Docker docker build --progress=plain chiqishida har qadamga CACHED belgisini qo'yadi — bu misolning qayta ustuni aynan shuni hisoblaydi.
Misol 3 — .dockerignore ta'siri: haqiqiy papkada kontekst hajmi
Vaqtinchalik papkada tipik ML loyihasi yaratiladi: kod, 11 MB xom ma'lumot, noutbuk, .git, .venv, __pycache__ va .env. Keyin .dockerignore qoidalari (**, *, ! istisno, "oxirgi mos kelgan g'olib") noldan amalga oshiriladi va build kontekst uch variantda hisoblanadi.
""".dockerignore ta'siri: haqiqiy vaqtinchalik papkada build kontekst hajmi."""
import hashlib
import random
import re
import tempfile
from pathlib import Path
LOYIHA = { # nisbiy yo'l -> hajm (bayt)
"Dockerfile": 600,
"requirements.txt": 120,
"ilova/__init__.py": 0,
"ilova/server.py": 4_000,
"ilova/model.py": 3_000,
"artefakt/model.joblib": 1_500_000,
"data/raw/buyurtmalar_2025.csv": 6_000_000,
"data/raw/buyurtmalar_2026.csv": 5_000_000,
"data/schema.json": 800,
"notebooks/eda.ipynb": 900_000,
".git/objects/pack/pack-1.pack": 3_500_000,
".git/HEAD": 30,
".venv/lib/site-packages/numpy/core.so": 2_500_000,
"ilova/__pycache__/server.cpython-312.pyc": 9_000,
"tests/test_api.py": 1_500,
".env": 90,
"README.md": 700,
}
ZAIF_IGNORE = """\
.git
"""
YAXSHI_IGNORE = """\
# versiya nazorati va muhit
.git
.venv
**/__pycache__
**/*.pyc
# sirlar - HECH QACHON kontekstga
.env
.env.*
# katta ma'lumot va tahlil
data/
!data/schema.json
notebooks/
tests/
README.md
"""
def naqsh_regex(naqsh):
"""Soddalashtirilgan Docker qoidasi: ** - istalgan chuqurlik, * - bitta
bo'lak ichida, naqsh papkaga mos kelsa - uning ichidagi hamma narsa."""
naqsh = naqsh.strip("/")
r, i = "", 0
while i < len(naqsh):
if naqsh.startswith("**/", i):
r += "(?:.*/)?"
i += 3
elif naqsh.startswith("**", i):
r += ".*"
i += 2
elif naqsh[i] == "*":
r += "[^/]*"
i += 1
elif naqsh[i] == "?":
r += "[^/]"
i += 1
else:
r += re.escape(naqsh[i])
i += 1
return re.compile(r + "(?:/.*)?")
def qoidalar(matn):
natija = []
for qator in matn.splitlines():
qator = qator.strip()
if not qator or qator.startswith("#"):
continue
inkor = qator.startswith("!")
natija.append((inkor, naqsh_regex(qator.lstrip("!"))))
return natija
def chiqarilganmi(nisbiy, qoida_royxati):
"""Oxirgi mos kelgan qoida g'olib (Docker ham shunday)."""
holat = False
for inkor, rx in qoida_royxati:
if rx.fullmatch(nisbiy):
holat = not inkor
return holat
def kontekst(ildiz, ignore_matni):
q = qoidalar(ignore_matni) if ignore_matni else []
fayllar = []
for yol in ildiz.rglob("*"):
if yol.is_file():
nisbiy = yol.relative_to(ildiz).as_posix()
if nisbiy == ".dockerignore":
continue
if not chiqarilganmi(nisbiy, q):
fayllar.append(nisbiy)
return sorted(fayllar) # OS tartibiga tayanmaymiz
def kontekst_xeshi(ildiz, fayllar):
h = hashlib.sha256()
for nisbiy in fayllar:
h.update(nisbiy.encode() + (ildiz / nisbiy).read_bytes())
return h.hexdigest()[:12]
def main() -> None:
rng = random.Random(0)
with tempfile.TemporaryDirectory(prefix="kontekst_") as papka:
ildiz = Path(papka)
for nisbiy, hajm in LOYIHA.items():
yol = ildiz / nisbiy
yol.parent.mkdir(parents=True, exist_ok=True)
if nisbiy == ".env":
matn = "DB_URL=postgresql://ilova:SINOV-QIYMAT@db:5432/ml\n"
yol.write_text(matn, encoding="utf-8")
else:
yol.write_bytes(rng.randbytes(hajm))
print("=== 1. Loyiha papkasi (haqiqiy fayllar) ===")
jami = sum(p.stat().st_size for p in ildiz.rglob("*") if p.is_file())
print(f" fayllar: {sum(1 for p in ildiz.rglob('*') if p.is_file())}, "
f"jami {jami / 1e6:.2f} MB")
variantlar = {"ignore yo'q": "", "faqat .git": ZAIF_IGNORE,
"yaxshi .dockerignore": YAXSHI_IGNORE}
print("\n=== 2. Build kontekst: docker build . nima yuboradi ===")
print(f" {'variant':<22} {'fayl':>4} {'MB':>7} {'.env':>6} "
f"{'.git':>6} {'data':>6}")
natijalar = {}
for nom, matn in variantlar.items():
f = kontekst(ildiz, matn)
mb = sum((ildiz / n).stat().st_size for n in f) / 1e6
natijalar[nom] = f
env = "BOR" if ".env" in f else "yo'q"
git = "BOR" if any(n.startswith(".git/") for n in f) else "yo'q"
data = sum(n.startswith("data/") for n in f)
print(f" {nom:<22} {len(f):>4} {mb:>7.2f} {env:>6} {git:>6} "
f"{data:>6}")
print("\n=== 3. Xavfsizlik tekshiruvi ===")
for nom, f in natijalar.items():
xavfli = [n for n in f if n == ".env" or n.startswith(".env.")]
if xavfli:
print(f" {nom:<22} XAVF: {', '.join(xavfli)} kontekstda - "
f"COPY . . uni image ga yozadi")
else:
print(f" {nom:<22} sir fayllar kontekstda yo'q")
print("\n=== 4. Yaxshi ignore: nima qoldi ===")
for n in natijalar["yaxshi .dockerignore"]:
print(f" {n}")
print(" data/ chiqarildi, lekin !data/schema.json qaytarildi")
print("\n=== 5. Kesh bilan bog'liqligi: noutbuk o'zgardi ===")
oldin = {k: kontekst_xeshi(ildiz, f) for k, f in natijalar.items()}
nb = ildiz / "notebooks" / "eda.ipynb"
nb.write_bytes(nb.read_bytes() + b"yangi katak")
for nom in variantlar:
f = kontekst(ildiz, variantlar[nom])
keyin = kontekst_xeshi(ildiz, f)
holat = ("o'zgardi -> COPY . . keshi buziladi" if keyin != oldin[nom]
else "o'zgarmadi -> kesh saqlanadi")
print(f" {nom:<22} {oldin[nom]} -> {keyin} {holat}")
print("\n=== 6. Qoida tartibi muhim: oxirgi mos kelgan g'olib ===")
for matn in ["data/\n!data/schema.json\n", "!data/schema.json\ndata/\n"]:
q = qoidalar(matn)
kirdi = not chiqarilganmi("data/schema.json", q)
print(f" {matn.replace(chr(10), ' | '):<34} schema.json "
f"{'kontekstda' if kirdi else 'chiqarildi'}")
print(" ⭐ .dockerignore - tezlik VA xavfsizlik: .env hech qachon kirmasin")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Loyiha papkasi (haqiqiy fayllar) ===
fayllar: 17, jami 19.42 MB
=== 2. Build kontekst: docker build . nima yuboradi ===
variant fayl MB .env .git data
ignore yo'q 17 19.42 BOR BOR 3
faqat .git 15 15.92 BOR yo'q 3
yaxshi .dockerignore 7 1.51 yo'q yo'q 1
=== 3. Xavfsizlik tekshiruvi ===
ignore yo'q XAVF: .env kontekstda - COPY . . uni image ga yozadi
faqat .git XAVF: .env kontekstda - COPY . . uni image ga yozadi
yaxshi .dockerignore sir fayllar kontekstda yo'q
=== 4. Yaxshi ignore: nima qoldi ===
Dockerfile
artefakt/model.joblib
data/schema.json
ilova/__init__.py
ilova/model.py
ilova/server.py
requirements.txt
data/ chiqarildi, lekin !data/schema.json qaytarildi
=== 5. Kesh bilan bog'liqligi: noutbuk o'zgardi ===
ignore yo'q 37667f1d3b98 -> 0d98e8eb478f o'zgardi -> COPY . . keshi buziladi
faqat .git c155b77f32fe -> c5575fbeec36 o'zgardi -> COPY . . keshi buziladi
yaxshi .dockerignore 8901076740f7 -> 8901076740f7 o'zgarmadi -> kesh saqlanadi
=== 6. Qoida tartibi muhim: oxirgi mos kelgan g'olib ===
data/ | !data/schema.json | schema.json kontekstda
!data/schema.json | data/ | schema.json chiqarildi
⭐ .dockerignore - tezlik VA xavfsizlik: .env hech qachon kirmasinNatija tahlili.
- 2-bo'lim: ignore siz
docker build .builder ga 17 fayl va 19.42 MB yuboradi. Faqat.gitni chiqarish 15.92 MB qoldiradi — hali ham.venv, xom ma'lumot va noutbuk bor. Yaxshi.dockerignorebilan 7 fayl va 1.51 MB — taxminan 13 marta kam. Haqiqiy loyihalardadata/va.venvgigabaytlab bo'ladi, shunda farq "build 2 soniya" va "build 3 daqiqa" orasida. - 3-bo'lim — xavfsizlik: ignore siz ham, "faqat .git" bilan ham
.envkontekstda. Dockerfile daCOPY . .bo'lsa, u image qatlamiga yoziladi va image ni tortgan har kim uni o'qiy oladi. Bu tekshiruvni CI ga qo'shish oson: kontekst ro'yxatida.envbo'lsa — build to'xtaydi. - 4-bo'lim: model artefakti (
artefakt/model.joblib) kontekstda qoldi — 2.10-bo'limdagi A variant: model image ichida yetkaziladi.data/chiqarildi, lekin!data/schema.jsonistisnosi sxemani qaytardi (sxema kirish validatsiyasi uchun kerak). - 5-bo'lim — kesh bilan bog'liqlik: noutbukka bitta katak qo'shildi. Ignore siz variantlarda kontekst xeshi o'zgardi —
COPY . .keshi buziladi va 2-misoldagi to'liq qayta qurish boshlanadi. Yaxshi ignore bilan xesh o'zgarmadi. Ya'ni.dockerignorefaqat hajm emas, kesh barqarorligi masalasi ham. - 6-bo'lim: qoidalar tartibi muhim.
data/keyin!data/schema.json— fayl kontekstda; teskari tartibda — chiqarildi, chunki oxirgi mos kelgan qoida g'olib.
Bizning naqsh moslashtiruvchi soddalashtirilgan — haqiqiy Docker Go ning filepath.Match qoidalarini ishlatadi, lekin **, *, ! va tartib qoidasi bir xil ma'noda.
Misol 4 — 12-factor konfiguratsiya: env, pydantic va sirni niqoblash
Ilova sozlamalari faqat ILOVA_ prefiksli muhit o'zgaruvchilaridan o'qiladi va pydantic modeli bilan tekshiriladi: turlar, oraliqlar, ruxsat etilgan qiymatlar, noma'lum kalitlar va muhitlararo qoida (prod da DEBUG yo'q). Sirlar SecretStr da saqlanadi va log filtri ularni *** ga almashtiradi. os.environ misol ichida vaqtincha o'zgartiriladi va finally bilan qaytariladi. Misoldagi parol va kalit — sinov qiymatlari.
"""12-factor konfiguratsiya: env -> pydantic, validatsiya, sirni niqoblash."""
import logging
import os
import sys
from contextlib import contextmanager
from typing import Literal
from pydantic import (BaseModel, ConfigDict, Field, SecretStr, ValidationError,
model_validator)
PREFIKS = "ILOVA_"
class Sozlamalar(BaseModel):
model_config = ConfigDict(frozen=True, extra="forbid")
muhit: Literal["dev", "staging", "prod"]
port: int = Field(8000, ge=1, le=65535)
ishchilar: int = Field(2, ge=1, le=32)
model_yoli: str
chegara: float = Field(0.5, gt=0, lt=1)
log_darajasi: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
db_url: SecretStr
api_kalit: SecretStr
@model_validator(mode="after")
def prod_qoidalari(self):
if self.muhit == "prod" and self.log_darajasi == "DEBUG":
raise ValueError("prod da DEBUG log taqiqlangan (sirlar oqishi)")
return self
def env_dan_yukla(env=None):
"""ILOVA_* o'zgaruvchilarini yig'ib, pydantic ga beradi."""
env = os.environ if env is None else env
xom = {k[len(PREFIKS):].lower(): v for k, v in env.items()
if k.startswith(PREFIKS)}
return Sozlamalar.model_validate(xom)
@contextmanager
def vaqtincha_env(**qiymatlar):
"""os.environ ni vaqtincha o'zgartiradi va ALBATTA qaytaradi."""
eski = {k: os.environ.get(k) for k in qiymatlar}
try:
for k, v in qiymatlar.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
yield
finally:
for k, v in eski.items():
if v is None:
os.environ.pop(k, None)
else:
os.environ[k] = v
class SirNiqobi(logging.Filter):
"""Log xabarida uchragan har qanday sir qiymatini *** ga almashtiradi."""
def __init__(self, sirlar):
super().__init__()
self.sirlar = [s for s in sirlar if s]
def filter(self, yozuv):
xabar = yozuv.getMessage()
for s in self.sirlar:
xabar = xabar.replace(s, "***")
yozuv.msg, yozuv.args = xabar, None
return True
def xatolarni_chiqar(xato):
for e in xato.errors():
joy = ".".join(str(x) for x in e["loc"])
nom = PREFIKS + joy.upper() if joy else "(butun model)"
print(f" {nom}: {e['type']} - {e['msg'][:62].rstrip()}")
def main() -> None:
# SINOV qiymatlari - haqiqiy sir emas; haqiqiy tizimda ular
# orkestrator/secret manager tomonidan env ga qo'yiladi
sinov_parol = "sinov-parol-" + "x7"
sinov_kalit = "sk-sinov-" + "0" * 8
asos = {
"ILOVA_MUHIT": "prod",
"ILOVA_PORT": "8080",
"ILOVA_MODEL_YOLI": "/models/churn/v3/model.joblib",
"ILOVA_DB_URL": f"postgresql://ilova:{sinov_parol}@db:5432/ml",
"ILOVA_API_KALIT": sinov_kalit,
"PATH_BOSHQA": "bu o'zgaruvchi e'tiborga olinmaydi",
}
print("=== 1. To'g'ri konfiguratsiya (env satrlari -> turlar) ===")
s = env_dan_yukla(asos)
print(f" port: {s.port!r} ({type(s.port).__name__}), "
f"chegara: {s.chegara}, ishchilar: {s.ishchilar} (sukut)")
print(f" repr: {str(s)[:74]}")
print(f" db_url repr: {s.db_url!r}")
print("\n=== 2. Noto'g'ri konfiguratsiya - ishga tushishda darhol xato ===")
holatlar = {
"API kalit berilmagan": {"ILOVA_API_KALIT": None},
"port matn": {"ILOVA_PORT": "sakkiz ming"},
"port oraliqdan tashqari": {"ILOVA_PORT": "70000"},
"muhit imlo xatosi": {"ILOVA_MUHIT": "production"},
"o'zgaruvchi nomida xato": {"ILOVA_CHEGRA": "0.7"},
"prod + DEBUG": {"ILOVA_LOG_DARAJASI": "DEBUG"},
}
for nom, ozgarish in holatlar.items():
env = {**asos, **ozgarish}
env = {k: v for k, v in env.items() if v is not None}
try:
env_dan_yukla(env)
print(f" {nom}: qabul qilindi (kutilmagan)")
except ValidationError as xato:
print(f" {nom}: {xato.error_count()} xato")
xatolarni_chiqar(xato)
print("\n=== 3. Haqiqiy os.environ bilan, vaqtincha ===")
oldin = {k: os.environ.get(k) for k in asos}
with vaqtincha_env(**asos):
s2 = env_dan_yukla()
print(f" ichkarida: muhit={s2.muhit}, port={s2.port}")
qaytdi = all(os.environ.get(k) == v for k, v in oldin.items())
print(f" chiqqandan keyin os.environ asl holatida: {qaytdi}")
print("\n=== 4. Sirni loglarda niqoblash ===")
log = logging.getLogger("ilova")
log.setLevel(logging.INFO)
log.propagate = False
ishlovchi = logging.StreamHandler(sys.stdout)
ishlovchi.setFormatter(logging.Formatter(" %(levelname)s %(message)s"))
log.addHandler(ishlovchi)
print(" niqobsiz (xavfli):")
log.info("DB ga ulanish: %s", s.db_url.get_secret_value())
niqob = SirNiqobi([s.db_url.get_secret_value(),
s.api_kalit.get_secret_value(), sinov_parol])
ishlovchi.addFilter(niqob)
print(" niqob bilan:")
log.info("DB ga ulanish: %s", s.db_url.get_secret_value())
log.info("parol alohida chiqsa ham: %s", sinov_parol)
log.info("sozlamalar: %s", s.model_dump_json(include={"muhit", "db_url"}))
log.removeHandler(ishlovchi)
print("\n=== 5. Bitta image, uch muhit ===")
muhitlar = {
"dev": {"ILOVA_MUHIT": "dev", "ILOVA_LOG_DARAJASI": "DEBUG",
"ILOVA_ISHCHILAR": "1"},
"staging": {"ILOVA_MUHIT": "staging", "ILOVA_ISHCHILAR": "2"},
"prod": {"ILOVA_MUHIT": "prod", "ILOVA_ISHCHILAR": "8"},
}
for nom, ozgarish in muhitlar.items():
m = env_dan_yukla({**asos, **ozgarish})
print(f" {nom:<8} log={m.log_darajasi:<5} ishchilar={m.ishchilar} "
f"port={m.port}")
print(" kod va image bir xil - faqat env farq qiladi")
print(" ⭐ Konfiguratsiya - env da, validatsiya - ishga tushishda, "
"sir - hech qachon logda")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. To'g'ri konfiguratsiya (env satrlari -> turlar) ===
port: 8080 (int), chegara: 0.5, ishchilar: 2 (sukut)
repr: muhit='prod' port=8080 ishchilar=2 model_yoli='/models/churn/v3/model.jobl
db_url repr: SecretStr('**********')
=== 2. Noto'g'ri konfiguratsiya - ishga tushishda darhol xato ===
API kalit berilmagan: 1 xato
ILOVA_API_KALIT: missing - Field required
port matn: 1 xato
ILOVA_PORT: int_parsing - Input should be a valid integer, unable to parse string as an
port oraliqdan tashqari: 1 xato
ILOVA_PORT: less_than_equal - Input should be less than or equal to 65535
muhit imlo xatosi: 1 xato
ILOVA_MUHIT: literal_error - Input should be 'dev', 'staging' or 'prod'
o'zgaruvchi nomida xato: 1 xato
ILOVA_CHEGRA: extra_forbidden - Extra inputs are not permitted
prod + DEBUG: 1 xato
(butun model): value_error - Value error, prod da DEBUG log taqiqlangan (sirlar oqishi)
=== 3. Haqiqiy os.environ bilan, vaqtincha ===
ichkarida: muhit=prod, port=8080
chiqqandan keyin os.environ asl holatida: True
=== 4. Sirni loglarda niqoblash ===
niqobsiz (xavfli):
INFO DB ga ulanish: postgresql://ilova:sinov-parol-x7@db:5432/ml
niqob bilan:
INFO DB ga ulanish: ***
INFO parol alohida chiqsa ham: ***
INFO sozlamalar: {"muhit":"prod","db_url":"**********"}
=== 5. Bitta image, uch muhit ===
dev log=DEBUG ishchilar=1 port=8080
staging log=INFO ishchilar=2 port=8080
prod log=INFO ishchilar=8 port=8080
kod va image bir xil - faqat env farq qiladi
⭐ Konfiguratsiya - env da, validatsiya - ishga tushishda, sir - hech qachon logdaNatija tahlili.
- 1-bo'lim: muhit o'zgaruvchilari har doim satr —
"8080". Pydantic uniintga aylantirdi, berilmaganishchilarsukut qiymatini oldi,ILOVA_prefiksi yo'qPATH_BOSHQAe'tiborga olinmadi.SecretStrningrepri'**********'— sozlamalar obyektini tasodifan chop etish sirni oshkor qilmaydi. - 2-bo'lim — "fail fast": olti xil xato konfiguratsiyaning har biri ishga tushishda aniq xabar bilan to'xtatildi: qaysi o'zgaruvchi va nima noto'g'ri. Eng qimmatlisi —
ILOVA_CHEGRA(imlo xatosi):extra="forbid"bo'lmasa, u jim e'tiborsiz qolar va model sukutchegara=0.5bilan ishlardi — hech kim sezmasdan.prod + DEBUG— bitta maydonga emas, butun modelga tegishli qoida, shuning uchun joyi(butun model). - 3-bo'lim:
vaqtincha_envhaqiqiyos.environni o'zgartirdi vafinallybilan qaytardi. Testlarda muhitni shu tarzda (yokipytestningmonkeypatch.setenvi bilan) o'zgartirish kerak — aks holda bir test o'zgartirgan muhit keyingi testga "oqib" o'tadi. - 4-bo'lim: niqobsiz log DB manzilini parol bilan birga chiqardi — bu log keyin log tizimiga, u yerdan kirish huquqi kengroq odamlarga boradi. Filtr qo'shilgach, to'liq URL ham, parolning alohida o'zi ham
***ga almashdi,model_dump_jsonesaSecretStrni o'zi niqobladi. - 5-bo'lim: bitta image, uch muhit — kod o'zgarmadi, faqat env. Sinovdan (staging) o'tgan image aynan shu holida prod ga boradi.
Haqiqiy loyihada pydantic-settings (2.8-bo'lim) env o'qish va prefiksni o'zi bajaradi, sirlar esa orkestratorning secret mexanizmi yoki secret manager orqali env ga qo'yiladi.
5. To'g'ri va noto'g'ri tushunishlar
| Noto'g'ri fikr | To'g'risi |
|---|---|
| "Konteyner — kichik virtual mashina" | Izolyatsiya qilingan jarayon, yadro xost bilan umumiy |
"latest doim eng yangi va yaxshi" |
Bugun va ertaga turli image; takrorlanmaydi |
"RUN rm .env sirni o'chiradi" |
Oldingi qatlamda qoladi, image dan ochib olinadi |
"EXPOSE portni ochadi" |
Faqat hujjat; portni -p / orkestrator ochadi |
"ENV ga sir yozish mumkin, image yopiq" |
Image ni tortgan har kim docker inspect bilan ko'radi |
| "Qatlamlar tartibi faqat estetik" | Har commit da yuzlab MB va daqiqalar farqi |
"alpine — eng kichik, demak eng yaxshi" |
ML g'ildiraklari yo'q, manbadan kompilyatsiya |
| "Root konteyner ichida xavfsiz" | Zaiflikda hujumchi root; yadro umumiy |
| "Konfiguratsiya xatosi birinchi so'rovda chiqadi" | Ishga tushishda tekshirish kerak (fail fast) |
".dockerignore faqat tezlik uchun" |
Tezlik, kesh barqarorligi VA sir xavfsizligi |
6. Keng tarqalgan xatolar va yechimlari
1. Pinlanmagan base image
FROM python:latest # ⚠️
FROM python:3.12.7-slim # ✅ (yoki @sha256:... digest)2. Kod bog'liqliklardan oldin
COPY . . # ⚠️ har commit da pip qayta
RUN pip install -r requirements.txt
COPY requirements.txt . # ✅
RUN pip install --no-cache-dir -r requirements.txt
COPY ilova/ ./ilova/3. apt keshi boshqa qatlamda
RUN apt-get update && apt-get install -y gcc # ⚠️
RUN rm -rf /var/lib/apt/lists/* # kesh oldingi qatlamda qoldi
RUN apt-get update && \
apt-get install -y --no-install-recommends gcc && \
rm -rf /var/lib/apt/lists/* # ✅ bitta RUN ichida4. Sir image ichida
ENV API_KALIT=sk-... # ⚠️
COPY .env /app/.env # ⚠️
# ✅ Dockerfile da sir YO'Q; ishga tushishda:
# docker run --env-file .env ... yoki orkestrator secret5. Root sifatida ishlash
CMD ["python", "-m", "ilova.server"] # ⚠️ USER yo'q -> root
RUN useradd --create-home --uid 10001 ilova # ✅
USER ilova
CMD ["python", "-m", "ilova.server"]6. Shell shaklidagi CMD
CMD python -m ilova.server # ⚠️ PID 1 = sh
CMD ["python", "-m", "ilova.server"] # ✅ SIGTERM ilovaga7. Konfiguratsiyani tekshirmasdan o'qish
port = int(os.environ.get("PORT", 8000)) # ⚠️ "PROT" imlo xatosi jim
chegara = os.environ.get("CHEGARA") # ⚠️ None yoki satr
sozlamalar = env_dan_yukla() # ✅ pydantic, extra="forbid"8. .dockerignore siz build
docker build . # ⚠️ .git, .venv, data/, .env -> kontekstga
# ✅ .dockerignore: .git .venv .env .env.* data/ notebooks/ **/__pycache__7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 27.2-dars (o'tilgan): pinlangan
requirements.txtva lock fayl — Dockerfile dagipip installshunga tayanadi - 27.5-dars (o'tilgan): model registri — model image ichida yoki ishga tushishda registrdan
- 27.7, 27.8-darslar (o'tilgan): FastAPI ilovasi va uning testlari — image ichidagi
CMDaynan shu ilovani ishga tushiradi; testlar image qurilishidan oldin CI da ishlaydi - 27.10-dars: konteynerlar bulutga qanday joylanadi, qanday masshtablanadi va yangi versiya qanday xavfsiz chiqariladi
- 27.11-dars:
HEALTHCHECKva strukturali loglar monitoringning boshlang'ich nuqtasi - 27.14-dars: amaliyot — butun quvur bitta loyihada, image va konfiguratsiya bilan
8. Eng yaxshi amaliyotlar
Base image — aniq teg (
3.12.7-slim), ishlab chiqarishda digest.Kam o'zgaradigan tepada: requirements, keyin kod.
Multi-stage: kompilyator faqat quruvchi bosqichda.
--no-cache-dir, apt keshi shuRUNichida tozalanadi.Root emas foydalanuvchi (
USER),CMDJSON shaklida,HEALTHCHECK..dockerignore:.git,.venv,.env,data/,notebooks/.Sirlar faqat ishga tushishda (env / secret); build sirlari —
--mount=type=secret.Konfiguratsiya env dan, pydantic bilan ishga tushishda tekshiriladi.
Dockerfile ham kod: CI da lint va image skaneri.
Image tegi — git xesh yoki semver;
latestbilan deploy yo'q.
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
1. Konteyner va VM ning asosiy farqi nima?
2. Image va konteyner farqi?
3. Teg va digest farqi?
4. Nima uchun requirements.txt koddan oldin nusxalanadi?
5. RUN qatlamining kesh kaliti nimaga bog'liq?
6. Multi-stage build nima beradi?
7. Nima uchun ML uchun alpine ko'pincha yomon tanlov?
8. RUN rm .env sirni o'chiradimi?
9. EXPOSE portni ochadimi?
10. CMD ning shell shakli nima uchun xavfli?
11. .dockerignore nimaga ta'sir qiladi (uch narsa)?
12. extra="forbid" nima uchun kerak?Javoblar
- Konteyner xost yadrosini bo'lishadi, VM o'z OT si bilan
- Image — o'zgarmas shablon, konteyner — uning ishlayotgan nusxasi
- Teg ko'chirilishi mumkin, digest — mazmun xeshi, o'zgarmas
- Kod o'zgarganda
pip installqatlami keshdan olinishi uchun - Ota kalit va buyruq MATNIGA (natijaga emas)
- Qurish asboblari yakuniy image ga tushmaydi — kichik va xavfsiz
- musl libc: ko'p g'ildiraklar yo'q, manbadan kompilyatsiya
- Yo'q — oldingi qatlamda qoladi
- Yo'q — faqat hujjat
- PID 1 = sh, SIGTERM ilovaga yetmaydi
- Build tezligi, kesh barqarorligi, sir xavfsizligi
- Imlo xatoli o'zgaruvchi jim e'tiborsiz qolmasligi uchun
Vazifa 2: Xatolarni tuzating
Quyidagi Dockerfile da kamida 7 ta muammo bor. Toping va tuzatilgan variantni yozing.
FROM python
ENV HF_TOKEN=hf_abc123
COPY . /app
WORKDIR /app
RUN apt-get update
RUN apt-get install -y build-essential
RUN pip install -r requirements.txt
RUN pip install torch
CMD python serve.pyJavob
Muammolar: (1) teg yo'q — latest; (2) token ENV da; (3) COPY . /app pip dan oldin; (4) apt-get update va install alohida RUN da — eskirgan indeks keshlanadi; (5) apt keshi tozalanmagan va --no-install-recommends yo'q; (6) --no-cache-dir yo'q; (7) torch versiyasiz va CUDA li to'liq g'ildirak; (8) root; (9) CMD shell shaklida; (10) build-essential yakuniy image da qoladi.
FROM python:3.12.7-slim AS quruvchi
RUN apt-get update && \
apt-get install -y --no-install-recommends build-essential && \
rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
# requirements.txt ichida: torch==2.5.1 (CPU index bilan), qolganlari ham pinlangan
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt
FROM python:3.12.7-slim
RUN useradd --create-home --uid 10001 ilova
WORKDIR /app
COPY --from=quruvchi /install /usr/local
COPY --chown=ilova:ilova serve.py ./
USER ilova
CMD ["python", "serve.py"]
# HF_TOKEN ishga tushishda: docker run -e HF_TOKEN=... yoki secretVazifa 3: Linterni kengaytiring
1-misoldagi linterga uch qoida qo'shing: (a) RUN apt-get update alohida RUN da (install siz) — OGOH; (b) WORKDIR siz COPY nisbiy yo'lga — MASLAHAT; (c) EXPOSE dagi port CMD dagi --port bilan mos emas — OGOH. Har qoida uchun mutatsion test yozing (4-bo'lim kabi) va yaxshi Dockerfile da yolg'on topilma yo'qligini tekshiring.
Vazifa 4: Kesh simulyatsiyasi
2-misolni kengaytiring:
- Uchinchi tartib qo'shing:
COPY requirements.txt+RUN pip+COPY ilova/+COPY artefakt/model.joblib(model 50 MB, faraziy). Model har 10 commit da yangilanadi deb, uch tartibni taqqoslang. - Model qatlamini koddan oldin yoki keyin qo'yish qaysi biri yaxshi? Nima uchun?
- Juftlashgan farq va SE bilan xulosa qiling.
Vazifa 5: .dockerignore auditi
3-misoldagi funksiyalardan foydalanib, kontekst_auditi(ildiz, ignore_matni) yozing: (a) eng katta 5 fayl, (b) sir bo'lishi mumkin fayllar (.env*, *.pem, *id_rsa*, credentials*.json), (c) 1 MB dan katta fayllar, (d) umumiy hajm. Chegaradan oshsa (> 50 MB yoki sir topilsa) — SystemExit(1), CI uchun.
Vazifa 6: Konfiguratsiya
4-misoldagi Sozlamalar ga qo'shing:
ILOVA_RUXSAT_ETILGAN_KELIBCHIQISH— vergul bilan ajratilgan domenlar ro'yxati ("a.uz,b.uz"->["a.uz", "b.uz"]).- Qoida:
muhit == "prod"bo'lsamodel_yoli/models/bilan boshlanishi shart. - Sozlamalarning "xavfsiz ko'rinishi" funksiyasi: sirlarsiz lug'at — ishga tushishda logga yozish uchun.
Vazifa 7: O'ylash
Jamoangizda ML API image i 4.2 GB, CI da har commit da build 11 daqiqa davom etadi va dasturchilar "Docker sekin" deb shikoyat qiladi. Kimdir "Docker dan voz kechib, serverda virtualenv ishlatamiz" deb taklif qildi. Nima qilasiz?
Javob
Qisqa javob: muammo Docker da emas, Dockerfile da. Voz kechishdan oldin o'lchang va to'rtta odatiy sababni tekshiring — ko'pincha image 5-10 marta kichrayadi va build daqiqalardan soniyalarga tushadi.
1. Avval o'lchash
docker history --no-trunc churn-api:dev # qaysi qatlam qancha
docker build --progress=plain . # qaysi qadam CACHED emas2. Odatiy sabablar va yechimlar
| Belgi | Sabab | Yechim |
|---|---|---|
pip install har commit da |
COPY . . requirements dan oldin |
Tartib (2-misol) |
| Image GB lab | CUDA li torch, CPU da ishlasa ham | CPU g'ildiragi |
| Image GB lab | python:3.12 to'liq, gcc |
-slim + multi-stage |
| Kontekst yuborish uzoq | .dockerignore yo'q, data/ ichida |
.dockerignore (3-misol) |
| CI da kesh yo'q | har build toza mashinada | registry kesh (--cache-from) |
3. Nima uchun virtualenv yechim emas
- Python versiyasi, tizim kutubxonalari (
libgomp,libstdc++) yana "mashinaga bog'liq" bo'ladi — darsning boshidagi jim noto'g'ri natija muammosi qaytadi - rollback murakkablashadi: image da oldingi tegni ishga tushirish kifoya, virtualenv da — qayta o'rnatish
- masshtablash 27.10-bob konteynerlarga tayanadi
4. Qaror qoidasi
O'zgarishlarni bittadan kiriting va har birining ta'sirini o'lchang (image hajmi, keshli va keshsiz build vaqti). "Eng sodda yetarli" tamoyili: ko'pincha faqat qatlam tartibi va .dockerignore build vaqtini 11 daqiqadan 1 daqiqadan kamga tushiradi — multi-stage va distroless ga faqat kerak bo'lsa o'ting.
Xulosa: sekin Docker — odatda yomon yozilgan Dockerfile belgisi. Voz kechish takrorlanuvchanlikni yo'qotadi; tuzatish esa bir necha qator o'zgarish.
Nimani mustahkamlaydi: 2.4, 2.7, 2.8-bo'limlar.
Xulosa
Bu darsda model API sini konteynerga joylashni va Dockerfile ni to'g'ri yozishni ko'rdik.
Eng muhim uch fikr:
Qatlam tartibi — tezlikning asosi. Kesh kaliti zanjir: bitta qatlam o'zgarsa, pastdagi hammasi qayta quriladi. 2-misolda 40 commit tarixida
requirements.txtni koddan oldin nusxalash qurish vaqtini 3840 soniyadan 229 soniyaga va serverga yuklanadigan hajmni 95.0% ga kamaytirdi (faraziy qatlam hajmlari bilan).RUNkeshi buyruq matniga bog'liq — shuning uchun versiyalar pinlanadi.Xavfsizlik uch qatlamli: root emas foydalanuvchi, image da sir yo'q,
.dockerignore. 1-misoldagi linter yomon Dockerfile da 4 XATO topdi, "deyarli to'g'ri" faylda ham 2 ta (root va tartib). 3-misolda.dockerignoresiz.envbuild kontekstiga tushdi, yaxshi ignore esa kontekstni 19.42 MB dan 1.51 MB ga qisqartirdi va noutbuk o'zgarishi keshni buzmaydigan qildi.Konfiguratsiya — muhitda, tekshiruv — ishga tushishda. Bitta image dev, staging va prod da ishlaydi; farq faqat env da. 4-misolda pydantic olti xil xato konfiguratsiyani ishga tushishdayoq aniq xabar bilan to'xtatdi (jumladan imlo xatoli o'zgaruvchi nomini), sirlar esa
SecretStrva log filtri bilan niqoblandi.
Keyingi darsda Bulut va deploy strategiyalari: tayyor image ni qayerda ishga tushirish (VM, konteyner xizmati, Kubernetes, serverless), nechta replika kerakligini navbat nazariyasi bilan hisoblash va yangi versiyani foydalanuvchilarga zarar yetkazmasdan chiqarish.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!