Asosiy qismga o'tish

Docker: muhitni ham qulflash

7-bob 19 kod bloki 2393 so'z

Bu bobdagi kod shu fayllar bilan ishlaydio'z kompyuteringizda ishga tushiring

"Mening kompyuterimda ishlaydi"

Kodingizni hamkasbingizga berdingiz. Yarim soatdan keyin xabar keladi:

— Ishlamayapti. Xato beryapti.

Siz o'zingizda ochasiz — ishlaydi. Bir xil kod, bir xil fayl, bir xil buyruq.

Sabab kodda emas. Sizda Python 3.11, unda 3.10. Sizda scikit-learn 1.5, unda 1.3. Sizda tizimda bir kutubxona o'rnatilgan, unda yo'q.

Bu dasturlashdagi eng eski hazil, va u har kuni takrorlanadi.

Retsept emas, oshxonani yuboring

Muammoning ildizi shunda: siz retsept yubordingiz, lekin oshxona boshqa.

Retseptda "180 darajada 40 daqiqa" deb yozilgan. Lekin uning duxovkasi boshqacha qizidi. Retsept to'g'ri, natija boshqa.

Yagona ishonchli yechim — retsept bilan birga butun oshxonani yuborish: duxovka, tarozi, idishlar, hatto un va tuzning aynan o'sha markasi.

Kompyuterda buni qilish mumkin, va u konteyner deyiladi.

Docker ning ishi aynan shu: u kodingizni Python, kutubxonalar va tizim fayllari bilan birga bitta qutiga joylaydi. Hamkasbingiz o'sha qutini ishga tushiradi — va uning mashinasida nima o'rnatilgani ahamiyatsiz bo'lib qoladi.

Takrorlanishning ikkinchi darajasi

2-bobda takrorlanishning uch darajasi bor edi. Birinchisini bajardik: seed va ma'lumot hash i bilan bir xil mashinada natijani qaytara olamiz.

Ikkinchi daraja qiyinroq: boshqa mashinada. U yerda boshqa Python, boshqa kutubxona versiyasi, boshqa tizim kutubxonalari bo'lishi mumkin.

python17 qator
diagramma(
    {
        "Sizning mashinangiz\nPython 3.10, sklearn 1.7": (0, 0, 3.0, 1.1, "manba"),
        "Hamkasb mashinasi\nPython 3.12, sklearn 1.3": (0, 2.2, 3.0, 1.1, "manba"),
        "Server\nPython 3.9, sklearn 1.1": (0, 4.4, 3.0, 1.1, "manba"),
        "DOCKER IMAGE\n(aynan bitta muhit)": (4.2, 2.2, 3.0, 1.1, "qayta"),
        "Bir xil natija\nhamma joyda": (8.0, 2.2, 2.8, 1.1, "saqlash"),
    },
    [
        ("Sizning mashinangiz\nPython 3.10, sklearn 1.7", "DOCKER IMAGE\n(aynan bitta muhit)"),
        ("Hamkasb mashinasi\nPython 3.12, sklearn 1.3", "DOCKER IMAGE\n(aynan bitta muhit)"),
        ("Server\nPython 3.9, sklearn 1.1", "DOCKER IMAGE\n(aynan bitta muhit)"),
        ("DOCKER IMAGE\n(aynan bitta muhit)", "Bir xil natija\nhamma joyda"),
    ],
    nom="docker_nima_uchun.png", kenglik=13,
    izoh="Docker muhitni ham artefaktga aylantiradi: Python, kutubxonalar va tizim fayllari birga qadoqlanadi.",
)

Docker muhitni ham artefaktga aylantiradi: Python, kutubxonalar va tizim fayllari birga qadoqlanadi.

Docker muhitni ham artefaktga aylantiradi: Python, kutubxonalar va tizim fayllari birga qadoqlanadi.

Docker muhitni fayl qilib qadoqlaydi. Shu fayl (image) qayerda ishga tushsa, ichi bir xil.

bash2 qator
docker --version
docker compose version
Chiqish (qaytish kodi 0, 30 ms)
Docker version 29.2.1, build a5c7197
Docker Compose version v5.0.2

Birinchi Dockerfile — sodda, lekin yomon

Avval eng oddiy variantni yozamiz va uni o'lchaymiz:

shu faylni yaratingDockerfile.sodda

python8 qator
# ENG SODDA variant — ataylab optimallashtirilmagan.
FROM python:3.11

WORKDIR /app
COPY . .
RUN pip install scikit-learn pandas fastapi uvicorn

CMD ["python", "-c", "print('salom')"]

Uni qurishdan oldin muhim narsa: Docker COPY . . bilan butun papkani nusxalaydi — jumladan .venv, .ombor, ma'lumot fayllari va __pycache__. Buning oldini .dockerignore oladi:

shu faylni yarating.dockerignore

matn13 qator
# Image ga TUSHMASLIGI kerak bo'lgan narsalar
.venv/
.ombor/
__pycache__/
*.pyc
.pytest_cache/
.mypy_cache/
.git/
*.csv
*.parquet
*.json
modellar/
namuna/
bash2 qator
docker build -q -f Dockerfile.sodda -t mlops-kitob:sodda . 2>&1 | tail -2
docker images mlops-kitob:sodda --format "sodda variant: {{.Size}}"
Chiqish (qaytish kodi 0, 8415 ms)
sha256:acfecdbfcd38ecb2b71dad7eb1805b5f570ed6973145f662a2dd48136e0caf0a
sodda variant: 2.27GB

Yaxshilangan variant

Uchta o'zgarish qilamiz va har birining sababi bor:

shu faylni yaratingDockerfile

python27 qator
# 1-QADAM: quruvchi (builder) — bu yerda kutubxonalar o'rnatiladi
FROM python:3.11-slim AS quruvchi

WORKDIR /app

# Avval FAQAT bog'liqliklar ro'yxati nusxalanadi.
# Sabab: kod o'zgarganda bu qatlam KESHDAN olinadi, pip qayta ishlamaydi.
COPY talablar.txt .
RUN pip install --no-cache-dir --user -r talablar.txt

# 2-QADAM: yakuniy image — faqat kerakli narsalar
FROM python:3.11-slim

# xavfsizlik: root emas, oddiy foydalanuvchi
RUN useradd --create-home --uid 10001 ishchi
USER ishchi
WORKDIR /app

# quruvchidan faqat o'rnatilgan paketlar ko'chiriladi
COPY --from=quruvchi --chown=ishchi:ishchi /root/.local /home/ishchi/.local
ENV PATH=/home/ishchi/.local/bin:$PATH \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

COPY --chown=ishchi:ishchi src/ ./src/

CMD ["python", "-c", "import sklearn, pandas; print('tayyor:', sklearn.__version__)"]

shu faylni yaratingtalablar.txt

python3 qator
scikit-learn==1.7.2
pandas==2.3.3
numpy==2.2.6
bash2 qator
docker build -q -t mlops-kitob:yaxshi . 2>&1 | tail -2
docker images --format "{{.Repository}}:{{.Tag}}  {{.Size}}" | grep mlops-kitob
Chiqish (qaytish kodi 0, 2723 ms)
sha256:2e929b10725324206e398bba021c67e51760952f8e4a05bac65cb3fa12a379bf
mlops-kitob:yaxshi  665MB
mlops-kitob:sodda  2.27GB

Farq katta: 2.27 GB dan 665 MB ga, ya'ni uch barobardan ko'p kichrayadi.

Farqni jadvalga yig'amiz:

bash3 qator
docker images --format "{{.Tag}}|{{.Size}}" | grep -E "^(sodda|yaxshi)" | while IFS='|' read tag size; do
  printf "  %-8s %s\n" "$tag" "$size"
done
Chiqish (qaytish kodi 0, 296 ms)
  yaxshi   665MB
  sodda    2.27GB

Nima uchun kichrayadi

python13 qator
import pandas as pd
show(pd.DataFrame([
    {"o'zgarish": "python:3.11 -> python:3.11-slim",
     "sabab": "Asosiy image dan kompilyator va keraksiz tizim paketlari olib tashlangan"},
    {"o'zgarish": "Ko'p bosqichli qurish (multi-stage)",
     "sabab": "pip ning keshi va qurish artefaktlari yakuniy image ga tushmaydi"},
    {"o'zgarish": "--no-cache-dir",
     "sabab": "pip yuklab olgan .whl fayllarini saqlamaydi"},
    {"o'zgarish": ".dockerignore",
     "sabab": "Ma'lumot, .venv va __pycache__ umuman nusxalanmaydi"},
    {"o'zgarish": "COPY src/ (hammasi emas)",
     "sabab": "Faqat kerakli kod; test va hujjatlar tushmaydi"},
]))
o'zgarishsabab
python:3.11 -> python:3.11-slimAsosiy image dan kompilyator va keraksiz tizim paketlari olib tashlangan
Ko'p bosqichli qurish (multi-stage)pip ning keshi va qurish artefaktlari yakuniy image ga tushmaydi
--no-cache-dirpip yuklab olgan .whl fayllarini saqlamaydi
.dockerignoreMa'lumot, .venv va __pycache__ umuman nusxalanmaydi
COPY src/ (hammasi emas)Faqat kerakli kod; test va hujjatlar tushmaydi

Qatlam keshi: nima uchun tartib muhim

Bu bo'lim Dockerfile yozishdagi eng ko'p vaqt tejaydigan bilim, va u ko'p qo'llanmada aytilmaydi.

Dockerfile dagi har buyruq qatlam (layer) yasaydi. Qatlamlar bir-birining ustiga qo'yiladi, xuddi tort qavatlari kabi.

Docker juda foydali bir ish qiladi: agar qatlam o'zgarmagan bo'lsa, uni qaytadan tayyorlamaydi — keshdan oladi.

Lekin bitta shafqatsiz qoida bor: bir qatlam o'zgarsa, undan yuqoridagi hamma qatlam qaytadan tayyorlanadi.

Tortni tasavvur qiling. Pastki qavatni almashtirmoqchisiz — demak ustidagi hamma qavatni olib tashlab, qaytadan terishingiz kerak. Eng yuqoridagi qavatni almashtirsangiz esa, qolganiga tegmaysiz.

Shuning uchun tartib muhim. COPY talablar.txt oldin, COPY src/ keyin yoziladi: kodni o'zgartirganda pip install qatlami pastda qoladi va qayta ishlamaydi.

Buni raqam bilan ko'rsatamiz.

O'lchaymiz — faqat kodni o'zgartirib, qayta quramiz:

bash4 qator
echo "# kichik o'zgarish $(date +%s)" >> src/quvur/takrorlanish.py
t0=$SECONDS
docker build -q -t mlops-kitob:yaxshi . > /dev/null 2>&1
echo "kod o'zgardi, qayta qurish: $((SECONDS - t0)) s   (pip KESHDAN olindi)"
Chiqish (qaytish kodi 0, 4918 ms)
kod o'zgardi, qayta qurish: 5 s   (pip KESHDAN olindi)
bash5 qator
# talablar.txt ga o'zgarish kiritamiz -> COPY talablar.txt qatlami eskiradi
echo "# qayta qurish belgisi $(date +%s%N)" >> talablar.txt
t0=$SECONDS
docker build -q -t mlops-kitob:yaxshi . > /dev/null 2>&1
echo "talablar.txt o'zgardi: $((SECONDS - t0)) s   (pip QAYTA ishladi)"
Chiqish (qaytish kodi 0, 37622 ms)
talablar.txt o'zgardi: 37 s   (pip QAYTA ishladi)

Ikki raqamni solishtiring — farq o'nlab barobar.

Birinchi holatda faqat kod o'zgardi. Kod qatlami eng tepada, shuning uchun pip install keshdan olindi.

Ikkinchi holatda talablar.txt o'zgardi. U pastroqda turibdi — demak undan yuqoridagi hamma narsa, shu jumladan pip install, qaytadan bajarildi.

Endi hisoblang. Kod kuniga o'n marta o'zgaradi, bog'liqliklar esa oyiga bir marta.

Tartib to'g'ri bo'lsa: kuniga 10 × 3 s = 30 soniya.

Tartib teskari bo'lsa: kuniga 10 × 56 s = 9 daqiqa, har kuni, hech qanday sababsiz.

Yiliga bu 40 soatdan ko'p — bir ish haftasi, bitta qator tartibi tufayli.

Buni shunday eslang

"Eng kam o'zgaradigani pastga, eng ko'p o'zgaradigani tepaga."

Xuddi kiyinishdek: ichki kiyimni oxirida almashtirmaysiz.

Eng ko'p o'zgaradigan narsa eng oxirida

Dockerfile ni shunday tartibda yozing:

  1. Asosiy image (FROM) — deyarli o'zgarmaydi
  2. Tizim paketlari (apt-get) — kamdan-kam
  3. Bog'liqliklar (COPY talablar.txt + pip install) — oyiga bir marta
  4. Kod (COPY src/) — kuniga o'n marta

Teskari tartib har qurishda hamma narsani qayta bajaradi.

Konteynerni ishga tushiramiz

bash1 qator
docker run --rm mlops-kitob:yaxshi
Chiqish (qaytish kodi 0, 2942 ms)
tayyor: 1.7.2

Ichkaridagi muhitni tekshiramiz:

bash8 qator
docker run --rm mlops-kitob:yaxshi python -c "
import sys, sklearn, pandas, numpy, getpass, os
print('Python   :', sys.version.split()[0])
print('sklearn  :', sklearn.__version__)
print('pandas   :', pandas.__version__)
print('numpy    :', numpy.__version__)
print('foydalanuvchi:', os.getuid(), '(0 = root, xavfli)')
"
Chiqish (qaytish kodi 0, 1769 ms)
Python   : 3.11.16
sklearn  : 1.7.2
pandas   : 2.3.3
numpy    : 2.2.6
foydalanuvchi: 10001 (0 = root, xavfli)

uid 0 emas — konteyner oddiy foydalanuvchi sifatida ishlayapti. Bu muhim xavfsizlik qoidasi: konteyner buzilsa ham, hujumchi root huquqini olmaydi.

Model bilan ishlaydigan konteyner

Endi haqiqiy ish: modelni o'qitib, konteyner ichida ishlatamiz.

shu faylni yaratingsrc/oqitish/oddiy.py

python30 qator
"""Modelni o'qitib, faylga saqlaydi."""
import sys
from pathlib import Path

import joblib
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import f1_score

XUS = ["xonalar", "maydon", "qavat", "jami_qavat", "narx_mln", "tavsif_uzunligi",
       "rasmlar_soni", "telefon_takrorlanishi", "kunlik_elonlar", "egasi_tasdiqlangan"]


def oqit(malumot: str, chiqish: str) -> float:
    d = pd.read_csv(malumot)
    Xtr, Xte, ytr, yte = train_test_split(d[XUS], d["broker"], test_size=0.25,
                                          stratify=d["broker"], random_state=42)
    m = RandomForestClassifier(n_estimators=150, min_samples_leaf=3,
                               random_state=42, n_jobs=1).fit(Xtr, ytr)
    f1 = float(f1_score(yte, m.predict(Xte)))
    Path(chiqish).parent.mkdir(parents=True, exist_ok=True)
    joblib.dump({"model": m, "xususiyatlar": XUS, "f1": round(f1, 4)}, chiqish)
    return f1


if __name__ == "__main__":
    malumot = sys.argv[1] if len(sys.argv) > 1 else "datasets/ijara.csv"
    chiqish = sys.argv[2] if len(sys.argv) > 2 else "modellar/model.joblib"
    print(f"F1 = {oqit(malumot, chiqish):.4f}  ->  {chiqish}")

Modelni konteyner ichida o'qitamiz. Ma'lumot va natija papkasi tashqaridan ulanadi (--mount), shunda model konteyner o'chganda ham qoladi:

bash10 qator
sed -i 's/^joblib.*$//' talablar.txt 2>/dev/null
grep -q joblib talablar.txt || echo "joblib==1.5.3" >> talablar.txt
docker build -q -t mlops-kitob:yaxshi . > /dev/null 2>&1
mkdir -p modellar
docker run --rm \
  --mount type=bind,src="$(pwd)/../datasets",dst=/app/datasets,ro \
  --mount type=bind,src="$(pwd)/modellar",dst=/app/modellar \
  -w /app mlops-kitob:yaxshi \
  python src/oqitish/oddiy.py datasets/ijara.csv modellar/model.joblib
ls -la modellar/
Chiqish (qaytish kodi 0, 38640 ms)
F1 = 0.8603  ->  modellar/model.joblib
total 14580
drwxrwxrwx 1 abulqosim abulqosim      512 Sep 29 18:36 .
drwxrwxrwx 1 abulqosim abulqosim      512 Sep 29 18:36 ..
-rwxrwxrwx 1 abulqosim abulqosim 14928925 Sep 29 18:36 model.joblib

ro bayrog'iga e'tibor bering — ma'lumot faqat o'qish uchun ulandi. Konteyner uni o'zgartira olmaydi. Bu tasodifiy zararning oldini oladi.

Model faylini tekshiramiz — u konteynerdan tashqarida ham o'qiladi:

python8 qator
python3 -c "
import joblib
m = joblib.load('modellar/model.joblib')
print('model turi   :', type(m['model']).__name__)
print('daraxtlar    :', m['model'].n_estimators)
print('xususiyatlar :', len(m['xususiyatlar']))
print('F1 (o\'qitishda):', m['f1'])
"
Chiqish (qaytish kodi 0, 766 ms)
model turi   : RandomForestClassifier
daraxtlar    : 150
xususiyatlar : 10
F1 (o'qitishda): 0.8603

Tozalash

Kitob yaratgan image larni o'chiramiz — sizning boshqa konteyneringizga tegmaydi:

bash3 qator
docker rmi -f mlops-kitob:sodda > /dev/null 2>&1
docker images --format "{{.Repository}}:{{.Tag}}" | grep -c "^mlops-kitob" || true
echo "qolgan mlops-kitob image lari yuqorida"
Chiqish (qaytish kodi 0, 472 ms)
1
qolgan mlops-kitob image lari yuqorida

`docker system prune` ni o'ylamasdan ishlatmang

docker system prune -a hamma ishlatilmayotgan image, konteyner va tarmoqni o'chiradi — jumladan boshqa loyihalaringiznikini ham.

Faqat o'zingiznikini o'chiring: docker rmi <nom>:<tag>, yoki loyihaga yorliq (label) qo'yib, docker image prune --filter "label=loyiha=mlops-kitob" bilan filtrlang.

Docker nimani hal qilmaydi

python10 qator
import pandas as pd
show(pd.DataFrame([
    {"muammo": "Kutubxona versiyalari", "Docker hal qiladimi": "HA", "izoh": "Image ichida qulflangan"},
    {"muammo": "Tizim kutubxonalari (libgomp)", "Docker hal qiladimi": "HA", "izoh": "Image ga kiradi"},
    {"muammo": "Python versiyasi", "Docker hal qiladimi": "HA", "izoh": "FROM da belgilanadi"},
    {"muammo": "Ma'lumot versiyasi", "Docker hal qiladimi": "YO'Q", "izoh": "5-bob: manifest va ombor"},
    {"muammo": "Tasodifiy seed", "Docker hal qiladimi": "YO'Q", "izoh": "2-bob: kodda qulflanadi"},
    {"muammo": "CPU arxitekturasi (arm64/amd64)", "Docker hal qiladimi": "QISMAN", "izoh": "Har platformaga alohida image"},
    {"muammo": "GPU drayverlari", "Docker hal qiladimi": "QISMAN", "izoh": "nvidia-container-toolkit kerak"},
]))
muammoDocker hal qiladimiizoh
Kutubxona versiyalariHAImage ichida qulflangan
Tizim kutubxonalari (libgomp)HAImage ga kiradi
Python versiyasiHAFROM da belgilanadi
Ma'lumot versiyasiYO'Q5-bob: manifest va ombor
Tasodifiy seedYO'Q2-bob: kodda qulflanadi
CPU arxitekturasi (arm64/amd64)QISMANHar platformaga alohida image
GPU drayverlariQISMANnvidia-container-toolkit kerak

Docker — takrorlanishning bir qismi, hammasi emas. Uni 2-bobdagi seed va 5-bobdagi ma'lumot versiyalash bilan birga ishlatish kerak.

Xulosa

Tushuncha Asosiy fikr
Nima uchun Muhitni ham artefaktga aylantiradi — 2-darajali takrorlanish
-slim Asosiy image ni sezilarli kichraytiradi
Ko'p bosqichli Qurish artefaktlari yakuniy image ga tushmaydi
.dockerignore Ma'lumot va .venv umuman nusxalanmaydi
Qatlam tartibi Bog'liqliklar oldin, kod oxirida — kesh shunda ishlaydi
USER root emas; konteyner buzilsa zarar cheklanadi
--mount ...,ro Ma'lumotni faqat o'qish uchun ulang
prune xavfi -a boshqa loyihalaringizni ham o'chiradi
Chegara Ma'lumot va seed ni Docker qulflamaydi

Talaba ishi

  1. Dockerfile dagi COPY talablar.txt va COPY src/ qatorlarining o'rnini almashtiring. Kodni o'zgartirib qayta quring — qurish vaqti qanday o'zgardi?
  2. .dockerignore dan *.csv qatorini olib tashlang va image hajmini qayta o'lchang. Qancha oshdi?
  3. USER ishchi qatorini olib tashlang va os.getuid() ni qayta tekshiring. Bu nega xavfli?
  4. python:3.11-slim o'rniga python:3.11-alpine ni sinab ko'ring. Kichikroqmi? numpy o'rnatilishi bilan nima bo'ladi? (Maslahat: musl va glibc.)

Bu bobni bepul o'qidingiz.

Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.

Telegramda yozish Narxlar