Serverga chiqarish: Compose, qayta ishga tushirish va teskari proksi
"Serverda ishlamayapti"
Rasm tayyor (28-bob), tezligi o'lchangan (29-bob). Endi uni serverga chiqarish kerak.
Birinchi urinish odatda shunday bo'ladi: SSH bilan serverga kirasiz va qo'lda yozasiz:
docker run -d -p 8000:8000 mlops-servis:v1
Ishlaydi. Xursandsiz.
Uch kun o'tadi. Server qayta yuklanadi (yangilanish, elektr, nima bo'lsa ham). Ertalab sayt ishlamayapti.
Nega? Chunki docker run bir martalik buyruq. Server o'chsa, konteyner o'chadi va
hech kim uni qaytadan ko'tarmaydi.
Ikkinchi muammo ham bor: o'sha buyruq faqat sizning boshingizda. Qaysi portlar, qaysi o'zgaruvchilar, qaysi hajmlar edi? Uch oydan keyin eslay olmaysiz.
Buyruq emas, ta'rif
Yechim — buyruqni faylga aylantirish.
Bu 2-bobdagi retsept g'oyasining aynan o'zi: boshda saqlangan bilim yo'qoladi, yozilgani qoladi.
docker compose aynan shu ishni qiladi: siz nima kerakligini yozasiz, u esa buyruqni
o'zi tuzadi.
shu faylni yaratingcompose.yaml
services:
servis:
image: mlops-servis:v1
container_name: mlops-servis
restart: unless-stopped # <-- eng muhim qator
ports:
- "18110:8000"
environment:
MODEL_YOLI: /app/model.onnx
BROKER_CHEGARASI: "0.5"
healthcheck:
test: ["CMD", "python3", "-c",
"import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/healthz')"]
interval: 15s
timeout: 3s
retries: 3
start_period: 10s
deploy:
resources:
limits:
memory: 512M
To'rtta qator alohida e'tiborga loyiq.
restart: unless-stopped — yuqoridagi muammoning yechimi. Server qayta yuklansa,
Docker konteynerni o'zi ko'taradi. "unless-stopped" degani: siz o'zingiz to'xtatgan
bo'lsangiz, ko'tarmaydi.
healthcheck — 28-bobdagi tekshiruv, endi Compose darajasida.
memory: 512M — chegara. Konteyner buzilib xotirani yeb ketsa, u o'zi o'ladi va
qayta ko'tariladi. Chegara bo'lmasa, u butun serverni yiqitadi — shu jumladan boshqa
xizmatlarni.
environment — 24-bobdagi sozlama qoidasi: qiymatlar kodda emas, tashqarida.
`docker kill` qayta ishga tushirishni **ishga solmaydi**
Men bu demoni avval docker kill bilan yozdim va u ishlamadi: konteyner o'ldi va
o'sha yerda qoldi, RestartCount 0 bo'lib turdi.
Sabab mantiqiy: Docker uchun docker kill va docker stop — bu sizning qaroringiz.
Siz konteynerni to'xtatdingiz, demak u to'xtab tursin. Siz ta'mirlayotgan bo'lishingiz
mumkin.
Qayta ishga tushirish siyosati faqat o'z-o'zidan o'lgan konteynerga tegishli: dastur qulasa, xotira tugasa, jarayon xato bilan chiqsa.
Shuning uchun yuqoridagi kodda docker exec ... kill 1 ishlatiladi — u konteyner
ichidagi asosiy jarayonni o'ldiradi, ya'ni haqiqiy qulashga o'xshaydi.
Sinovda buni bilmaslik chalg'itadi: "siyosat yozdim, lekin ishlamayapti" deb o'ylaysiz.
`restart: always` va `unless-stopped` farqi
Ikkalasi ham konteynerni qayta ko'taradi, lekin bitta muhim farq bor.
always — siz docker stop qilsangiz ham, Docker demoni qayta ishga tushganda konteyner
yana ko'tariladi. Ta'mirlash paytida bu juda bezovta qiladi.
unless-stopped — siz o'zingiz to'xtatgan bo'lsangiz, tinch qoldiradi.
Ishlab chiqarish uchun deyarli har doim unless-stopped.
Ko'taramiz
python3 -c " import sys, pathlib; sys.path.insert(0, 'src') if not pathlib.Path('model.onnx').exists(): import pandas as pd from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from quvur.xususiyat import XUSUSIYATLAR, tayyorla from quvur.format import onnx_ga d = pd.read_csv('../datasets/ijara.csv') x, y = tayyorla(d), d['broker'] Xtr, _, ytr, _ = train_test_split(x, y, test_size=0.25, stratify=y, random_state=42) m = RandomForestClassifier(n_estimators=150, min_samples_leaf=3, random_state=42, n_jobs=1).fit(Xtr, ytr) onnx_ga(m, len(XUSUSIYATLAR), 'model.onnx') print('model tayyor') " docker images mlops-servis:v1 -q | grep -q . || docker build -q -f Dockerfile.servis -t mlops-servis:v1 . > /dev/null 2>&1 docker compose -f compose.yaml down > /dev/null 2>&1 docker compose -f compose.yaml up -d > /dev/null 2>&1 for i in $(seq 1 40); do [ "$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:18110/healthz 2>/dev/null)" = "200" ] && { echo "servis ${i} urinishda tayyor"; break; } sleep 0.5 done docker compose -f compose.yaml ps --format '{{.Name}} {{.Status}}' 2>/dev/null
model tayyor servis 3 urinishda tayyor mlops-servis Up 1 second (health: starting)
Diqqat: buyruq docker run ... uzun qatori emas, shunchaki docker compose up -d.
Qolgan hamma narsa faylda yozilgan va u git da saqlanadi.
Qayta ishga tushirishni sinaymiz
Aytilgan gap yetarli emas — tekshiramiz. Konteyner ichidagi jarayonni ataylab o'ldiramiz, ya'ni qulashni taqlid qilamiz.
echo "qulashdan oldin:" docker inspect mlops-servis --format ' holat: {{.State.Status}} qayta ishga tushish: {{.RestartCount}}' docker exec mlops-servis kill 1 > /dev/null 2>&1 sleep 8 echo "qulagandan keyin:" docker inspect mlops-servis --format ' holat: {{.State.Status}} qayta ishga tushish: {{.RestartCount}}' for i in $(seq 1 30); do curl -s http://127.0.0.1:18110/healthz > /dev/null 2>&1 && break; sleep 1; done echo -n " servis javob beryaptimi: "; curl -s http://127.0.0.1:18110/healthz echo ""
qulashdan oldin:
holat: running qayta ishga tushish: 0
qulagandan keyin:
holat: running qayta ishga tushish: 0
servis javob beryaptimi: {"holat":"tirik"}
Natijaga qarang: konteyner o'lmadi ham. RestartCount 0 bo'lib qoldi, holat running.
Sabab qiziq va uni bilish kerak: konteyner ichida PID 1 signallarni e'tiborsiz
qoldiradi. Bu Linux yadrosining qoidasi — 1-raqamli jarayon maxsus himoyalangan, hatto
kill -9 ham unga ta'sir qilmaydi.
Ya'ni qulashni ichkaridan taqlid qilib bo'lmaydi.
Demak qayta ishga tushirish siyosatini sinash uchun haqiqiy sabab kerak:
| Sabab | Siyosat ishlaydimi |
|---|---|
| Server qayta yuklandi | Ha |
| Docker demoni qayta ishga tushdi | Ha |
| Dastur ichida istisno → jarayon chiqdi | Ha |
| Xotira chegarasidan oshdi (OOM) | Ha |
docker stop / docker kill |
Yo'q — bu sizning qaroringiz |
Konteyner ichidan kill 1 |
Yo'q — PID 1 himoyalangan |
RestartCount raqami esa keyinchalik muhim bo'ladi: agar u tez o'sib borsa, demak
konteyner qayta-qayta yiqilmoqda va sabab ichkarida. 34-bobdagi monitoring aynan
shunga qaraydi.
Oldiga proksi qo'yamiz
Hozir servis 18110-portda turibdi. Haqiqiy saytda esa foydalanuvchi 80/443 portga
keladi va https://model.rostuy.uz deb yozadi.
O'rtada teskari proksi (reverse proxy) turadi. U:
- 443-portda turadi va TLS ni hal qiladi;
- so'rovni ichkaridagi servisga uzatadi;
- bir nechta servisni bitta domen ostida birlashtiradi;
- so'rov sonini cheklaydi (rate limit).
shu faylni yaratingnginx.conf
events {}
http {
# 29-bobdagi yuk sinovi: cheklovsiz server navbatga to'ladi
limit_req_zone $binary_remote_addr zone=bashorat:10m rate=30r/s;
upstream model {
server servis:8000;
}
server {
listen 80;
# sog'liq tekshiruvi - cheklovsiz
location /healthz {
proxy_pass http://model;
}
location / {
limit_req zone=bashorat burst=20 nodelay;
limit_req_status 429;
proxy_pass http://model;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 2s;
proxy_read_timeout 10s;
}
}
}
shu faylni yaratingcompose-proksi.yaml
services:
servis:
image: mlops-servis:v1
restart: unless-stopped
environment:
MODEL_YOLI: /app/model.onnx
expose:
- "8000" # faqat ichki tarmoqqa
proksi:
image: nginx:1.27-alpine
container_name: mlops-proksi
restart: unless-stopped
ports:
- "18120:80" # tashqariga FAQAT proksi chiqadi
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- servis
Eng muhim o'zgarishni sezdingizmi? Servisda endi ports yo'q, faqat expose bor.
Ya'ni servis tashqaridan umuman ko'rinmaydi. Unga faqat proksi orqali kirish mumkin. Bu xavfsizlikning asosiy qoidasi: tashqariga faqat kerakli narsa chiqsin.
docker compose -f compose.yaml down > /dev/null 2>&1 docker compose -f compose-proksi.yaml up -d > /dev/null 2>&1 for i in $(seq 1 40); do [ "$(curl -s -o /dev/null -w %{http_code} http://127.0.0.1:18120/healthz 2>/dev/null)" = "200" ] && { echo "proksi ${i} urinishda tayyor"; break; } sleep 0.5 done echo -n "proksi orqali healthz : "; curl -s http://127.0.0.1:18120/healthz echo "" echo -n "proksi orqali bashorat: " curl -s -X POST http://127.0.0.1:18120/bashorat -H "Content-Type: application/json" -d '{ "xonalar": 2, "maydon": 55, "qavat": 3, "jami_qavat": 9, "narx_mln": 4.2, "tavsif_uzunligi": 60, "rasmlar_soni": 2, "telefon_takrorlanishi": 18, "kunlik_elonlar": 9, "egasi_tasdiqlangan": 0}' echo "" echo -n "servisga TO'G'RIDAN kirish: " curl -s -m 3 -o /dev/null -w "HTTP %{http_code}\n" http://127.0.0.1:18110/healthz || echo "ULANIB BO'LMADI (kutilgani)"
proksi 2 urinishda tayyor
proksi orqali healthz : {"holat":"tirik"}
proksi orqali bashorat: {"broker_ehtimoli":0.9179,"broker":true,"chegara":0.5,"ms":3.2}
servisga TO'G'RIDAN kirish: HTTP 000
ULANIB BO'LMADI (kutilgani)
Oxirgi qatorga qarang: 18110-port endi javob bermaydi. Servis faqat proksi orqali ishlaydi.
Cheklov ishlaydimi
29-bobda ko'rdik: yuk oshsa navbat paydo bo'ladi va p99 qulaydi. Proksidagi limit_req
shuning oldini oladi — u ortiqcha so'rovni serverga qo'ymaydi.
python3 -c " import urllib.request, urllib.error, json from concurrent.futures import ThreadPoolExecutor import collections ELON = {'xonalar': 2, 'maydon': 55, 'qavat': 3, 'jami_qavat': 9, 'narx_mln': 4.5, 'tavsif_uzunligi': 300, 'rasmlar_soni': 8, 'telefon_takrorlanishi': 4, 'kunlik_elonlar': 2, 'egasi_tasdiqlangan': 0} xom = json.dumps(ELON).encode() def sorov(_): r = urllib.request.Request('http://127.0.0.1:18120/bashorat', data=xom, headers={'Content-Type': 'application/json'}) try: with urllib.request.urlopen(r, timeout=10) as j: j.read(); return 200 except urllib.error.HTTPError as e: return e.code except Exception: return 0 with ThreadPoolExecutor(max_workers=60) as ex: kodlar = collections.Counter(ex.map(sorov, range(400))) print('400 ta tez sorov natijasi:') for k, n in sorted(kodlar.items()): nom = {200: 'OK', 429: 'juda kop sorov (cheklov ishladi)', 0: 'ulanmadi'}.get(k, 'boshqa') print(f' HTTP {k}: {n:>4} {nom}') "
400 ta tez sorov natijasi: HTTP 200: 25 OK HTTP 429: 375 juda kop sorov (cheklov ishladi)
429 chiqdi — demak cheklov ishlayapti.
Bu yaxshi xabar: server yiqilmadi. U ortiqcha so'rovga "hozir emas" dedi va o'z ishini davom ettirdi.
Cheklovsiz servis — o'zini o'zi yiqitadi
29-bobdagi jadvalni eslang: 50 parallel so'rovda p99 120 ms ga chiqdi. 500 parallel bo'lsa nima bo'ladi?
Server "yo'q" demaydi — u hammasini qabul qiladi va hammasi navbatda turadi. Xotira to'ladi, javob vaqti o'sadi, oxirida hamma so'rov timeout bo'ladi.
Ya'ni hech kim xizmat olmaydi. 90% so'rovni rad etib, 10% ga to'g'ri xizmat qilish ancha yaxshi.
Bu limit_req ning butun ma'nosi: yomon kunda qisman ishlash, umuman ishlamaslikdan
afzal.
Haqiqiy serverda nima o'zgaradi
Bu bobda hammasi mahalliy ishladi. Haqiqiy VPS da farqlar quyidagicha:
import pandas as pd show(pd.DataFrame([ {"Narsa": "Port", "Bu yerda": "18120", "Haqiqiy serverda": "80 va 443"}, {"Narsa": "TLS", "Bu yerda": "Yo'q", "Haqiqiy serverda": "Let's Encrypt (certbot)"}, {"Narsa": "Domen", "Bu yerda": "127.0.0.1", "Haqiqiy serverda": "model.domeningiz.uz"}, {"Narsa": "Rasm", "Bu yerda": "Mahalliy qurilgan", "Haqiqiy serverda": "Registry dan (31-bob)"}, {"Narsa": "Yangilash", "Bu yerda": "Qo'lda", "Haqiqiy serverda": "CI/CD (31, 32-bob)"}, {"Narsa": "Kirish", "Bu yerda": "Ochiq", "Haqiqiy serverda": "Faqat kalit bilan SSH"}, ]))
| Narsa | Bu yerda | Haqiqiy serverda |
|---|---|---|
| Port | 18120 | 80 va 443 |
| TLS | Yo'q | Let's Encrypt (certbot) |
| Domen | 127.0.0.1 | model.domeningiz.uz |
| Rasm | Mahalliy qurilgan | Registry dan (31-bob) |
| Yangilash | Qo'lda | CI/CD (31, 32-bob) |
| Kirish | Ochiq | Faqat kalit bilan SSH |
Serverni himoyalashning eng kam to'plami
Model servisi chiqarishdan oldin:
- SSH parol bilan emas, kalit bilan.
PasswordAuthentication no. - Faqat 22, 80, 443 ochiq. Qolgani
ufw deny. Ayniqsa ma'lumotlar bazasi porti hech qachon tashqariga chiqmasin. - Konteyner root emas (28-bob).
- Xotira chegarasi — bitta xizmat butun serverni yiqitmasin.
- Avtomatik xavfsizlik yangilanishlari yoqilgan.
Beshtasi ham bir martalik ish, lekin ular eng ko'p uchraydigan hujumlarni to'sadi.
Tozalash
docker compose -f compose-proksi.yaml down > /dev/null 2>&1 docker compose -f compose.yaml down > /dev/null 2>&1 echo "compose xizmatlari to'xtatildi" docker ps --format '{{.Names}}' | grep -v k3d
compose xizmatlari to'xtatildi novavivo-postgres novavivo-redis stratix-med-frontend-1
Xulosa
| Tushuncha | Asosiy fikr |
|---|---|
docker run muammosi |
Bir martalik buyruq; server o'chsa qaytmaydi |
| Compose fayli | Buyruq emas — ta'rif, git da saqlanadi |
unless-stopped |
Qayta ko'taradi, lekin siz to'xtatsangiz tinch qoldiradi |
| Xotira chegarasi | Bitta xizmat butun serverni yiqitmasin |
RestartCount |
Tez o'ssa — konteyner qayta-qayta yiqilmoqda |
| Teskari proksi | TLS, bir nechta xizmat, cheklov bitta joyda |
expose va ports |
Servis tashqariga umuman chiqmasin |
limit_req |
Yomon kunda qisman ishlash — umuman ishlamaslikdan afzal |
429 |
Rad etish — yiqilishdan yaxshiroq |
Talaba ishi
restart: unless-stoppedni olib tashlang va konteynerni o'ldiring. Nima bo'ldi?- Xotira chegarasini
50Mqiling. Servis ko'tariladimi?docker inspectdaOOMKillednima deydi? rate=30r/sni5r/sqiling va sinovni qayta o'tkazing. Nechta429chiqdi?- Proksiga ikkinchi servis qo'shing (masalan
/v2/yo'lida) va ikkalasini bitta port ostida ishlating. proxy_read_timeoutni1sqiling va to'plamli so'rov yuboring. Qanday xato chiqadi va u kimning aybi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.