Asosiy kontentga o‘tish

Maqolalar 9 daqiqa 108 ko‘rish

Docker'siz production deploy: uv, gunicorn, systemd va nginx

Hamma joyda «deploy demak Docker» deb o‘rgatishadi, lekin qo‘lingizda 1GB xotirali arzon VPS bo‘lsa, Docker ortiqcha yuk bo‘lishi mumkin. Shu sayt aynan Docker'siz ishlaydi — to‘liq retseptni boshidan oxirigacha ko‘rsataman.

linux devops backend

Mundarija

Deploy haqidagi deyarli har bir qo‘llanma bugun bitta so‘z bilan boshlanadi: Docker. Ko‘p holatda bu to‘g‘ri maslahat. Lekin qo‘lingizda bir gigabayt xotirali, eskiroq operatsion tizimli arzon VPS bo‘lsa — yoki o‘sha serverda allaqachon boshqa loyihalar yashab tursa — Docker o‘zi bilan olib keladigan qatlam ortiqcha yukka aylanishi mumkin. Shu sayt, solijonov.uz, aynan shunday serverda va aynan Docker'siz ishlaydi. Quyida uning to‘liq retsepti.

Umumiy manzara#

Avval nima nima bilan gaplashishini ko‘rib olaylik:

Sxema
Internet → nginx (443, TLS) → gunicorn (127.0.0.1:8001) → Django
                └── /static/ fayllarni to‘g‘ridan-to‘g‘ri beradi

Bu yerda uch qatlam bor va har biri o‘z ishini qiladi. nginx tashqi eshik vazifasini bajaradi: HTTPS'ni o‘zida tugatadi, statik fayllarni Python'ni bezovta qilmasdan o‘zi beradi, qolgan so‘rovlarni esa gunicorn'ga uzatadi. gunicorn Django ilovasini ko‘tarib turadi. systemd esa qorovul rolini o‘ynaydi: server qayta yuklansa yoki jarayon yiqilsa, uni o‘zi qaytadan ko‘taradi.

1. Python va kutubxonalar — uv bilan#

Eski serverlarda tizimning o‘z Python'i odatda qadimiy bo‘ladi va unga tegmagan ma'qul. uv buni bir necha buyruq bilan hal qiladi — kerakli Python versiyasini o‘zi o‘rnatadi:

Terminal
curl -LsSf https://astral.sh/uv/install.sh | sh
uv python install 3.13
cd /opt/mysite && uv sync --frozen --no-dev

Bu yerda --frozen muhim: u lockfayldagi aynan o‘sha versiyalarni o‘rnatadi, ya'ni lokal kompyuteringizda qanday ishlagan bo‘lsa, serverda ham aynan shunday ishlaydi.

2. Gunicorn'ni systemd ostiga qo‘yish#

Gunicorn'ni shunchaki qo‘lda ishga tushirsa, terminal yopilishi bilan yoki server qayta yuklanishi bilan u o‘ladi. Shuning uchun uni systemd xizmatiga aylantiramiz. /etc/systemd/system/mysite.service fayliga:

INI
[Unit]
Description=Mysite (gunicorn)
After=network.target

[Service]
WorkingDirectory=/opt/mysite
Environment=DJANGO_SETTINGS_MODULE=config.settings.prod
ExecStart=/opt/mysite/.venv/bin/gunicorn config.wsgi:application --bind 127.0.0.1:8001 --workers 2
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
Terminal
systemctl daemon-reload
systemctl enable --now mysite

Bu faylning ikki qatori — Restart=always va enable — mayda ko‘rinsa ham, aynan ular "server reboot bo‘ldi, sayt o‘lik" degan tungi qo‘ng‘iroqlardan saqlaydi. Worker soni haqida esa oddiy qoida bor: kichik server uchun ikkitasi yetadi. Umumiy formula "2 × protsessor yadrosi + 1" bo‘lsa-da, xotira tanqis bo‘lsa uni kamaytirgan ma'qul.

3. Nginx — proxy va statik fayllar#

/etc/nginx/conf.d/mysite.conf fayliga:

nginx
server {
    listen 80;
    server_name mysite.uz www.mysite.uz;

    location /static/ {
        alias /opt/mysite/staticfiles/;
        expires 30d;
        add_header Cache-Control "public, immutable";
    }

    location / {
        proxy_pass http://127.0.0.1:8001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Bu yerdagi X-Forwarded-Proto sarlavhasiga alohida e'tibor bering: Django so‘rov HTTPS orqali kelganini aynan shu sarlavhadan, SECURE_PROXY_SSL_HEADER sozlamasi orqali biladi. Usiz redirect va cookie'lar bilan bog‘liq g‘alati xatolar chiqadi.

Bu blokda birinchi kuni yo‘q edi, keyin qo‘shildi#

Yuqoridagi sozlama ishlaydi va boshlash uchun yetarli. Lekin shu saytning haqiqiy nginx.conf ida bugun yana ikkita narsa bor va ikkalasi ham o‘z-o‘zidan emas, muammodan keyin paydo bo‘lgan.

Birinchisi — siqish. Saytni o‘lchaganimda Django javobni 2 millisekundda tayyorlardi, ya'ni sekinlik serverda emas edi — butun vaqt tarmoqqa va uzatishga ketardi, HTML esa siqilmasdan 50 kilobayt bo‘lib borardi:

nginx
gzip on;
gzip_vary on;
gzip_comp_level 5;
gzip_min_length 512;
gzip_types text/css text/plain application/javascript application/json
           application/xml application/rss+xml image/svg+xml;

text/html ro‘yxatda yo‘q, chunki nginx uni doim siqadi. gzip_vary esa oradagi kesh-proksilarga siqilgan va siqilmagan javobni ajratishni aytadi.

Ikkinchisi — so‘rov chegarasi. Bir kuni sayt yiqildi va sababi hujum ham, kod ham emas edi: bitta yig‘uvchi bot filtr manzillarining barcha kombinatsiyasini bir kunda yuz mingga yaqin marta aylanib chiqdi. Har javob ~85 KB, yozilgan loglar esa diskni to‘ldirdi — va disk to‘lgach sayt to‘xtadi. robots.txt bunday botga iltimos, majburiyat emas. Majburiyat shu yerda bo‘ladi:

nginx
limit_req_zone $binary_remote_addr zone=site:10m rate=10r/s;
limit_req_status 429;   # standart 503 — u "server nosoz" degan ma'no beradi

server {
    location / {
        limit_req zone=site burst=20 nodelay;
        # ... qolgan proxy sozlamalari
    }
}

burst=20 nodelay — bitta sahifa bilan birga keladigan bir nechta so‘rov jazolanmasligi uchun; uzluksiz oqim esa sekinlashadi. 429 ni alohida qo‘yish ham muhim: standart 503 qidiruv tizimlariga "server buzilgan" degan signal beradi, 429 esa "sekinroq so‘rang" deydi.

Ishlashini ko‘rish uchun chegarani ataylab kichraytirib sinab ko‘rdim — rate=1r/s, burst=2 qilib, yigirmata so‘rovni bir vaqtda yubordim:

Natija
{429: 17, 200: 3}

Uchtasi o‘tdi (bittasi chegaradan, ikkitasi burst dan), qolgan o‘n yettitasi 429 oldi. Haqiqiy sozlamada raqamlar odamni bezovta qilmaydigan darajada katta, lekin mexanizm aynan shu.

Bir kichik tuzatish ham: yuqoridagi blokda expires 30d turibdi, lekin Django statik fayl nomiga mazmun hashini qo‘shsa (ManifestStaticFilesStorage), fayl o‘zgarganda nomi ham o‘zgaradi — ya'ni expires 1y xavfsiz va foydaliroq.

4. HTTPS — certbot bilan#

Terminal
uv tool install certbot --with certbot-nginx
~/.local/bin/certbot --nginx -d mysite.uz -d www.mysite.uz

Certbot nginx konfiguratsiyasiga TLS blokini o‘zi yozib qo‘yadi.

To‘liq yo‘l (~/.local/bin/certbot) yozganim tasodif emas. uv tool install buyrug‘i tugagach shunday ogohlantiradi:

Natija
warning: `/root/.local/bin` is not on your PATH.

Ya'ni certbot deb yozsangiz, "command not found" olasiz. Buni PATH'ga qo‘shib qo‘ysangiz ham masala yopilmaydi — va endi eng muhim joyga keldik.

Cron va PATH — sertifikat jimgina eskiradi

Sertifikatni avtomatik yangilash uchun cron'ga 17 3,15 * * * certbot renew --quiet deb yozish tabiiy ko‘rinadi. Lekin cron sizning shell sozlamalaringizni o‘qimaydi va juda qisqa PATH bilan ishlaydi — unda ~/.local/bin yo‘q. Natijada buyruq har safar jimgina "topilmadi" bo‘lib tugaydi, --quiet esa xatoni ham ko‘rsatmaydi. Siz esa hech narsa sezmaysiz — to‘qson kundan keyin sayt "sertifikat muddati tugagan" deb ochilmay qolguncha.

Shuning uchun cron qatorida ham to‘liq yo‘l yoziladi:

Terminal
17 3,15 * * * /root/.local/bin/certbot renew --quiet

Yozib qo‘ygandan keyin uni bir marta quruq sinab ko‘ring — hozir, uch oy o‘tib emas:

Terminal
/root/.local/bin/certbot renew --dry-run

5. Deploy skripti#

Deploy'ning har safar bir xil va bitta buyruq bo‘lishi kerak. Bizniki, soddalashtirilgan ko‘rinishda, shunday:

Terminal
rsync -az --delete \
  --exclude .git --exclude .venv --exclude __pycache__ \
  --exclude .env --exclude staticfiles --exclude var --exclude db.sqlite3 \
  ./ server:/opt/mysite/

ssh server "set -e; cd /opt/mysite; export PATH=\$HOME/.local/bin:\$PATH; \
  uv sync --frozen --no-dev; \
  uv run --no-sync python manage.py collectstatic --noinput; \
  uv run --no-sync python manage.py migrate --noinput; \
  systemctl restart mysite; \
  sleep 2; systemctl is-active mysite"

Bu qisqa skriptning deyarli har bir bo‘lagi biror marta tishlab olgan joydan chiqqan, shuning uchun ularni alohida aytib o‘tay.

--exclude ro‘yxati — eng muhimi. --delete bayrog‘i serverda bor, lokalda yo‘q fayllarni o‘chiradi. Bu kerak (eski fayllar to‘planib qolmasin), lekin shuning uchun ro‘yxatga e'tibor bering. db.sqlite3 ni chiqarib tashlamasangiz, lokal, bo‘sh bazangiz production bazasining ustiga yoziladi. var — ilova ishlash paytida yozadigan holat. staticfiles esa serverda collectstatic yig‘adi; uni lokaldan ko‘chirsangiz, ikkalasi bir-birini buzadi. .env ni ko‘chirmaslik sababi ham shu qatorda: sirlar serverda qoladi.

export PATH=$HOME/.local/bin:$PATH. uv aynan o‘sha papkaga o‘rnatiladi, lekin SSH orqali buyruq yuborganingizda shell interaktiv bo‘lmaydi va .bashrc o‘qilmaydi — ya'ni uv: command not found olasiz. Bu certbot bilan bo‘lgan tuzoqning aynan o‘zi, faqat boshqa buyruqda.

set -e. Usiz collectstatic xato bersa ham skript keyingi qatorga o‘tadi va oxirida xizmatni qayta ishga tushirib, "deploy muvaffaqiyatli" degan taassurot qoldiradi. set -e birinchi xatoda to‘xtatadi.

systemctl is-active — oxirgi qator. restart buyrug‘i xizmat ko‘tarilganini bildirmaydi, u faqat "buyruq qabul qilindi" deydi. Yangi kod ishga tushmay yiqilsa, restart baribir muvaffaqiyat qaytaradi. Ikki soniya kutib is-active so‘rash — deploy'ning haqiqatan o‘tganini tekshiradigan yagona qator.

Docker qachon kerak, qachon yo‘q#

Endi asosiy savolga qaytaylik. Docker'siz yo‘l qachon yaxshi? Loyihangiz bir-ikkita bo‘lsa, serveringiz kichik bo‘lsa, tez boshlash kerak bo‘lsa va xotira tanqis bo‘lsa. Docker qachon o‘zini oqlaydi? Loyihangiz bir necha xizmatdan iborat bo‘lsa — baza, Redis, fon ishchilari birga ishlasa — katta jamoada ishlasangiz va "mening kompyuterimda ishlagan edi" muammosi tez-tez chiqsa.

Ya'ni bu yerda "to‘g‘ri" va "noto‘g‘ri" yo‘l yo‘q, faqat vaziyatga mos yo‘l bor. Bizga bugun yengillik kerak edi, shuning uchun Docker'siz yo‘ldan bordik. Ertaga server kattalashib, ehtiyoj o‘zgarsa, Compose fayli tayyor turibdi — u yoqqa o‘tish qiyin bo‘lmaydi.

9 daqiqa 108 marta o‘qildi