Docker bilan o'rnatish: bitta buyruq bilan butun tizim
Bu bobning kodi: kod/21_docker.py — kitob papkasidan ishga tushiring: python kod/21_docker.py
Nega Docker Compose
20-bobda tizim ishladi, lakin uni ishga tushirish uchun olti xil jarayonni to'g'ri tartibda, to'g'ri muhit o'zgaruvchilari bilan ko'tarish kerak edi. Mijozning serverida buni qo'lda qilish — xato manbai: kimdir kutubxonani yangilaydi, kimdir parolni noto'g'ri yozadi, server qayta yuklanganda nimadir ishga tushmay qoladi. Docker Compose hammasini bitta faylda tasvirlaydi: qaysi xizmatlar, qaysi obrazdan, qanday sozlamalar bilan, qaysi tartibda, qaysi portlar ochiq. Keyin — bitta buyruq: docker compose up -d.
Tizim tarkibi:
internet / ofis tarmog'i
│ faqat 443 (HTTPS)
┌────┴────┐
│ nginx │ TLS, statik fayllar, so'rov chegaralari, xavfsizlik sarlavhalari
└────┬────┘
│ ichki tarmoq (tashqaridan ko'rinmaydi)
┌─────────────────┼───────────────────────────────┐
│ web (Django + gunicorn) signal (signal_yubor) │
│ db (Postgres, doimiy volume) │
│ mediamtx (RTSP) ◄── kameralar │
│ tahlilchi (GPU: YOLO + ByteTrack) │
└──────────────────────────────────────────────────┘
Tashqariga faqat nginx ochiladi. Baza, Django, ishchi — faqat Docker ning ichki tarmog'ida: ular bir-birini xizmat nomi bilan topadi (db, web, mediamtx), tashqaridan esa umuman ko'rinmaydi.
Obrazlar
Docker obrazni qurish kontekstidan yig'adi — papkadagi fayllar Docker ga yuboriladi. Bizning kitob papkasida gigabaytlab dataset bor, ular obrazga kerak emas. .dockerignore "hammasini chiqar, faqat kerakligini qo'sh" tarzida yoziladi — yangi katta papka paydo bo'lsa ham u tasodifan obrazga tushmaydi, sirlar (.env) ham.
# Docker ga faqat obrazlarga kerakli fayllar yuboriladi (kitob papkasida gigabaytlab dataset bor) * !loyiha/ !kod/oqim.py !kod/19_soxta_kamera.py !kod/20_tahlilchi.py !kod/20_soxta_telegram.py !weights/yolo11n.pt **/__pycache__
web obrazi — rasmiy Python obrazi ustida Django, psycopg va gunicorn (Python uchun ishlab chiqarish serveri; runserver emas). Versiyalar qat'iy — bugun va bir yildan keyin qurilgan obraz bir xil bo'lsin. Dastur root emas, alohida foydalanuvchi nomidan ishlaydi: kimdir Django orqali konteynerga kirib olsa ham, uning huquqlari cheklangan.
FROM python:3.12.13-slim ENV PYTHONDONTWRITEBYTECODE=1 PYTHONUNBUFFERED=1 RUN pip install --no-cache-dir "Django==5.2.17" "psycopg[binary]==3.3.3" "gunicorn==26.2.0" RUN useradd --create-home --uid 10001 ilova && mkdir -p /app/static && chown ilova /app/static WORKDIR /app COPY loyiha/ /app/ USER ilova EXPOSE 8000
tahlilchi obrazi — Ultralytics ning rasmiy GPU obrazi ustiga quriladi: unda PyTorch, CUDA kutubxonalari va aynan bizning versiya (8.4.17) bor. O'zimiz qo'shadigan narsalar: ByteTrack uchun lap (aks holda ultralytics uni ish paytida internetdan o'rnatishga urinadi), bizning kod va model. Obraz katta (bir necha GB), lekin u bir marta yuklab olinadi.
FROM ultralytics/ultralytics:8.4.17 RUN pip install --no-cache-dir "lap==0.5.13" RUN useradd --create-home --uid 10001 ilova WORKDIR /app COPY --chown=ilova kod/oqim.py kod/19_soxta_kamera.py kod/20_tahlilchi.py /app/kod/ COPY --chown=ilova weights/yolo11n.pt /app/weights/ USER ilova
nginx: tashqi eshik
nginx HTTPS ni qabul qiladi va so'rovni Django ga uzatadi. U yerda yana to'rtta himoya qatlami:
- faqat HTTPS: 80-portga kelgan so'rov 443 ga yo'naltiriladi; TLS 1.2 va 1.3;
- so'rovlar chegarasi IP bo'yicha: panelga soniyasiga 5 ta (parolni taxmin qilishni sekinlashtiradi), API ga 20 ta; oshsa — 429;
- so'rov hajmi — 64 KB dan katta so'rov Django ga yetmasdan 413 bilan qaytadi;
- sarlavhalar:
Strict-Transport-Security,X-Content-Type-Options,X-Frame-Options, nginx versiyasi yashirilgan (server_tokens off).
Statik fayllarni (panelning CSS i) Django emas, nginx beradi — tezroq va Django ni bo'shatadi.
worker_processes auto;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
server_tokens off;
access_log /dev/stdout;
error_log /dev/stderr warn;
limit_req_zone $binary_remote_addr zone=panel:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=api:10m rate=20r/s;
limit_req_status 429;
upstream django { server web:8000; }
server {
listen 80;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
ssl_protocols TLSv1.2 TLSv1.3;
client_max_body_size 64k;
add_header Strict-Transport-Security "max-age=31536000" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy same-origin always;
location /static/ { alias /srv/static/; expires 7d; }
location /api/ {
limit_req zone=api burst=40 nodelay;
proxy_pass http://django;
include /etc/nginx/proxy.conf;
}
location / {
limit_req zone=panel burst=20 nodelay;
proxy_pass http://django;
include /etc/nginx/proxy.conf;
}
}
}
proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto https; proxy_read_timeout 30s; proxy_hide_header Strict-Transport-Security; # bu sarlavhalarni Django ham qo'yadi — ikki marta yuborilmasin proxy_hide_header X-Content-Type-Options; proxy_hide_header X-Frame-Options; proxy_hide_header Referrer-Policy;
Django ning xavfsizlik middleware i ham shu sarlavhalarni qo'yadi — proxy_hide_header bilan ularni olib tashlaymiz, aks holda javobda har biri ikki marta bo'lardi. Endi sarlavhalarning yagona manbai — nginx, statik fayllar uchun ham.
MediaMTX: parolsiz sozlama fayli
19-bobda MediaMTX sozlamasi har safar parollar bilan yaratilardi. Bu yerda yaxshiroq yo'l: sozlama fayli parolsiz (uni Git ga qo'shsa bo'ladi), parollar muhit o'zgaruvchisidan keladi — MediaMTX istalgan sozlamani MTX_ bilan boshlanadigan o'zgaruvchi orqali almashtirishga imkon beradi (MTX_AUTHINTERNALUSERS_0_PASS — ro'yxatdagi birinchi foydalanuvchining paroli). Buni oldin alohida tekshirdik: o'zgaruvchidagi parol qabul qilinadi, fayldagi "ALMASHTIRILADI" rad etiladi.
logLevel: warn rtsp: true rtspTransports: [tcp] rtspAddress: :8554 rtmp: false hls: false webrtc: false srt: false moq: false api: false metrics: false pprof: false playback: false authInternalUsers: - user: kamera # parol: MTX_AUTHINTERNALUSERS_0_PASS pass: ALMASHTIRILADI permissions: - action: publish - user: tahlil # parol: MTX_AUTHINTERNALUSERS_1_PASS pass: ALMASHTIRILADI permissions: - action: read paths: all_others:
docker-compose.yml
Butun tizim — bitta fayl. E'tibor beradigan joylar:
- Har xizmatga faqat o'z sirlari. Parollar
.envfaylidan olinadi, lekin har bir xizmatga faqat keragi uzatiladi: nginx hech qanday parolni bilmaydi, tahlilchi baza parolini bilmaydi.${PG_PAROL:?...}— o'zgaruvchi bo'lmasa, Compose ishga tushmaydi. - Tartib va salomatlik.
depends_on+healthcheck: web baza tayyor bo'lgandagina, nginx web tayyor bo'lgandagina ishga tushadi. restart: unless-stopped— jarayon yiqilsa yoki server qayta yuklansa, xizmat o'zi turadi.- Cheklovlar:
read_only(fayl tizimiga yozib bo'lmaydi),no-new-privileges, jurnal hajmi (har xizmatga 3 × 10 MB — disk to'lib qolmasin). - Portlar faqat nginx va kameralar kiradigan RTSP da, va faqat
127.0.0.1da (sinov uchun; haqiqiy serverda nginx0.0.0.0:443da, qolganini firewall yopadi). - GPU:
deploy.resources.reservations.devices— konteynerga NVIDIA GPU beriladi. - Soxta Telegram
sinovprofilida: u faqat--profile sinovbilan ishga tushadi; haqiqiy o'rnatishda bu xizmat vaTELEGRAM_APIqatorlari olib tashlanadi.
name: yolo-kitob x-umumiy: &umumiy restart: unless-stopped security_opt: ["no-new-privileges:true"] logging: driver: json-file options: {max-size: "10m", max-file: "3"} services: db: <<: *umumiy image: postgres:16.13-alpine environment: POSTGRES_USER: kitob POSTGRES_DB: kuzatuv POSTGRES_PASSWORD: ${PG_PAROL:?.env da PG_PAROL yo'q} volumes: [pgdata:/var/lib/postgresql/data] healthcheck: test: ["CMD-SHELL", "pg_isready -U kitob -d kuzatuv"] interval: 3s retries: 20 web: <<: *umumiy build: {context: .., dockerfile: deploy/web.Dockerfile} image: yolo-kitob-web:1 environment: DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?} DJANGO_HTTPS: "1" DJANGO_ALLOWED_HOSTS: "127.0.0.1,localhost,web" PG_HOST: db PG_PAROL: ${PG_PAROL:?} ISHCHI_TOKENLAR: ${ISHCHI_TOKEN:?} command: >- sh -c "python manage.py migrate --noinput && python manage.py collectstatic --noinput -v 0 && exec gunicorn kuzatuv.wsgi --bind 0.0.0.0:8000 --workers 3 --worker-tmp-dir /dev/shm --access-logfile -" volumes: [static:/app/static] read_only: true tmpfs: [/tmp] depends_on: {db: {condition: service_healthy}} healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/api/salomatlik/', timeout=3)"] interval: 3s retries: 30 signal: <<: *umumiy image: yolo-kitob-web:1 environment: DJANGO_SECRET_KEY: ${DJANGO_SECRET_KEY:?} PG_HOST: db PG_PAROL: ${PG_PAROL:?} TELEGRAM_TOKEN: ${TELEGRAM_TOKEN:?} TELEGRAM_CHAT: ${TELEGRAM_CHAT:?} TELEGRAM_API: http://telegram:18081 # sinov; haqiqiy botda bu qatorni olib tashlang command: python manage.py signal_yubor read_only: true depends_on: {web: {condition: service_healthy}} telegram: <<: *umumiy profiles: [sinov] image: yolo-kitob-web:1 environment: TELEGRAM_TOKEN: ${TELEGRAM_TOKEN:?} command: python /kod/20_soxta_telegram.py --manzil 0.0.0.0 --jurnal /tmp/xabarlar.jsonl volumes: ["../kod/20_soxta_telegram.py:/kod/20_soxta_telegram.py:ro"] read_only: true tmpfs: [/tmp] nginx: <<: *umumiy image: nginx:1.29.5-alpine ports: ["127.0.0.1:18443:443", "127.0.0.1:18088:80"] volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./proxy.conf:/etc/nginx/proxy.conf:ro - ./certs:/etc/nginx/certs:ro - static:/srv/static:ro depends_on: {web: {condition: service_healthy}} mediamtx: <<: *umumiy image: bluenviron/mediamtx:1.21.1 volumes: ["./mediamtx.yml:/mediamtx.yml:ro"] environment: MTX_AUTHINTERNALUSERS_0_PASS: ${KAMERA_PAROL:?} MTX_AUTHINTERNALUSERS_1_PASS: ${TAHLIL_PAROL:?} ports: ["127.0.0.1:18554:8554"] tahlilchi: <<: *umumiy build: {context: .., dockerfile: deploy/tahlilchi.Dockerfile} image: yolo-kitob-tahlilchi:1 environment: RTSP_ASOS: rtsp://tahlil:${TAHLIL_PAROL:?}@mediamtx:8554/ ISHCHI_TOKEN: ${ISHCHI_TOKEN:?} command: python kod/20_tahlilchi.py --kameralar k1,k2 --api http://web:8000/api/hodisalar/ --model weights/yolo11n.pt --vaqt-kodi depends_on: {web: {condition: service_healthy}, mediamtx: {condition: service_started}} deploy: resources: reservations: devices: [{driver: nvidia, count: all, capabilities: [gpu]}] volumes: pgdata: static:
.env fayli Git ga hech qachon qo'shilmaydi. Loyihada uning o'rniga qiymatsiz namuna turadi:
# deploy/.env — faqat serverda, faqat egasi o'qiy oladi (chmod 600). Git ga qo'shilmaydi. # Har bir qiymat — uzun tasodifiy satr: python -c "import secrets; print(secrets.token_urlsafe(32))" DJANGO_SECRET_KEY= PG_PAROL= ISHCHI_TOKEN= KAMERA_PAROL= TAHLIL_PAROL= TELEGRAM_TOKEN= TELEGRAM_CHAT=
Qurish va ishga tushirish
Sirlarni yaratamiz (tasodifiy, hech qayerga chiqarilmaydi) va nginx uchun sertifikat. Bu yerda — o'zimiz imzolagan sertifikat (sinov uchun; brauzer ogohlantiradi). Haqiqiy domen bo'lsa — Let's Encrypt, pastda.
import json import os import re import secrets import signal import subprocess import sys import time import urllib.error import urllib.request from pathlib import Path import pandas as pd import psycopg sys.path.insert(0, "kod") from tizim import jarayon, postgres ILDIZ = Path.cwd() # kitob papkasining mutlaq yo'li DEPLOY = Path("deploy") DC = ["docker", "compose", "-f", str(ILDIZ / DEPLOY / "docker-compose.yml"), "--profile", "sinov"] def dc(*buyruq, **kw): try: return subprocess.run([*DC, *buyruq], capture_output=True, text=True, cwd=ILDIZ, **kw) finally: os.chdir(ILDIZ) # WSL eslatmasiga qarang (pastda) sirlar = {k: secrets.token_urlsafe(32) for k in ("DJANGO_SECRET_KEY", "PG_PAROL", "ISHCHI_TOKEN", "KAMERA_PAROL", "TAHLIL_PAROL")} sirlar["TELEGRAM_TOKEN"] = f"{secrets.randbelow(10 ** 9)}:{secrets.token_urlsafe(24)}" sirlar["TELEGRAM_CHAT"] = "-1001234567890" env_fayl = DEPLOY / ".env" env_fayl.write_text("".join(f"{k}={v}\n" for k, v in sirlar.items())) env_fayl.chmod(0o600) sertifikat = DEPLOY / "certs" sertifikat.mkdir(exist_ok=True) subprocess.run(["openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes", "-days", "30", "-subj", "/CN=localhost", "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", "-keyout", str(sertifikat / "server.key"), "-out", str(sertifikat / "server.crt")], check=True, capture_output=True) print("sirlar:", sorted(sirlar), "| sertifikat:", sorted(p.name for p in sertifikat.iterdir())) t0 = time.time() r = dc("build") print(f"docker compose build: {time.time() - t0:.0f} s, xato: {r.returncode != 0}") obrazlar = subprocess.run(["docker", "images", "--format", "{{.Repository}}:{{.Tag}}\t{{.Size}}"], capture_output=True, text=True).stdout show(pd.DataFrame([q.split("\t") for q in obrazlar.splitlines() if re.match(r"(yolo-kitob|postgres:16.13|nginx:1.29.5|bluenviron|ultralytics/ultralytics:8.4.17)", q)], columns=["obraz", "hajmi"]))
sirlar: ['DJANGO_SECRET_KEY', 'ISHCHI_TOKEN', 'KAMERA_PAROL', 'PG_PAROL', 'TAHLIL_PAROL', 'TELEGRAM_CHAT', 'TELEGRAM_TOKEN'] | sertifikat: ['server.crt', 'server.key'] docker compose build: 2 s, xato: False
| obraz | hajmi |
|---|---|
| yolo-kitob-web:1 | 269MB |
| yolo-kitob-tahlilchi:1 | 14.1GB |
| bluenviron/mediamtx:1.21.1 | 83.5MB |
| postgres:16.13-alpine | 395MB |
| nginx:1.29.5-alpine | 93.4MB |
| ultralytics/ultralytics:8.4.17 | 14.1GB |
Bu yerda build bir-ikki soniya oldi — obrazlar oldingi qurishdan keshda qolgan. Birinchi qurish ancha uzoq: tahlilchi uchun 14 GB li asos obraz yuklab olinadi (bizning ulanishda ~10 daqiqa), web uchun Python kutubxonalari o'rnatiladi. Tahlilchi obrazining hajmi deyarli butunlay asos obrazdan: PyTorch va CUDA kutubxonalari.
Endi — up -d. Birinchi marta Compose qolgan obrazlarni (Postgres, nginx, MediaMTX) yuklab oladi. Hamma xizmat "healthy" yoki "running" bo'lguncha kutamiz.
t0 = time.time() dc("up", "-d") while True: holat = [json.loads(q) for q in dc("ps", "--format", "json").stdout.splitlines()] tayyor = all(h["State"] == "running" and h.get("Health", "") in ("", "healthy") for h in holat) if tayyor or time.time() - t0 > 180: break time.sleep(1) print(f"up -d dan hamma xizmat tayyor bo'lguncha: {time.time() - t0:.0f} s") show(pd.DataFrame([{"xizmat": h["Service"], "holat": h["State"], "salomatlik": h.get("Health") or "-", "portlar": h.get("Publishers") and ", ".join(f"{p['URL']}:{p['PublishedPort']}->{p['TargetPort']}" for p in h["Publishers"] if p["PublishedPort"]) or "-"} for h in holat]))
up -d dan hamma xizmat tayyor bo'lguncha: 9 s
| xizmat | holat | salomatlik | portlar |
|---|---|---|---|
| db | running | healthy | - |
| mediamtx | running | - | 127.0.0.1:18554->8554 |
| nginx | running | - | 127.0.0.1:18088->80, 127.0.0.1:18443->443 |
| signal | running | - | - |
| tahlilchi | running | - | - |
| telegram | running | - | - |
| web | running | healthy | - |
Tashqariga faqat ikkita manzil ochiq: nginx (18443 — HTTPS, 18088 — HTTP) va kameralar uchun RTSP (18554), hammasi 127.0.0.1 da. Baza va Django ning porti yo'q.
WSL eslatmasi. Bu bob birinchi marta ishga tushganda keyingi bloklar tushunarsiz yiqildi: soxta kamera import argparse da "No such file or directory" bilan to'xtadi, docker compose -f deploy/... bo'sh natija qaytardi. Tekshiruv ko'rsatdi: docker compose up -d dan keyin Python jarayonining joriy papkasi yaroqsiz bo'lib qolgan — os.getcwd() xato beradi, nisbiy yo'llar bilan ishlaydigan dasturlar esa undan meros olib yiqiladi. Bu faqat loyiha Windows diskida (/mnt/c/...) bo'lganda ko'rindi; ehtimoliy sabab — Docker Desktop Windows diskidagi papkalarni konteynerlarga ulayotganda (bind mount) ularni qayta ulashi. Yechim kodda: har bir docker compose buyrug'idan keyin os.chdir(ILDIZ) — mutlaq yo'l bilan papkaga qaytish. Undan yaxshisi — Docker loyihalarini WSL ning o'z fayl tizimida (~/...) saqlash: Docker o'zi ham shuni tavsiya qiladi, u yerda fayl tizimi tezroq ham ishlaydi.
Tashqi eshikni tekshirish
nginx haqiqatan himoya qiladimi — HTTP so'rovlar bilan tekshiramiz. Sertifikat o'zimiz imzolagan, shuning uchun tekshiruvda uni ishonchli deb ko'rsatamiz (faqat shu sertifikatni, cafile bilan — umumiy tekshiruvni o'chirmaymiz).
import ssl tls = ssl.create_default_context(cafile=str(sertifikat / "server.crt")) ASOS = "https://127.0.0.1:18443" class YonaltirmaslikUchun(urllib.request.HTTPRedirectHandler): def redirect_request(self, *a, **kw): return None ochuvchi = urllib.request.build_opener(YonaltirmaslikUchun, urllib.request.HTTPSHandler(context=tls)) def sorov(url, data=None, sarlavha=None): try: with ochuvchi.open(urllib.request.Request(url, data=data, headers=sarlavha or {}), timeout=10) as r: return r.status, dict(r.headers) except urllib.error.HTTPError as e: return e.code, dict(e.headers) qatorlar = [] kod, h = sorov(f"{ASOS}/api/salomatlik/") qatorlar.append({"tekshiruv": "API salomatlik (HTTPS)", "javob": kod, "izoh": "server: " + h.get("Server", "-")}) kod, h = sorov("http://127.0.0.1:18088/panel/") qatorlar.append({"tekshiruv": "HTTP -> HTTPS", "javob": kod, "izoh": h.get("Location", "-")}) kod, h = sorov(f"{ASOS}/panel/") qatorlar.append({"tekshiruv": "panel, kirishsiz", "javob": kod, "izoh": h.get("Location", "-")}) kod, h = sorov(f"{ASOS}/api/hodisalar/", data=b"x" * 70_000, sarlavha={"Authorization": "Bearer xato"}) qatorlar.append({"tekshiruv": "70 KB so'rov", "javob": kod, "izoh": "Django ga yetmaydi"}) kod, h = sorov(f"{ASOS}/api/hodisalar/", data=b"{}", sarlavha={"Authorization": "Bearer xato", "Content-Type": "application/json"}) qatorlar.append({"tekshiruv": "noto'g'ri token", "javob": kod, "izoh": "Django javobi"}) show(pd.DataFrame(qatorlar)) print("xavfsizlik sarlavhalari:", {k: v for k, v in sorov(f"{ASOS}/api/salomatlik/")[1].items() if k in ("Strict-Transport-Security", "X-Content-Type-Options", "X-Frame-Options", "Referrer-Policy")}) kodlar = [sorov(f"{ASOS}/panel/login/")[0] for _ in range(60)] print("panel: 60 ta ketma-ket so'rov ->", {k: kodlar.count(k) for k in sorted(set(kodlar))})
xavfsizlik sarlavhalari: {'Strict-Transport-Security': 'max-age=31536000', 'X-Content-Type-Options': 'nosniff', 'X-Frame-Options': 'DENY', 'Referrer-Policy': 'same-origin'}
panel: 60 ta ketma-ket so'rov -> {200: 21, 429: 39}
| tekshiruv | javob | izoh |
|---|---|---|
| API salomatlik (HTTPS) | 200 | server: nginx |
| HTTP -> HTTPS | 301 | https://127.0.0.1/panel/ |
| panel, kirishsiz | 302 | /panel/login/?next=/panel/ |
| 70 KB so'rov | 413 | Django ga yetmaydi |
| noto'g'ri token | 401 | Django javobi |
- Server o'z versiyasini aytmaydi (
nginx, raqamsiz) — hujumchiga zaif versiyani qidirish osonlashmaydi. - HTTP so'rov HTTPS ga yo'naltiriladi. Sinovda
Locationda port yo'q (18443 emas): nginx konteyner ichida 443 da, tashqi port boshqa. Haqiqiy serverda ikkalasi 443 bo'ladi. - 70 KB so'rov nginx da 413 bilan to'xtadi — Django uni ko'rmadi ham.
- Panelga ketma-ket 60 ta so'rovdan 21 tasi o'tdi (bitta + 20 talik zaxira), qolgani 429 bilan qaytdi — parolni taxmin qilish sekinlashadi.
Kameralar bilan
Soxta kameralarni (19-bob) shu kompyuterda ishga tushiramiz — ular konteynerdagi MediaMTX ga 127.0.0.1:18554 orqali yuboradi, xuddi haqiqiy kamera tarmoqdan yuborgandek. Tahlilchi konteyneri esa ularni ichki tarmoqdan (mediamtx:8554) o'qiydi va hodisalarni web:8000 ga yuboradi. 40 soniyadan keyin bazaga qaraymiz — endi bazaning porti yo'q, shuning uchun so'rov konteyner ichida, docker compose exec bilan.
def sql(so_rov): r = dc("exec", "-T", "db", "psql", "-U", "kitob", "-d", "kuzatuv", "-At", "-F", "|", "-c", so_rov) return [q.split("|") for q in r.stdout.strip().splitlines() if q] kamera = [sys.executable, "kod/19_soxta_kamera.py", "--url", f"rtsp://kamera:{sirlar['KAMERA_PAROL']}@127.0.0.1:18554/", "--soni", "2"] with jarayon(kamera): time.sleep(40) show(pd.DataFrame(sql("select tur, signal, count(*) from hodisalar_hodisa group by 1, 2 order by 1, 2"), columns=["tur", "signal", "hodisalar"])) kech = sql("select tur, percentile_cont(0.5) within group (order by extract(epoch from yaratildi - vaqt)), " "percentile_cont(0.5) within group (order by extract(epoch from yuborildi - vaqt)) from hodisalar_hodisa " "where tur in ('chiziq', 'zona') group by 1") show(pd.DataFrame([{"tur": t, "kadr → baza, s (median)": round(float(a), 2), "kadr → Telegram, s (median)": round(float(b), 2) if b else None} for t, a, b in kech])) gpu = dc("exec", "-T", "tahlilchi", "python", "-c", "import torch; print(torch.cuda.get_device_name(0))").stdout.strip() print("tahlilchi konteyneri ko'rgan GPU:", gpu)
tahlilchi konteyneri ko'rgan GPU: NVIDIA GeForce RTX 5070 Ti
| tur | signal | hodisalar |
|---|---|---|
| chiziq | kerak_emas | 25 |
| zona | yuborildi | 23 |
| tur | kadr → baza, s (median) | kadr → Telegram, s (median) |
|---|---|---|
| chiziq | 0.1 | NaN |
| zona | 0.11 | 0.73 |
Butun zanjir konteynerlarda ishladi: kameralar → MediaMTX → tahlilchi (GPU) → web → Postgres → signal → Telegram. Bu yerda tahlilchi TensorRT emas, oddiy PyTorch modeli (yolo11n.pt) bilan ishlaydi: Ultralytics obrazida TensorRT umuman yo'q (tekshirdik), 18-bobdagi dvigatel esa uni yaratgan TensorRT versiyasiga bog'langan. Konteynerga TensorRT qo'shish va dvigatelni o'sha yerda eksport qilish — mashq.
Yiqilish, qayta ishga tushish, zaxira
Uch savol, har biriga o'lchov bilan javob:
- Django jarayoni yiqilsa, tizim o'zi turadimi va qancha vaqtda?
- Konteynerlar o'chirib yoqilsa (masalan, serverni yangilash), ma'lumotlar qoladimi?
- Baza zaxirasi haqiqatan tiklanadimi?
def salomat(): try: return sorov(f"{ASOS}/api/salomatlik/")[0] == 200 except OSError: return False hodisalar = int(sql("select count(*) from hodisalar_hodisa")[0][0]) dc("exec", "-T", "web", "sh", "-c", "kill -TERM 1") # gunicorn — konteynerning 1-jarayoni t0 = time.time() while salomat() and time.time() - t0 < 10: time.sleep(0.1) yiqildi = time.time() while not salomat() and time.time() - t0 < 120: time.sleep(0.2) tiklandi = time.time() qayta = json.loads(subprocess.run(["docker", "inspect", "yolo-kitob-web-1"], capture_output=True, text=True).stdout)[0]["RestartCount"] print(f"web jarayoni to'xtatildi: {yiqildi - t0:.1f} s dan keyin API javob bermadi, yana {tiklandi - yiqildi:.1f} s dan keyin tiklandi " f"(Docker qayta ishga tushirishlari: {qayta})") dc("down") # konteynerlar o'chiriladi, volume qoladi dc("up", "-d", "--wait") print("down + up: hodisalar oldin", hodisalar, "| keyin", sql("select count(*) from hodisalar_hodisa")[0][0]) zaxira = Path("out/zaxira/kuzatuv.sql") zaxira.parent.mkdir(parents=True, exist_ok=True) zaxira.write_text(dc("exec", "-T", "db", "pg_dump", "-U", "kitob", "--no-owner", "kuzatuv").stdout) with postgres(port=25433, nom="yolo-kitob-tiklash") as pg: tiklash = subprocess.run(["psql", "-h", pg["PG_HOST"], "-p", pg["PG_PORT"], "-U", "kitob", "-d", "kuzatuv", "-q", "-f", str(zaxira)], env={**os.environ, "PGPASSWORD": pg["PG_PAROL"]}, capture_output=True, text=True) with psycopg.connect(host=pg["PG_HOST"], port=pg["PG_PORT"], user="kitob", password=pg["PG_PAROL"], dbname="kuzatuv") as db: tiklangan = db.execute("select count(*) from hodisalar_hodisa").fetchone()[0] print(f"zaxira: {zaxira.stat().st_size // 1024} KB | yangi bazaga tiklandi: {tiklangan} hodisa (asl bazada {hodisalar}), xatolar: {tiklash.returncode}")
web jarayoni to'xtatildi: 0.0 s dan keyin API javob bermadi, yana 2.2 s dan keyin tiklandi (Docker qayta ishga tushirishlari: 1) down + up: hodisalar oldin 48 | keyin 48 zaxira: 32 KB | yangi bazaga tiklandi: 48 hodisa (asl bazada 48), xatolar: 0
- Yiqilish: gunicorn to'xtatildi — Docker
restart: unless-stoppedbo'yicha konteynerni o'zi qayta ko'tardi, API bir necha soniyada tiklandi. Hech kim tungi soat 3 da serverga kirishi shart emas. - down + up: konteynerlar o'chirildi va yangidan yaratildi, hodisalar esa joyida — ular
pgdatavolume ida, konteynerda emas. Konteyner — bir martalik, volume — doimiy. (docker compose down -vesa volume ni ham o'chiradi — haqiqiy serverda hech qachon.) - Zaxira:
pg_dumpbitta SQL fayl beradi, u toza bazaga to'liq tiklandi. Zaxira tiklanishi tekshirilmagan bo'lsa — u zaxira emas. Haqiqiy serverda: har kechapg_dump, shifrlangan holda boshqa joyga (masalan, boshqa server yoki S3-mos ombor), va oyiga bir marta tiklash sinovi.
Resurslar:
statlar = subprocess.run(["docker", "stats", "--no-stream", "--format", "{{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"], capture_output=True, text=True).stdout show(pd.DataFrame([q.split("\t") for q in statlar.splitlines() if q.startswith("yolo-kitob")], columns=["konteyner", "xotira", "CPU"]))
| konteyner | xotira | CPU |
|---|---|---|
| yolo-kitob-nginx-1 | 9.633MiB / 15.62GiB | 0.00% |
| yolo-kitob-tahlilchi-1 | 696.1MiB / 15.62GiB | 99.18% |
| yolo-kitob-signal-1 | 45.29MiB / 15.62GiB | 0.13% |
| yolo-kitob-web-1 | 132.5MiB / 15.62GiB | 0.01% |
| yolo-kitob-db-1 | 27.26MiB / 15.62GiB | 0.07% |
| yolo-kitob-mediamtx-1 | 19.99MiB / 15.62GiB | 0.00% |
| yolo-kitob-telegram-1 | 13.73MiB / 15.62GiB | 0.01% |
GPU li tahlilchi eng ko'p xotira oladi (PyTorch va CUDA), qolganlari — 10 dan 130 MB gacha. Tahlilchining CPU si ~100% — bitta yadro: RTSP dekodlash va Python sikli (18-bob). Kichik mijoz uchun bitta GPU li kompyuter butun tizimni ko'taradi.
Sinovdan keyin tozalaymiz: hammasini o'chiramiz, sinov volume larini ham (-v), va sirlar yozilgan fayllarni o'chiramiz. Obrazlar qoladi — keyingi build tez bo'lishi uchun; ular kerak bo'lmasa: docker image rm yolo-kitob-web:1 yolo-kitob-tahlilchi:1 ultralytics/ultralytics:8.4.17 (oxirgisi bir necha GB).
dc("down", "-v") env_fayl.unlink() for f in sertifikat.iterdir(): f.unlink() sertifikat.rmdir() zaxira.unlink() qoldi = subprocess.run(["docker", "ps", "-a", "--filter", "name=yolo-kitob", "--format", "{{.Names}}"], capture_output=True, text=True).stdout.split() print("qolgan konteynerlar:", qoldi, "| .env bormi:", env_fayl.exists())
qolgan konteynerlar: [] | .env bormi: False
Haqiqiy serverga
Sinov kompyuteridan haqiqiy serverga farqlar:
- Server: Ubuntu LTS, Docker Engine (20-bob), NVIDIA drayveri va NVIDIA Container Toolkit (GPU ni konteynerga berish uchun, docs.nvidia.com dagi rasmiy yo'riqnoma).
- Kirish: SSH faqat kalit bilan, parol bilan kirish o'chiq;
ufwbilan faqat 22 (SSH) va 443 (HTTPS) ochiq, 80 — Let's Encrypt uchun. Kameralar alohida tarmoqda (19-bob), RTSP porti internetga ochilmaydi. - Sertifikat: domen serverga yo'naltiriladi va Let's Encrypt dan bepul sertifikat olinadi (
certbot), u 90 kunda o'zi yangilanadi.certs/papkasiga o'zimiz imzolagan emas, shu sertifikat qo'yiladi. - Sirlar:
deploy/.envfaqat serverda,chmod 600, Git da faqat.env.misol. Parol yoki token oshkor bo'lsa — darhol yangisi bilan almashtiriladi. - Yangilash:
git pull,docker compose up -d --build— migratsiyalar web ishga tushganda o'zi qo'llanadi. Oldindan zaxira. - Kuzatuv:
docker compose psvadocker compose logs --since 1h; tizimning o'zi ham "kamera jim" signalini yuboradi. Disk to'lishini kuzating (video yozuvlar!).
Asosiy fikr
- Docker Compose butun tizimni bitta faylda tasvirlaydi;
up -duni har safar bir xil ko'taradi,restart: unless-stoppedyiqilgan xizmatni o'zi turg'izadi. - Tashqariga faqat nginx: HTTPS, so'rov chegaralari, hajm chegarasi, xavfsizlik sarlavhalari. Baza va Django — faqat ichki tarmoqda.
- Har xizmatga faqat o'z sirlari; sozlama fayllarida parol yo'q (
${...:?}vaMTX_o'zgaruvchilari);.env— Git dan tashqarida. - Ma'lumotlar volume da yashaydi, konteynerda emas; zaxira faqat tiklanishi tekshirilgandagina zaxira.
Mashqlar
tahlilchi.Dockerfilegatensorrt-cu12==10.16.1.11ni qo'shing, obrazni qayta quring, konteynerda dvigatel yarating (docker compose exec tahlilchi yolo export model=weights/yolo11n.pt format=engine half=True imgsz=384,640 dynamic=True batch=16) va--modelni unga o'zgartiring. Dvigatel konteyner qayta yaratilganda yo'qolmasligi uchun uni qayerda saqlash kerak? Tahlilchining CPU va kechikishi o'zgaradimi?docker compose exec dborqali tungi zaxira skriptini yozing:pg_dump→gzip→ sanali fayl, 14 kundan eskisini o'chirish. Unicronga qo'shing va bitta zaxirani toza bazaga tiklab ko'ring.- nginx da panel chegarasini (
rate=5r/s,burst=20) o'zgartirib, 60 ta so'rov tajribasini takrorlang. Oddiy foydalanuvchiga xalaqit bermaydigan, lekin parolni taxmin qilishni sekinlashtiradigan qiymat qaysi?
Bu bobni bepul o'qidingiz.
Kitob o'rgatadi, lekin sizning kodingiz nega ishlamaganini ayta olmaydi. Kursda uy vazifangiz tekshiriladi.