Asosiy kontentga o‘tish

Maqolalar 9 daqiqa 279 ko‘rish

Nginx amaliyotda: reverse proxy, statika va kelajakdagi 502 lar

Gunicorn 127.0.0.1'da ishlab turibdi, endi uni dunyoga chiqarish kerak. Nginx'ni reverse proxy qilib sozlaymiz: to'g'ri header'lar, statikani tez berish, WebSocket'ni o'tkazish va 502 chiqqanda qayerga qarashni bilish.

devops django

Mundarija

Avvalgi qismda gunicorn'ni systemd ostida ishga tushirdik, lekin u ataylab 127.0.0.1:8001ga bog'langan — tashqaridan unga kirib bo'lmaydi. Bu tasodif emas: Python-serverni to'g'ridan-to'g'ri internetga ochish yomon odat. Uning oldida nginx turadi va uchta ishni o'z zimmasiga oladi: statik fayllarni Python'ni bezovta qilmasdan beradi, sekin mijozlarning so'rovlarini o'zi qabul qilib gunicorn'ni band qilmaydi, keyinchalik esa TLS'ni ham o'zi ko'taradi.

O'rnatish#

Terminal
sudo apt update && sudo apt install -y nginx
systemctl status nginx --no-pager | head -3   # active (running)

Nginx o'rnatilishi bilan ishga tushadi va 80-portni tinglaydi. Brauzerda http://SERVER_IP ni ochsangiz, "Welcome to nginx!" sahifasi chiqadi — bu paket bilan kelgan standart sayt. 80-port 1-qismda firewall'da ochilgan edi.

Konfiguratsiya fayli qayerda yashaydi#

Ubuntu'da nginx saytlari ikki papka bilan boshqariladi: sites-availableda fayllar turadi, sites-enabledda esa ularga symlink qo'yiladi. Sayt o'chirish — symlink'ni olib tashlash, fayl esa joyida qoladi. Yangi sayt uchun fayl ochamiz:

Terminal
sudo nano /etc/nginx/sites-available/mysite
nginx
# /etc/nginx/sites-available/mysite
server {
    listen 80;
    server_name mysite.uz www.mysite.uz;

    # yuklanadigan fayl hajmi chegarasi (default atigi 1M — formalar uchun kam)
    client_max_body_size 10M;

    # ---- statika: Python'ga umuman tegmaydi ----
    location /static/ {
        alias /opt/mysite/staticfiles/;
        # fayl nomlarida hash bor (Django ManifestStaticFilesStorage),
        # shuning uchun ularni "abadiy" keshlash xavfsiz: nom o'zgarsa,
        # bu boshqa fayl degani
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    location /media/ {
        alias /opt/mysite/media/;
        expires 7d;
    }

    # ---- qolgan hamma narsa gunicorn'ga ----
    location / {
        proxy_pass http://127.0.0.1:8001;
        # bu header'larsiz Django "hamma 127.0.0.1'dan kelyapti" deb o'ylaydi
        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;
    }
}

Yoqamiz va tekshiramiz:

Terminal
sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo nginx -t          # sintaksisni ALBATTA tekshiramiz — xato bo'lsa aytadi
sudo systemctl reload nginx

nginx -tni odat qiling: reload xatoli konfiguratsiya bilan chaqirilsa, nginx eski holatida ishlashda davom etadi, lekin siz buni sezmay "nega o'zgarish tushmayapti" deb yarim soat yo'qotasiz.

Endi serverning o'zida tekshiramiz. Domen DNS'da hali serverga yo'naltirilmagan bo'lsa ham, Host sarlavhasini qo'lda berib nginx'ga "mysite.uz so'ralyapti" deymiz:

Terminal
curl -I -H "Host: mysite.uz" http://127.0.0.1/
# HTTP/1.1 200 OK

Host siz so'rov (yoki brauzerda http://SERVER_IP) bizning saytga emas, standart saytga tushadi — server_name faqat domen nomi bilan kelgan so'rovlarni ushlaydi. IP orqali kelganlarga "Welcome to nginx!" sahifasi kerak bo'lmasa, standart saytni o'chiring (fayli sites-available da qoladi):

Terminal
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t && sudo systemctl reload nginx

Header'lar nega bunchalik muhim#

proxy_set_header qatorlari shunchaki rasm-rusum emas. Ularsiz Django'ga yetib borgan so'rovda mijozning haqiqiy IP'si ham, so'ralgan domen ham, protokol ham yo'q — hammasi nginx'niki bilan almashib ketgan bo'ladi. Buning amaliy oqibatlari esa juda seziladi: log'da hamma mehmon 127.0.0.1 bo'lib chiqadi, rate limiting hammani bitta odam deb hisoblab bloklab qo'yadi, keyinchalik HTTPS yoqilganda esa Django redirect'lari noto'g'ri ishlaydi.

Django tomonida bunga mos ikki sozlama bor:

Python
# config/settings.py
ALLOWED_HOSTS = ["mysite.uz", "www.mysite.uz"]

# X-Forwarded-Proto'ga ishonamiz — TLS qismida kerak bo'ladi
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")

SECURE_PROXY_SSL_HEADER — faqat proxy ortida

Bu sozlama "X-Forwarded-Proto: https ko'rsam, so'rov xavfsiz deb hisoblayman" degani. Ilova to'g'ridan-to'g'ri internetga ochiq bo'lsa, istalgan mijoz shu header'ni o'zi yozib yuborishi mumkin. Bizning sxemada xavfsiz, chunki tashqaridan kelgan header'larni nginx o'ziniki bilan almashtiradi — lekin sxemani o'zgartirsangiz, buni yodda tuting.

Statika: collectstatic bilan kelishuv#

Yuqoridagi location /static/ ishlashi uchun Django fayllarni bitta papkaga yig'ib bergan bo'lishi kerak:

Python
# config/settings.py
STATIC_URL = "/static/"
STATIC_ROOT = "/opt/mysite/staticfiles"
STORAGES = {
    # "default" ni tashlab ketmang: STORAGES yozilsa, Django standart
    # qiymatlarni to'ldirmaydi va media yuklash InvalidStorageError beradi
    "default": {
        "BACKEND": "django.core.files.storage.FileSystemStorage",
    },
    "staticfiles": {
        # fayl nomiga kontent hash'i qo'shiladi: tailwind.4527ba3c91d2.css
        "BACKEND": "django.contrib.staticfiles.storage.ManifestStaticFilesStorage",
    },
}
MEDIA_URL = "/media/"
MEDIA_ROOT = "/opt/mysite/media"
Terminal
cd /opt/mysite && .venv/bin/python manage.py collectstatic --noinput
# 42 static files copied to '/opt/mysite/staticfiles', 42 post-processed.

ls /opt/mysite/staticfiles/css/
# tailwind.4527ba3c91d2.css  tailwind.css

Hash'li nusxa bilan birga asl nomli fayl ham turadi — Django shablonlarda {% static %} orqali avtomatik hash'li nomni qo'yadi.

Hash'li nomlar va nginx'dagi immutable juftligi chiroyli ishlaydi: brauzer CSS'ni bir yilga keshlab oladi, deploy'da esa fayl nomi o'zgargani uchun yangi versiya darhol tushadi. "Foydalanuvchida eski dizayn qolibdi, Ctrl+F5 bosing" degan suhbatlar shu ikki qator bilan tarixga aylanadi.

WebSocket'ni o'tkazish#

Loyihada Django Channels bo'lsa, alohida location kerak bo'ladi, chunki WebSocket oddiy proxy orqali o'tmaydi — ulanishni "upgrade" qilish kerak.

Lekin avval orqadagi serverni tekshiring. 2-qismdagi unit config.wsgi ni gunicorn'ning oddiy (sinxron) worker'i bilan ishga tushiradi — WSGI esa WebSocket'ni umuman tushunmaydi: nginx to'g'ri sozlangan bo'lsa ham /ws/ ga ulanish 404 bilan qaytadi. Channels loyihasida gunicorn ASGI ilovani uvicorn worker'i bilan ko'tarishi kerak. Worker uvicorn-worker paketida (uvicorn ichidagi eski uvicorn.workers eskirgan deb belgilangan), uvicorn ning o'zini esa [standard] qo'shimchasi bilan o'rnating — usiz uvicorn'da WebSocket kutubxonasi bo'lmaydi va ulanish yana 404 oladi:

Terminal
/opt/mysite/.venv/bin/pip install "uvicorn[standard]" uvicorn-worker
INI
# /etc/systemd/system/mysite-web.service — faqat ExecStart o'zgaradi
ExecStart=/opt/mysite/.venv/bin/gunicorn config.asgi:application \
    -k uvicorn_worker.UvicornWorker \
    --workers 2 \
    --bind 127.0.0.1:8001

sudo systemctl daemon-reload && sudo systemctl restart mysite-web — endi oddiy sahifalar ham, WebSocket ham shu bitta 8001-portdan o'tadi. Nginx tomonida esa shu location qo'shiladi (server { ... } bloki ichiga):

nginx
    location /ws/ {
        proxy_pass http://127.0.0.1:8001;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        # WebSocket ulanishlari uzoq yashaydi — default 60s uzib qo'ymasin
        proxy_read_timeout 3600s;
    }

Tekshirish — WebSocket qo'l siqishini curl bilan yuboramiz (/ws/alerts/ o'rniga o'z consumer manzilingiz):

Terminal
curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Host: mysite.uz" -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  http://127.0.0.1/ws/alerts/
# 101

101 (Switching Protocols) — ulanish WebSocket'ga o'tdi. 404 chiqsa — orqada hali WSGI yoki uvicorn[standard] o'rnatilmagan.

Channels maqolasida va'da qilingan qism aynan shu edi — endi to'liq sxema qo'lingizda.

502 chiqdi: qayerga qaraymiz#

Har kim hayotida ko'p marta ko'radigan xato — 502 Bad Gateway. Ma'nosi oddiy: nginx so'rovni qabul qildi, lekin orqadagi gunicorn'dan javob ololmadi. Demak muammo deyarli har doim nginx'da emas, orqada. Tekshirish tartibi:

Terminal
# 1. Xizmat tirikmi?
systemctl status mysite-web

# 2. Port to'g'rimi? (unit'dagi --bind bilan nginx'dagi proxy_pass bir xilmi)
ss -tlnp | grep 8001

# 3. Nginx aniq nima deyapti?
sudo tail -20 /var/log/nginx/error.log
# connect() failed (111: Connection refused) — xizmat yotibdi yoki port noto'g'ri

Uning qo'shnisi — 504 Gateway Timeout. Bu safar gunicorn tirik va ulanishni qabul qildi, lekin nginx kutgan vaqt (standart proxy_read_timeout — 60 soniya) ichida javob bermadi. Log'da upstream timed out yoziladi. Sabab odatda ilovaning o'zida: sekin SQL so'rov, javob bermayotgan tashqi API yoki qotib qolgan worker. Timeout'ni oshirish bilan emas, sekin joyni topish bilan hal qilinadi.

Shu uch buyruq 502'larning to'qson foizini yechadi. Qolgan holatlarga — loglarni tizimli o'qishga — alohida qism bag'ishlaymiz.

Yakuniy holat#

Endi zanjir to'liq: foydalanuvchi 80-portga keladi, statikani nginx beradi, dinamikani gunicorn'ga uzatadi, gunicorn'ni systemd qo'riqlaydi, hammasi esa 1-qismda qurilgan firewall ortida. Bitta katta kamchilik qoldi — trafik hali ochiq HTTP'da yurmoqda. Keyingi qismda buni yopamiz: Let's Encrypt bilan bepul TLS sertifikat olish, avtomatik yangilanishi va HTTPS'ga to'g'ri ko'chish.

9 daqiqa 279 marta o‘qildi