Asosiy qismga o'tish

Docker bilan o'rnatish: bitta buyruq bilan butun tizim

21-bob 16 kod bloki 4689 so'z

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:

python12 qator
            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.

.dockerignoreyangi fayl
python9 qator
# 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.

deploy/web.Dockerfileyangi fayl
python8 qator
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.

deploy/tahlilchi.Dockerfileyangi fayl
python7 qator
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.

deploy/nginx.confyangi fayl
python47 qator
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;
        }
    }
}
deploy/proxy.confyangi fayl
matn8 qator
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.

deploy/mediamtx.ymlyangi fayl
yaml24 qator
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 .env faylidan 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.1 da (sinov uchun; haqiqiy serverda nginx 0.0.0.0:443 da, qolganini firewall yopadi).
  • GPU: deploy.resources.reservations.devices — konteynerga NVIDIA GPU beriladi.
  • Soxta Telegram sinov profilida: u faqat --profile sinov bilan ishga tushadi; haqiqiy o'rnatishda bu xizmat va TELEGRAM_API qatorlari olib tashlanadi.
deploy/docker-compose.ymlyangi fayl
yaml108 qator
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.misolyangi fayl
python9 qator
# 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.

kod/21_docker.pyyangi fayl · 1-qator: from kitob_yordam import fig, show
python48 qator
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"]))
Chiqish
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
obrazhajmi
yolo-kitob-web:1269MB
yolo-kitob-tahlilchi:114.1GB
bluenviron/mediamtx:1.21.183.5MB
postgres:16.13-alpine395MB
nginx:1.29.5-alpine93.4MB
ultralytics/ultralytics:8.4.1714.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.

kod/21_docker.pydavomi
python12 qator
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]))
Chiqish
up -d dan hamma xizmat tayyor bo'lguncha: 9 s
xizmatholatsalomatlikportlar
dbrunninghealthy-
mediamtxrunning-127.0.0.1:18554->8554
nginxrunning-127.0.0.1:18088->80, 127.0.0.1:18443->443
signalrunning--
tahlilchirunning--
telegramrunning--
webrunninghealthy-

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).

kod/21_docker.pydavomi
python35 qator
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))})
Chiqish
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}
tekshiruvjavobizoh
API salomatlik (HTTPS)200server: nginx
HTTP -> HTTPS301https://127.0.0.1/panel/
panel, kirishsiz302/panel/login/?next=/panel/
70 KB so'rov413Django ga yetmaydi
noto'g'ri token401Django javobi
  • Server o'z versiyasini aytmaydi (nginx, raqamsiz) — hujumchiga zaif versiyani qidirish osonlashmaydi.
  • HTTP so'rov HTTPS ga yo'naltiriladi. Sinovda Location da 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.

kod/21_docker.pydavomi
python15 qator
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)
Chiqish
tahlilchi konteyneri ko'rgan GPU: NVIDIA GeForce RTX 5070 Ti
tursignalhodisalar
chiziqkerak_emas25
zonayuborildi23
turkadr → baza, s (median)kadr → Telegram, s (median)
chiziq0.1NaN
zona0.110.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:

  1. Django jarayoni yiqilsa, tizim o'zi turadimi va qancha vaqtda?
  2. Konteynerlar o'chirib yoqilsa (masalan, serverni yangilash), ma'lumotlar qoladimi?
  3. Baza zaxirasi haqiqatan tiklanadimi?
kod/21_docker.pydavomi
python32 qator
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}")
Chiqish
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-stopped bo'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 pgdata volume ida, konteynerda emas. Konteyner — bir martalik, volume — doimiy. (docker compose down -v esa volume ni ham o'chiradi — haqiqiy serverda hech qachon.)
  • Zaxira: pg_dump bitta SQL fayl beradi, u toza bazaga to'liq tiklandi. Zaxira tiklanishi tekshirilmagan bo'lsa — u zaxira emas. Haqiqiy serverda: har kecha pg_dump, shifrlangan holda boshqa joyga (masalan, boshqa server yoki S3-mos ombor), va oyiga bir marta tiklash sinovi.

Resurslar:

kod/21_docker.pydavomi
python2 qator
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"]))
konteynerxotiraCPU
yolo-kitob-nginx-19.633MiB / 15.62GiB0.00%
yolo-kitob-tahlilchi-1696.1MiB / 15.62GiB99.18%
yolo-kitob-signal-145.29MiB / 15.62GiB0.13%
yolo-kitob-web-1132.5MiB / 15.62GiB0.01%
yolo-kitob-db-127.26MiB / 15.62GiB0.07%
yolo-kitob-mediamtx-119.99MiB / 15.62GiB0.00%
yolo-kitob-telegram-113.73MiB / 15.62GiB0.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).

kod/21_docker.pydavomi
python8 qator
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())
Chiqish
qolgan konteynerlar: [] | .env bormi: False

Haqiqiy serverga

Sinov kompyuteridan haqiqiy serverga farqlar:

  1. 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).
  2. Kirish: SSH faqat kalit bilan, parol bilan kirish o'chiq; ufw bilan faqat 22 (SSH) va 443 (HTTPS) ochiq, 80 — Let's Encrypt uchun. Kameralar alohida tarmoqda (19-bob), RTSP porti internetga ochilmaydi.
  3. 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.
  4. Sirlar: deploy/.env faqat serverda, chmod 600, Git da faqat .env.misol. Parol yoki token oshkor bo'lsa — darhol yangisi bilan almashtiriladi.
  5. Yangilash: git pull, docker compose up -d --build — migratsiyalar web ishga tushganda o'zi qo'llanadi. Oldindan zaxira.
  6. Kuzatuv: docker compose ps va docker 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 -d uni har safar bir xil ko'taradi, restart: unless-stopped yiqilgan 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 (${...:?} va MTX_ o'zgaruvchilari); .env — Git dan tashqarida.
  • Ma'lumotlar volume da yashaydi, konteynerda emas; zaxira faqat tiklanishi tekshirilgandagina zaxira.

Mashqlar

  1. tahlilchi.Dockerfile ga tensorrt-cu12==10.16.1.11 ni 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 --model ni unga o'zgartiring. Dvigatel konteyner qayta yaratilganda yo'qolmasligi uchun uni qayerda saqlash kerak? Tahlilchining CPU va kechikishi o'zgaradimi?
  2. docker compose exec db orqali tungi zaxira skriptini yozing: pg_dump → gzip → sanali fayl, 14 kundan eskisini o'chirish. Uni cron ga qo'shing va bitta zaxirani toza bazaga tiklab ko'ring.
  3. 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.

Telegramda yozish Narxlar