IlmHamroh
Data Science va sun'iy intellekt/MLOps va deploy9/14-dars47 daqiqa
Mundarija (25)

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 docker buyrug'i chaqirilmaydi — Dockerfile va loyiha papkasi Python bilan tahlil qilinadi, shuning uchun natija istalgan mashinada bir xil. Dockerfile, compose va bash buyruqlari alohida bloklarda — ularni o'z mashinangizda sinab ko'ring.


2. Nazariya — chuqur tushuntirish

2.1. Konteyner va virtual mashina

text
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

text
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'ZGARMAS

Image — 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

dockerfile
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:

text
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

text
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 COPY

Qoida 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

dockerfile
# 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/
text
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 kodi

Multi-stage — qurish asboblari yakuniy image ga tushmaydi; faqat oxirgi bosqich ishga tushadi va yetkaziladi.

2.6. Base image tanlash

text
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

text
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 oqishi
text
QATLAMLAR "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

text
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 niqoblangan

Pydantic bilan buni BaseModel va kichik yuklovchi funksiya bilan qilish mumkin (4-misol). Katta loyihalarda pydantic-settings paketi xuddi shu ishni tayyor holda qiladi:

python
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 tekshiradi

Bitta image, ko'p muhit — farq faqat muhit o'zgaruvchilarida; ular ishga tushishda tekshiriladi.

2.9. docker-compose: API + ma'lumotlar bazasi

yaml
# 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.txt
text
MUHIM 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

text
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

dockerfile
# 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"]
text
# .dockerignore
.git
.venv
**/__pycache__
**/*.pyc
.env
.env.*
data/
!data/schema.json
notebooks/
tests/
bash
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.2

Tuzilma xulosasi

text
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 tekshiruv

4. Batafsil misollar

Misollar real Python bilan (Python 3.14, pydantic 2.13); docker chaqirilmaydi. 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.

python
"""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:

text
=== 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 tekshiruv

Natija 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_PAROL va ADD . /app ning pip install dan 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, CMD JSON shaklida. Lekin ikki XATO qoldi — root va COPY . . ning pip install dan oldin kelishi. Bu "deyarli to'g'ri" Dockerfile lar eng keng tarqalgani: ishlaydi, lekin sekin va xavfli.
  • Yaxshi fayl — ikki bosqichli (bosqich ustunida 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.

python
"""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:

text
=== 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 pastda

Natija 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.py o'zgarganda yomon tartibda 3 qatlam (shu jumladan 95 soniyalik pip 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.txt o'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.35 soniya — SE dan yigirma martadan ortiq, ya'ni tasodif emas.
  • 5-bo'lim — tuzoq: pip install pandas ning 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.

python
""".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:

text
=== 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 kirmasin

Natija tahlili.

  • 2-bo'lim: ignore siz docker build . builder ga 17 fayl va 19.42 MB yuboradi. Faqat .git ni chiqarish 15.92 MB qoldiradi — hali ham .venv, xom ma'lumot va noutbuk bor. Yaxshi .dockerignore bilan 7 fayl va 1.51 MB — taxminan 13 marta kam. Haqiqiy loyihalarda data/ va .venv gigabaytlab bo'ladi, shunda farq "build 2 soniya" va "build 3 daqiqa" orasida.
  • 3-bo'lim — xavfsizlik: ignore siz ham, "faqat .git" bilan ham .env kontekstda. Dockerfile da COPY . . 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 .env bo'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.json istisnosi 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 .dockerignore faqat 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.

python
"""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:

text
=== 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 logda

Natija tahlili.

  • 1-bo'lim: muhit o'zgaruvchilari har doim satr — "8080". Pydantic uni int ga aylantirdi, berilmagan ishchilar sukut qiymatini oldi, ILOVA_ prefiksi yo'q PATH_BOSHQA e'tiborga olinmadi. SecretStr ning repr i '**********' — 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 sukut chegara=0.5 bilan ishlardi — hech kim sezmasdan. prod + DEBUG — bitta maydonga emas, butun modelga tegishli qoida, shuning uchun joyi (butun model).
  • 3-bo'lim: vaqtincha_env haqiqiy os.environ ni o'zgartirdi va finally bilan qaytardi. Testlarda muhitni shu tarzda (yoki pytest ning monkeypatch.setenv i 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_json esa SecretStr ni 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

dockerfile
FROM python:latest                                   # ⚠️
FROM python:3.12.7-slim                              # ✅ (yoki @sha256:... digest)

2. Kod bog'liqliklardan oldin

dockerfile
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

dockerfile
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 ichida

4. Sir image ichida

dockerfile
ENV API_KALIT=sk-...                                 # ⚠️
COPY .env /app/.env                                  # ⚠️
# ✅ Dockerfile da sir YO'Q; ishga tushishda:
#    docker run --env-file .env ... yoki orkestrator secret

5. Root sifatida ishlash

dockerfile
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

dockerfile
CMD python -m ilova.server                           # ⚠️ PID 1 = sh
CMD ["python", "-m", "ilova.server"]                 # ✅ SIGTERM ilovaga

7. Konfiguratsiyani tekshirmasdan o'qish

python
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

text
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.txt va lock fayl — Dockerfile dagi pip install shunga 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 CMD aynan 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: HEALTHCHECK va strukturali loglar monitoringning boshlang'ich nuqtasi
  • 27.14-dars: amaliyot — butun quvur bitta loyihada, image va konfiguratsiya bilan

8. Eng yaxshi amaliyotlar

  1. Base image — aniq teg (3.12.7-slim), ishlab chiqarishda digest.

  2. Kam o'zgaradigan tepada: requirements, keyin kod.

  3. Multi-stage: kompilyator faqat quruvchi bosqichda.

  4. --no-cache-dir, apt keshi shu RUN ichida tozalanadi.

  5. Root emas foydalanuvchi (USER), CMD JSON shaklida, HEALTHCHECK.

  6. .dockerignore: .git, .venv, .env, data/, notebooks/.

  7. Sirlar faqat ishga tushishda (env / secret); build sirlari — --mount=type=secret.

  8. Konfiguratsiya env dan, pydantic bilan ishga tushishda tekshiriladi.

  9. Dockerfile ham kod: CI da lint va image skaneri.

  10. Image tegi — git xesh yoki semver; latest bilan deploy yo'q.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

text
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
  1. Konteyner xost yadrosini bo'lishadi, VM o'z OT si bilan
  2. Image — o'zgarmas shablon, konteyner — uning ishlayotgan nusxasi
  3. Teg ko'chirilishi mumkin, digest — mazmun xeshi, o'zgarmas
  4. Kod o'zgarganda pip install qatlami keshdan olinishi uchun
  5. Ota kalit va buyruq MATNIGA (natijaga emas)
  6. Qurish asboblari yakuniy image ga tushmaydi — kichik va xavfsiz
  7. musl libc: ko'p g'ildiraklar yo'q, manbadan kompilyatsiya
  8. Yo'q — oldingi qatlamda qoladi
  9. Yo'q — faqat hujjat
  10. PID 1 = sh, SIGTERM ilovaga yetmaydi
  11. Build tezligi, kesh barqarorligi, sir xavfsizligi
  12. 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.

dockerfile
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.py
Javob

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.

dockerfile
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 secret

Vazifa 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:

  1. 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.
  2. Model qatlamini koddan oldin yoki keyin qo'yish qaysi biri yaxshi? Nima uchun?
  3. 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:

  1. ILOVA_RUXSAT_ETILGAN_KELIBCHIQISH — vergul bilan ajratilgan domenlar ro'yxati ("a.uz,b.uz" -> ["a.uz", "b.uz"]).
  2. Qoida: muhit == "prod" bo'lsa model_yoli /models/ bilan boshlanishi shart.
  3. 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

bash
docker history --no-trunc churn-api:dev     # qaysi qatlam qancha
docker build --progress=plain .             # qaysi qadam CACHED emas

2. 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:

  1. Qatlam tartibi — tezlikning asosi. Kesh kaliti zanjir: bitta qatlam o'zgarsa, pastdagi hammasi qayta quriladi. 2-misolda 40 commit tarixida requirements.txt ni koddan oldin nusxalash qurish vaqtini 3840 soniyadan 229 soniyaga va serverga yuklanadigan hajmni 95.0% ga kamaytirdi (faraziy qatlam hajmlari bilan). RUN keshi buyruq matniga bog'liq — shuning uchun versiyalar pinlanadi.

  2. 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 .dockerignore siz .env build kontekstiga tushdi, yaxshi ignore esa kontekstni 19.42 MB dan 1.51 MB ga qisqartirdi va noutbuk o'zgarishi keshni buzmaydigan qildi.

  3. 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 SecretStr va 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.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
27.9-dars: Docker bilan konteynerlash — IlmHamroh