Во время разработки python manage.py runserver очень удобен, но он предназначен только для тестирования. Этот сервер плохо держит много одновременных запросов, не проходил аудит безопасности и неэффективно отдаёт статические файлы. На реальном сайте обязанности делятся надвое: Gunicorn выполняет Python-код, а Nginx общается с внешним миром — берёт на себя HTTPS, статику и медленных клиентов. Ниже соберём такую схему с нуля на VPS с Ubuntu.
Схема: какой путь проходит запрос
Когда браузер пользователя обращается к misol.uz, запрос сначала попадает в Nginx. Если это адрес /static/ или /media/, Nginx сам отдаёт файл с диска и вообще не трогает Python. Остальные запросы передаются через Unix-сокет в Gunicorn. Gunicorn запускает несколько рабочих процессов и в каждом выполняет Django-приложение через интерфейс WSGI. Благодаря такому разделению пользователь с медленным интернетом не занимает Python-воркер: доставкой ответа до конца занимается Nginx.
Для примера возьмём сайт частной школы в Самарканде misol.uz. На нём есть новости, расписание уроков и админка Django для учителей. По утрам родители одновременно открывают расписание, и за короткое время на сайт приходит много запросов. Описанная ниже настройка спокойно выдерживает такую нагрузку.
Подготовка сервера и виртуального окружения
Сначала установим нужные пакеты и создадим отдельного системного пользователя для проекта. Запускать приложение от root опасно: одна уязвимость в коде открывает путь к управлению всем сервером.
sudo apt update
sudo apt install python3-venv python3-pip nginx
sudo adduser --system --group --home /srv/misol misol
sudo -u misol python3 -m venv /srv/misol/venv
sudo -u misol /srv/misol/venv/bin/pip install -r /srv/misol/requirements.txt gunicorn
Будем считать, что код проекта лежит в /srv/misol, а настройки Django — в пакете config. Если у вас пакет называется иначе, в следующих командах замените config.wsgi на своё имя.
Настройки Django для продакшена
В settings.py обязательно нужно изменить несколько значений. Секретный ключ и пароли хранятся не в коде, а в переменных окружения, чтобы не попасть в историю Git.
import os
DEBUG = False
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
ALLOWED_HOSTS = ["misol.uz", "www.misol.uz"]
CSRF_TRUSTED_ORIGINS = ["https://misol.uz", "https://www.misol.uz"]
STATIC_URL = "/static/"
STATIC_ROOT = "/srv/misol/staticfiles"
MEDIA_URL = "/media/"
MEDIA_ROOT = "/srv/misol/media"
SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
При DEBUG = False Django сам не раздаёт статику, поэтому мы собираем её в одну папку и отдаём Nginx. SECURE_PROXY_SSL_HEADER сообщает Django, что запрос на самом деле пришёл по HTTPS, иначе защищённые cookie и редиректы будут работать неправильно. Теперь выполните миграции и сбор статики:
sudo -u misol /srv/misol/venv/bin/python /srv/misol/manage.py migrate
sudo -u misol /srv/misol/venv/bin/python /srv/misol/manage.py collectstatic --noinput
Gunicorn как сервис systemd
Запустить Gunicorn вручную в терминале недостаточно: он остановится при закрытии SSH-сессии или перезагрузке сервера. Поэтому оформим его как сервис systemd. Создайте файл /etc/systemd/system/misol.service:
[Unit]
Description=Gunicorn for misol.uz
After=network.target
[Service]
User=misol
Group=www-data
WorkingDirectory=/srv/misol
EnvironmentFile=/srv/misol/.env
RuntimeDirectory=misol
ExecStart=/srv/misol/venv/bin/gunicorn --workers 3 --bind unix:/run/misol/gunicorn.sock --umask 007 config.wsgi:application
Restart=on-failure
[Install]
WantedBy=multi-user.target
RuntimeDirectory создаёт папку /run/misol при каждом запуске, а Group=www-data вместе с --umask 007 дают группе Nginx право записи в сокет. Количество воркеров обычно подбирают по числу ядер процессора; для двухъядерного сервера 3–5 воркеров — хорошая отправная точка. В файле .env лежат строки вида DJANGO_SECRET_KEY=..., и читать его должен только пользователь misol. Включите сервис и проверьте его состояние:
sudo systemctl daemon-reload
sudo systemctl enable --now misol
sudo systemctl status misol
Nginx как обратный прокси
Создайте файл /etc/nginx/sites-available/misol. Статика и медиа отдаются прямо с диска, остальные запросы уходят в сокет Gunicorn:
server {
listen 80;
server_name misol.uz www.misol.uz;
client_max_body_size 20M;
location /static/ {
alias /srv/misol/staticfiles/;
}
location /media/ {
alias /srv/misol/media/;
}
location / {
proxy_pass http://unix:/run/misol/gunicorn.sock;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo ln -s /etc/nginx/sites-available/misol /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d misol.uz -d www.misol.uz
Certbot получит сертификат Let's Encrypt, сам добавит HTTPS-блок в конфигурацию Nginx и настроит автоматическое продление. Перед этим шагом A-запись домена должна указывать на адрес VPS; проверьте это заранее в разделе DNS кабинета sayt.uz.
502 Bad Gateway и другие типичные ошибки
Самая частая ошибка — 502 Bad Gateway: она означает, что Nginx не смог связаться с Gunicorn. Сначала посмотрите логи сервиса командой sudo journalctl -u misol -n 50: чаще всего причина в ошибке импорта, отсутствующей переменной окружения или неверном пути config.wsgi. Если сервис работает, проверьте права на сокет: строка Permission denied в логе ошибок Nginx означает неверную группу или umask. Если статика отдаёт 404, убедитесь, что collectstatic выполнен и пользователь Nginx может читать папку /srv/misol/staticfiles.
Обновление кода всегда идёт в одном и том же порядке: получить новый код, установить зависимости, применить миграции, собрать статику и перезапустить сервис. Если оформить эти шаги небольшим скриптом, обновления школьного сайта будут проходить одинаково и без ошибок.
cd /srv/misol && sudo -u misol git pull
sudo -u misol venv/bin/pip install -r requirements.txt
sudo -u misol venv/bin/python manage.py migrate
sudo -u misol venv/bin/python manage.py collectstatic --noinput
sudo systemctl restart misol