Django-Admin.ru

Деплой Django: Gunicorn, Nginx и чек-лист продакшна

Как задеплоить проект Django: настройки продакшна, check --deploy, Gunicorn и systemd, Nginx с proxy_pass и статикой, HTTPS через certbot, Docker и Celery.

Редакция Django-Admin.ru
· обновлено · 16 мин чтения

Содержание · 12 разделов
  1. Схема деплоя Django: кто за что отвечает
  2. Чек-лист настроек Django для продакшна
  3. DEBUG, ALLOWED_HOSTS и SECRET_KEY из окружения
  4. HTTPS за прокси: CSRF_TRUSTED_ORIGINS и SECURE_PROXY_SSL_HEADER
  5. Статика: STATIC_ROOT и collectstatic
  6. Проверка: manage.py check —deploy
  7. База данных
  8. Кеш и сессии
  9. Подготовка сервера
  10. Gunicorn: запуск Django в продакшне
  11. Первый запуск
  12. Основные параметры Gunicorn
  13. Служба systemd для Gunicorn
  14. Nginx: proxy_pass и раздача статики
  15. HTTPS через certbot
  16. Обновление проекта на сервере
  17. Docker: тот же стек в контейнерах
  18. Фоновые задачи: Celery и Redis
  19. Частые ошибки при деплое Django
  20. 400 Bad Request на всех страницах
  21. 502 Bad Gateway
  22. 504 Gateway Timeout или WORKER TIMEOUT в логах
  23. Бесконечный редирект после включения HTTPS
  24. Админка без стилей
  25. Частые вопросы
  26. Как запустить Django в продакшне на Windows Server?
  27. Gunicorn или Uvicorn: что выбрать для Django?
  28. Как подключить Gunicorn к Nginx через Unix-сокет?
  29. Как обновить код без простоя сайта?
  30. Где смотреть ошибки Django на сервере?
  31. Итоговый чек-лист деплоя

Деплой Django-проекта на собственный сервер почти всегда строится по одной схеме: Nginx принимает запросы из интернета, сам отдаёт статику и передаёт остальное через proxy_pass в Gunicorn, а Gunicorn запускает несколько процессов с вашим приложением. Встроенный сервер разработки runserver для продакшна не предназначен: он не проходил ни аудит безопасности, ни тесты производительности.

Короткий ответ, что нужно сделать: выключить DEBUG, заполнить ALLOWED_HOSTS, вынести SECRET_KEY в переменную окружения, выполнить collectstatic и manage.py check --deploy, запустить gunicorn config.wsgi:application как службу systemd, настроить Nginx и получить сертификат через certbot. Ниже — каждый шаг с конфигами. Примеры рассчитаны на Ubuntu/Debian, Django 5.2 LTS и 6.x; проект создан командой startproject с пакетом настроек config.

Схема деплоя Django: кто за что отвечает

КомпонентРоль
NginxHTTPS, приём соединений, раздача /static/ и /media/, proxy_pass
GunicornWSGI-сервер: запускает воркеры с Django и передаёт им запросы
DjangoОбрабатывает запросы, работает с базой данных
systemdЗапускает Gunicorn при старте сервера и перезапускает при падении
certbotВыпускает и продлевает бесплатный сертификат Let’s Encrypt
CeleryОпционально: фоновые и периодические задачи

Такое разделение важно: медленные клиенты и раздачу файлов берёт на себя Nginx, а воркеры Gunicorn заняты только Python-кодом.

Важно: Gunicorn работает только на Unix-системах (Linux, macOS). На Windows он не запускается — при импорте падает с ModuleNotFoundError: No module named 'fcntl'. Для разработки на Windows используйте runserver, а боевой сервер разворачивайте на Linux, в WSL или в Docker-контейнере.

Чек-лист настроек Django для продакшна

Начинаем с settings.py: большинство проблем после деплоя — это забытые настройки, а не ошибки в Nginx.

DEBUG, ALLOWED_HOSTS и SECRET_KEY из окружения

Секреты и всё, что отличается между машинами, читайте из переменных окружения:

# config/settings.py
import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parent.parent

SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]
DEBUG = os.environ.get("DJANGO_DEBUG", "0") == "1"
ALLOWED_HOSTS = [
    host for host in os.environ.get("DJANGO_ALLOWED_HOSTS", "").split(",") if host
]
  • DEBUG = False обязательно: с включённым режимом отладки любая ошибка показывает посетителю трейсбек, фрагменты кода и настройки.
  • ALLOWED_HOSTS — список доменов, на которые отвечает сайт, например example.ru,www.example.ru. При DEBUG = False и пустом списке Django отвечает 400 Bad Request на любой запрос.
  • SECRET_KEY используется для подписи сессий, токенов сброса пароля и CSRF. Ключ из startproject начинается с django-insecure- — в продакшне сгенерируйте новый и не храните его в Git.

Сгенерировать ключ:

python3 -c "import secrets; print(secrets.token_urlsafe(50))"

token_urlsafe выдаёт только буквы, цифры, - и _ — такую строку безопасно класть в .env и unit-файлы без экранирования. Для удобной работы с .env в разработке есть пакеты django-environ и python-decouple, но в продакшне переменные можно передать и через systemd (см. ниже).

HTTPS за прокси: CSRF_TRUSTED_ORIGINS и SECURE_PROXY_SSL_HEADER

Gunicorn получает запросы от Nginx по обычному HTTP, поэтому Django не знает, что пользователь пришёл по HTTPS. Это чинит заголовок X-Forwarded-Proto:

# config/settings.py
CSRF_TRUSTED_ORIGINS = ["https://example.ru", "https://www.example.ru"]

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
SECURE_SSL_REDIRECT = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
SECURE_HSTS_SECONDS = 3600  # начните с малого, потом увеличьте
  • CSRF_TRUSTED_ORIGINS указывается со схемой (https://). Без него формы и вход в админку Django за прокси часто падают с «CSRF verification failed».
  • SECURE_PROXY_SSL_HEADER безопасен, только если прокси всегда перезаписывает X-Forwarded-Proto — в конфиге Nginx ниже это делает строка proxy_set_header X-Forwarded-Proto $scheme;. Без неё клиент мог бы подделать заголовок сам.
  • SECURE_HSTS_SECONDS заставляет браузер ходить на сайт только по HTTPS. Ошибка с HSTS необратима на срок действия заголовка, поэтому увеличивайте значение (до года, 31536000) только после проверки, что HTTPS работает везде.

Статика: STATIC_ROOT и collectstatic

При DEBUG = False Django не раздаёт статические файлы — их собирают в одну папку и отдаёт Nginx:

# config/settings.py
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

MEDIA_URL = "media/"
MEDIA_ROOT = BASE_DIR / "media"
python manage.py collectstatic --noinput

collectstatic копирует в STATIC_ROOT статику всех приложений, включая стили админки. Если после деплоя админка открылась «голой», без CSS, — почти наверняка забыт этот шаг или путь в Nginx не совпадает со STATIC_ROOT.

Альтернатива для простых проектов и PaaS — пакет WhiteNoise: он отдаёт статику прямо из Django, его middleware ставится сразу после SecurityMiddleware. Как устроен список middleware и почему важен порядок — в руководстве по middleware.

Проверка: manage.py check —deploy

Django умеет сам проверить настройки безопасности:

python manage.py check --deploy

На свежем проекте вывод выглядит примерно так (сокращено):

WARNINGS:
?: (security.W004) You have not set a value for the SECURE_HSTS_SECONDS setting. ...
?: (security.W008) Your SECURE_SSL_REDIRECT setting is not set to True. ...
?: (security.W009) Your SECRET_KEY has less than 50 characters, ... prefixed with 'django-insecure-' ...
?: (security.W012) SESSION_COOKIE_SECURE is not set to True. ...
?: (security.W016) ... you have not set CSRF_COOKIE_SECURE to True. ...
?: (security.W018) You should not have DEBUG set to True in deployment.
?: (security.W020) ALLOWED_HOSTS must not be empty in deployment.

После настроек выше останутся только W005 и W021 — про SECURE_HSTS_INCLUDE_SUBDOMAINS и SECURE_HSTS_PRELOAD. Их включают осознанно, когда все поддомены тоже работают по HTTPS. Запускайте проверку с теми же переменными окружения, что и на сервере, иначе она проверит не те значения.

В проекте, созданном на Django 6.1, check --deploy дополнительно выдаёт ошибку mail.E001: шаблон settings.py содержит настройку MAILERS с консольным бэкендом, который печатает письма в терминал. В продакшне подключите SMTP-сервер:

# config/settings.py (Django 6.1+)
MAILERS = {
    "default": {
        "BACKEND": "django.core.mail.backends.smtp.EmailBackend",
        "OPTIONS": {
            "host": os.environ["EMAIL_HOST"],
            "port": 587,
            "username": os.environ["EMAIL_HOST_USER"],
            "password": os.environ["EMAIL_HOST_PASSWORD"],
            "use_tls": True,
        },
    },
}

В Django 5.2 и 6.0 почту настраивают прежними параметрами EMAIL_BACKEND, EMAIL_HOST и другими EMAIL_*; в 6.1 они ещё работают, но объявлены устаревшими.

База данных

SQLite подходит для небольших сайтов с редкой записью, но для продакшна обычно берут PostgreSQL: несколько воркеров Gunicorn пишут в базу параллельно, а SQLite блокирует файл на запись. Параметры подключения (DATABASES) тоже передавайте через окружение. Миграции на сервере применяются той же командой, что и локально: python manage.py migrate.

Кеш и сессии

Кеш по умолчанию (LocMemCache) живёт в памяти каждого процесса: у нескольких воркеров Gunicorn он свой и не переживает перезапуск службы. В продакшне кеш и сессии обычно переносят в Redis — его же затем используют как брокер Celery. Настройка CACHES, cache_page и хранение сессий разобраны в статье «Redis в Django: кеширование и сессии».

Подготовка сервера

Пример для Ubuntu: код лежит в /srv/mysite, приложение работает от отдельного пользователя deploy.

sudo apt update
sudo apt install python3-venv python3-pip nginx git

sudo mkdir -p /srv/mysite
sudo chown deploy:deploy /srv/mysite
cd /srv/mysite
git clone https://github.com/you/mysite.git .

python3 -m venv .venv
source .venv/bin/activate
python -m pip install -r requirements.txt gunicorn

Зачем нужно отдельное окружение и как с ним работать — в статье про виртуальное окружение Python.

Переменные окружения положите в файл /srv/mysite/.env (его не коммитят, права — только для владельца):

# /srv/mysite/.env
DJANGO_SECRET_KEY=сгенерированный-ключ
DJANGO_ALLOWED_HOSTS=example.ru,www.example.ru
DJANGO_DEBUG=0
chmod 600 /srv/mysite/.env

Django сам этот файл не читает — его подгрузит systemd через EnvironmentFile. Для ручных команд экспортируйте переменные в текущую сессию:

set -a; source .env; set +a
python manage.py migrate
python manage.py collectstatic --noinput
python manage.py check --deploy

Gunicorn: запуск Django в продакшне

Первый запуск

Gunicorn нужно указать WSGI-приложение в формате модуль:переменная. Модуль config/wsgi.py создаётся startproject автоматически:

gunicorn config.wsgi:application --bind 127.0.0.1:8000 --workers 3

Проверьте с этого же сервера: curl -I http://127.0.0.1:8000/ должен вернуть ответ Django. Если в ответ 400 Bad Request — добавьте 127.0.0.1 в ALLOWED_HOSTS на время проверки или передайте заголовок Host с вашим доменом. Статики на этом этапе ещё нет — её отдаст Nginx.

Основные параметры Gunicorn

ПараметрЗначение по умолчаниюЗачем
-b, --bind127.0.0.1:8000Адрес и порт или Unix-сокет (unix:/путь/к.sock)
-w, --workers1Число процессов-воркеров
-k, --worker-classsyncТип воркера: sync, gthread, gevent и другие
--threads1Потоки в воркере (для класса gthread)
-t, --timeout30Через сколько секунд «зависший» воркер перезапускается
--max-requests0 (выключено)Перезапуск воркера после N запросов — от утечек памяти
--access-logfileнетЖурнал запросов; - — в stdout
--error-logfile-Журнал ошибок

Сколько ставить воркеров: Gunicorn рекомендует начинать с (2 × число ядер) + 1 и подбирать значение под нагрузку; обычно хватает 4–12 воркеров даже при большом трафике. Для сервера с 2 ядрами это 5 воркеров. Каждый воркер — отдельный процесс с копией приложения в памяти, поэтому на маленьком VPS упираются обычно в RAM, а не в CPU.

Параметры можно вынести в файл gunicorn.conf.py в рабочей директории — Gunicorn читает его автоматически:

# /srv/mysite/gunicorn.conf.py
bind = "127.0.0.1:8000"
workers = 3
timeout = 30
accesslog = "-"
errorlog = "-"

Служба systemd для Gunicorn

Запускать Gunicorn руками в screen или tmux — путь к падению сайта после перезагрузки. Создайте unit-файл:

# /etc/systemd/system/gunicorn-mysite.service
[Unit]
Description=Gunicorn for mysite
After=network.target

[Service]
User=deploy
Group=www-data
WorkingDirectory=/srv/mysite
EnvironmentFile=/srv/mysite/.env
ExecStart=/srv/mysite/.venv/bin/gunicorn config.wsgi:application \
    --bind 127.0.0.1:8000 \
    --workers 3 \
    --access-logfile - \
    --error-logfile -
Restart=on-failure

[Install]
WantedBy=multi-user.target

Включите и запустите службу:

sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn-mysite
sudo systemctl status gunicorn-mysite
journalctl -u gunicorn-mysite -f   # логи в реальном времени

Путь к gunicorn указан внутри .venv, поэтому активировать окружение в unit-файле не нужно. Логи Gunicorn и вывод print попадают в журнал systemd.

Nginx: proxy_pass и раздача статики

Nginx встаёт перед Gunicorn: принимает соединения на 80/443 порту, сам отдаёт файлы и проксирует остальные запросы.

# /etc/nginx/sites-available/mysite
server {
    listen 80;
    server_name example.ru www.example.ru;

    client_max_body_size 20M;

    location /static/ {
        alias /srv/mysite/staticfiles/;
    }

    location /media/ {
        alias /srv/mysite/media/;
    }

    location / {
        proxy_pass http://127.0.0.1:8000;
        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;
    }
}

Что здесь важно:

  • alias для /static/ указывает на STATIC_ROOT; слеш в конце пути у location и alias должен совпадать.
  • proxy_set_header Host $host передаёт Django исходный домен — именно его проверяет ALLOWED_HOSTS.
  • X-Forwarded-Proto $scheme нужен для SECURE_PROXY_SSL_HEADER.
  • client_max_body_size ограничивает размер загрузки; по умолчанию у Nginx это 1 МБ, и большие файлы получат ошибку 413.

Активируйте сайт, проверьте синтаксис и перечитайте конфигурацию:

sudo ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

Пользователь Nginx (www-data в Ubuntu) должен иметь право читать файлы в staticfiles и media и проходить по каталогам до них. Если статика отдаёт 403 — проверьте права на всю цепочку каталогов, начиная с /srv.

HTTPS через certbot

Бесплатный сертификат Let’s Encrypt проще всего получить через certbot с плагином для Nginx. Перед этим DNS-записи домена должны указывать на сервер, а порт 80 — быть открыт.

sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.ru -d www.example.ru

Certbot сам допишет в конфиг блок listen 443 ssl, пути к сертификату и, по вашему выбору, редирект с HTTP на HTTPS. Разработчики certbot также предлагают установку через snap — команды после установки те же.

Сертификат Let’s Encrypt сейчас действует 90 дней; с февраля 2027 года срок сократится до 64 дней, а с февраля 2028-го — до 45. Продлевать вручную не нужно: пакет certbot при установке настраивает таймер systemd или задание cron, которое регулярно запускает certbot renew; сертификат обновляется заранее, у 90-дневного — примерно за 30 дней до окончания. Проверить, что продление сработает, можно так:

sudo certbot renew --dry-run

После включения HTTPS ещё раз откройте сайт, войдите в админку и отправьте любую форму — так вы проверите CSRF_TRUSTED_ORIGINS и cookie-настройки.

Обновление проекта на сервере

Типовой порядок выкладки новой версии:

cd /srv/mysite
git pull
source .venv/bin/activate
set -a; source .env; set +a
python -m pip install -r requirements.txt
python manage.py migrate
python manage.py collectstatic --noinput
sudo systemctl restart gunicorn-mysite

Nginx при обновлении кода перезапускать не нужно — только при изменении его конфигурации.

Docker: тот же стек в контейнерах

В Docker схема не меняется: контейнер с Django запускает Gunicorn, перед ним стоит Nginx (или другой обратный прокси вроде Traefik), а база, Redis и Celery — отдельные сервисы в docker-compose.yml. Минимальный Dockerfile:

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

WORKDIR /app
COPY requirements.txt .
# gunicorn должен быть в requirements.txt
RUN pip install --no-cache-dir -r requirements.txt
COPY . .

CMD ["gunicorn", "config.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "3"]

Внутри контейнера Gunicorn слушает 0.0.0.0, иначе до него не достучится прокси из соседнего контейнера. Секреты передаются через environment или env_file в compose, а не копируются в образ. migrate и collectstatic выполняют при старте контейнера или отдельной командой при деплое: на этапе сборки образа обычно нет переменных окружения продакшна.

Фоновые задачи: Celery и Redis

Отправку писем, генерацию отчётов и обращения к внешним API не стоит выполнять внутри запроса — воркер Gunicorn будет занят, а при превышении --timeout его перезапустят. Такие задачи выносят в Celery с брокером Redis. На сервере Celery-воркер и планировщик beat запускаются как ещё две службы systemd рядом с Gunicorn.

Как подключить Celery к Django, писать задачи и настраивать периодический запуск — в отдельной статье «Celery в Django».

Частые ошибки при деплое Django

400 Bad Request на всех страницах

DEBUG = False, а домена нет в ALLOWED_HOSTS. Проверьте, что переменная окружения действительно попала в процесс (unit-файл ссылается на правильный EnvironmentFile), и что Nginx передаёт Host $host.

502 Bad Gateway

Nginx не может достучаться до Gunicorn: служба не запущена, упала при старте или слушает другой адрес. Смотрите systemctl status gunicorn-mysite и journalctl -u gunicorn-mysite. Частая причина — исключение при импорте настроек, например KeyError: 'DJANGO_SECRET_KEY', когда переменная не задана.

504 Gateway Timeout или WORKER TIMEOUT в логах

Запрос обрабатывается дольше, чем --timeout у Gunicorn или proxy_read_timeout у Nginx (по умолчанию 60 секунд). Увеличивать таймауты — временная мера: ищите медленные запросы к базе (проблема N+1) и выносите долгую работу в Celery.

Бесконечный редирект после включения HTTPS

SECURE_SSL_REDIRECT = True, но Django не видит HTTPS: не задан SECURE_PROXY_SSL_HEADER или Nginx не передаёт X-Forwarded-Proto. Django перенаправляет на HTTPS, Nginx снова проксирует запрос как HTTP — и так по кругу.

Админка без стилей

Не выполнен collectstatic, не задан STATIC_ROOT или alias в Nginx указывает не туда. Откройте URL любого CSS-файла админки напрямую и посмотрите код ответа.

Частые вопросы

Как запустить Django в продакшне на Windows Server?

Gunicorn на Windows не работает, поэтому берут другой сервер приложений. Для WSGI подходит Waitress — он написан на чистом Python и запускается на Windows: waitress-serve --listen=127.0.0.1:8000 config.wsgi:application. Для ASGI-проекта — Uvicorn. Перед ними так же ставят обратный прокси (IIS или Nginx), который отдаёт статику и держит HTTPS. Если есть выбор, проще развернуть Linux в WSL, Docker или на отдельном VPS.

Gunicorn или Uvicorn: что выбрать для Django?

Для обычного синхронного проекта достаточно Gunicorn с config.wsgi:application. Если в проекте асинхронные view, долгие соединения или WebSocket через Channels, нужен ASGI-сервер — например, Uvicorn с несколькими процессами:

uvicorn config.asgi:application --host 127.0.0.1 --port 8000 --workers 4

Nginx, статика и systemd настраиваются так же, как для Gunicorn.

Как подключить Gunicorn к Nginx через Unix-сокет?

Вместо порта укажите путь к сокету и передайте его же в proxy_pass. Сокет быстрее TCP на одной машине и недоступен из сети:

gunicorn config.wsgi:application --bind unix:/run/mysite/gunicorn.sock
proxy_pass http://unix:/run/mysite/gunicorn.sock;

Каталог /run/mysite создайте директивой RuntimeDirectory=mysite в unit-файле, а пользователю Nginx дайте право на запись в сокет — например, через общую группу www-data.

Как обновить код без простоя сайта?

Вместо restart отправьте Gunicorn сигнал HUP: он запустит новые воркеры с обновлённым кодом (если не включён preload_app) и плавно завершит старые, дав им дообработать текущие запросы. Добавьте в секцию [Service] строку ExecReload=/bin/kill -s HUP $MAINPID и выкладывайте обновление командой sudo systemctl reload gunicorn-mysite. Миграции, которые удаляют или переименовывают поля, всё равно выкатывайте в два релиза: старый код не должен обращаться к уже удалённым колонкам.

Где смотреть ошибки Django на сервере?

Трейсбеки и вывод Gunicorn пишутся в журнал службы: journalctl -u gunicorn-mysite -n 100. Ошибки проксирования (502, 504, 413) — в /var/log/nginx/error.log. Чтобы узнавать о падениях сразу, заполните ADMINS и настройте отправку почты: при DEBUG = False Django пришлёт администраторам письмо с трейсбеком каждой ошибки 500. Для продакшна удобнее сервис отслеживания ошибок, например Sentry.

Итоговый чек-лист деплоя

  • DEBUG = False, ALLOWED_HOSTS заполнен
  • SECRET_KEY и пароли к базе — из переменных окружения, не в Git
  • CSRF_TRUSTED_ORIGINS со схемой https://
  • SECURE_PROXY_SSL_HEADER, SECURE_SSL_REDIRECT, secure-cookie включены
  • STATIC_ROOT задан, collectstatic выполнен
  • python manage.py check --deploy без критичных предупреждений
  • Gunicorn работает как служба systemd с Restart=on-failure
  • Nginx отдаёт /static/ и /media/, проксирует остальное с заголовками Host и X-Forwarded-Proto
  • сертификат выпущен, certbot renew --dry-run проходит
  • админка спрятана и защищена — см. защиту URL админки

Деплой Django сводится к трём слоям: правильные настройки, Gunicorn под управлением systemd и Nginx с HTTPS перед ним. Остальные статьи раздела «Деплой и инфраструктура» разбирают отдельные части стека, начиная с фоновых задач на Celery. Если вы только начинаете путь с фреймворком, вернитесь к Django для начинающих. Полный список настроек безопасности — в официальном чек-листе развёртывания Django.

  • #Django
  • #Деплой
  • #Gunicorn
  • #Nginx
  • #HTTPS
  • #systemd
↑ ↓ выборEnter открытьEsc закрыть