Перейти до змісту

Nginx

Nginx як reverse proxy: браузер не підключається до Uvicorn напряму. Nginx приймає з'єднання, термінує SSL, роздає статику, проксує решту.


Архітектура nginx у цьому стеку

Браузер → nginx:80 → routing:

  GET /static/css/main.css
    → alias /staticfiles/css/main.css   ← з тома, без Django
    → 200 з Cache-Control: max-age=604800

  GET /notes/
    → proxy_pass http://web:8001/notes/  ← HTTP до Uvicorn
    → Django view → HTML → nginx → браузер

  WebSocket UPGRADE /ws/groups/7/chat/
    → proxy_http_version 1.1
    → proxy_set_header Upgrade $http_upgrade    ← ОБОВ'ЯЗКОВО!
    → proxy_set_header Connection $connection_upgrade
    → proxy_pass http://web:8001/ws/groups/7/chat/
    → Uvicorn ASGI → GroupChatConsumer → persistent

nginx.conf — повний з коментарями

worker_processes auto;   # auto = кількість CPU cores
# 2 cores → 2 worker processes

events {
    worker_connections 1024;   # одночасних з'єднань на один worker process
    # 2 cores × 1024 = 2048 одночасних з'єднань
}

http {
    include /etc/nginx/mime.types;   # .css → text/css, .js → application/javascript

    # ── WebSocket Upgrade handling ────────────────────────────────────────────
    # $http_upgrade = заголовок "Upgrade" від браузера ("websocket" або "")
    # Якщо браузер надсилає Upgrade: websocket → $connection_upgrade = "upgrade"
    # Якщо звичайний HTTP запит (Upgrade = "") → $connection_upgrade = "close"
    map $http_upgrade $connection_upgrade {
        default upgrade;
        ""      close;
    }

    # ── Upstream: Django/Uvicorn ──────────────────────────────────────────────
    upstream django {
        server web:8001;   # "web" — DNS ім'я сервісу в Docker network app-net
        # Можна додати кілька: server web2:8001; → load balancing
    }

    server {
        listen 80;
        # У production: listen 443 ssl; + ssl_certificate

        client_max_body_size 10M;   # максимальний розмір upload (форми з файлами)

        # ── Статичні файли (nginx роздає напряму) ────────────────────────────
        location /static/ {
            alias /staticfiles/;   # volume з collectstatic (web → nginx)
            expires 7d;            # браузер кешує 7 днів
            add_header Cache-Control "public, immutable";
            # "immutable": браузер не перевіряє freshness (файли мають hash в іменах)
        }

        # ── Все інше → Uvicorn ───────────────────────────────────────────────
        location / {
            proxy_pass http://django;

            # Реальний IP клієнта (Django бачить nginx IP без цих заголовків):
            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;

            # ── WEBSOCKET UPGRADE (критично!) ─────────────────────────────
            proxy_http_version 1.1;
            # ↑ WebSocket вимагає HTTP/1.1 (connection keep-alive)
            # Без цього nginx downgrade до HTTP/1.0 → Upgrade заголовки ігноруються
            # → WS handshake fails → браузер отримує 400 або 101 а потім одразу close

            proxy_set_header Upgrade    $http_upgrade;
            proxy_set_header Connection $connection_upgrade;
            # ↑ Передаємо Upgrade: websocket і Connection: upgrade до Uvicorn
            # Uvicorn бачить ці заголовки → перемикає протокол → ASGI WS scope

            proxy_read_timeout 300s;    # WS-з'єднання може жити 5 хвилин без активності
            proxy_connect_timeout 75s;  # ліміт на встановлення з'єднання до upstream
            proxy_send_timeout 300s;    # ліміт на відправку відповіді клієнту
        }
    }
}

Як staticfiles потрапляє до nginx

1. Dockerfile COPY . .
   → весь проєкт у /app/

2. entrypoint.sh:
   python manage.py collectstatic --noinput
   → Django збирає всі static файли в STATIC_ROOT (/app/staticfiles/)
   → CSS з crispy_forms, JS, admin, icons — все в одному місці

3. docker-compose.yml:
   web:   volumes: - staticfiles:/app/staticfiles   ← web WRITES here
   nginx: volumes: - staticfiles:/staticfiles:ro    ← nginx READS here (read-only!)

4. nginx.conf:
   location /static/ { alias /staticfiles/; }      ← nginx → volume → файл

→ GET /static/admin/css/base.css
  → nginx → /staticfiles/admin/css/base.css
  → 200 без Python!

STATIC_ROOT у settings.py

# settings.py
STATIC_URL = '/static/'

# collectstatic копіює всі static files сюди:
STATIC_ROOT = BASE_DIR / 'staticfiles'   # /app/staticfiles

# Де Django шукає static files (окрім <app>/static/):
STATICFILES_DIRS = []  # Django знаходить <app>/static/ автоматично

Без nginx vs з nginx

Без nginx:
  Browser → Django/Uvicorn → читає CSS/JS з disk → повертає
  Python process задіяний для кожного статичного файлу

З nginx:
  Browser → nginx → читає CSS/JS з disk напряму → повертає
  Django/Uvicorn не задіяний взагалі

Nginx роздає статику у 5–10x швидше і звільняє Python workers
для обробки реальних запитів.

WebSocket проксі — чому потрібен HTTP/1.1

HTTP/1.0: кожен запит = нове з'єднання
HTTP/1.1: keepalive — з'єднання залишається відкритим

WebSocket Upgrade handshake:
  GET /ws/groups/7/chat/ HTTP/1.1  ← ОБОВ'ЯЗКОВО 1.1
  Upgrade: websocket
  Connection: Upgrade

Якщо nginx проксює як HTTP/1.0:
  Upgrade заголовок ігнорується
  → handshake не проходить
  → WS з'єднання не встановлюється
  → браузер отримує 400 або 404

proxy_http_version 1.1 у location / — обов'язкова директива для WebSocket підтримки.


Nginx у production (з SSL)

server {
    listen 443 ssl;
    server_name your-domain.com;

    ssl_certificate     /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location /static/ {
        alias /staticfiles/;
        expires 7d;
    }

    location / {
        proxy_pass http://django;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header Host $host;
        proxy_read_timeout 300s;
    }
}

server {
    listen 80;
    server_name your-domain.com;
    return 301 https://$host$request_uri;  # HTTP → HTTPS redirect
}

Читання логів nginx

nginx-1  | 172.29.0.1 - - [13/Jun/2026:10:00:00 +0000] "GET /notes/ HTTP/1.1" 200 4523 "-" "Mozilla..."
          │             │                               │             │    │
          IP клієнта    Час                             Метод+URL     Статус  Байти

nginx-1  | "GET /ws/groups/7/chat/ HTTP/1.1" 101 839
                                                  ↑ 101 = WebSocket upgrade SUCCESS

nginx-1  | "GET /static/admin/css/base.css HTTP/1.1" 304 0
                                                       ↑ 304 = Not Modified (браузер використовує кеш)
# Переглянути логи nginx:
docker compose logs nginx

# Слідкувати у реальному часі:
docker compose logs -f nginx

# Перевірити nginx конфіг:
docker compose exec nginx nginx -t

# Перезапустити nginx (після зміни nginx.conf):
docker compose restart nginx

# Тест upstream з середини nginx контейнера:
docker compose exec nginx curl -I http://web:8001/accounts/login/

---

## У книзі

- [Частина X. Linux і DevOps](../../10_linux_and_devops/README.md) — nginx, reverse proxy, статичні файли, HTTP headers, SSL/TLS
- [Частина XI. Deployment](../../11_deployment/README.md) — production стек, nginx + Uvicorn, WebSocket через proxy

---

## Офіційна документація

- [nginx: Reverse proxy](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) — proxy_pass, upstream, headers
- [nginx: WebSocket proxying](https://nginx.org/en/docs/http/websocket.html) — `Upgrade`, `Connection` headers, HTTP/1.1
- [nginx: Static files](https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/) — `location`, `alias`, `expires`
- [MDN: HTTP/1.1 Upgrade mechanism](https://developer.mozilla.org/en-US/docs/Web/HTTP/Protocol_upgrade_mechanism) — як відбувається WebSocket handshake
- [MDN: Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control) — `public`, `immutable`, `max-age`