Деплой Django: Gunicorn, Nginx и чек-лист продакшна
Как задеплоить проект Django: настройки продакшна, check --deploy, Gunicorn и systemd, Nginx с proxy_pass и статикой, HTTPS через certbot, Docker и Celery.
Содержание · 12 разделов
- Схема деплоя Django: кто за что отвечает
- Чек-лист настроек Django для продакшна
- DEBUG, ALLOWED_HOSTS и SECRET_KEY из окружения
- HTTPS за прокси: CSRF_TRUSTED_ORIGINS и SECURE_PROXY_SSL_HEADER
- Статика: STATIC_ROOT и collectstatic
- Проверка: manage.py check —deploy
- База данных
- Кеш и сессии
- Подготовка сервера
- Gunicorn: запуск Django в продакшне
- Первый запуск
- Основные параметры Gunicorn
- Служба systemd для Gunicorn
- Nginx: proxy_pass и раздача статики
- HTTPS через certbot
- Обновление проекта на сервере
- Docker: тот же стек в контейнерах
- Фоновые задачи: Celery и Redis
- Частые ошибки при деплое Django
- 400 Bad Request на всех страницах
- 502 Bad Gateway
- 504 Gateway Timeout или WORKER TIMEOUT в логах
- Бесконечный редирект после включения HTTPS
- Админка без стилей
- Частые вопросы
- Как запустить Django в продакшне на Windows Server?
- Gunicorn или Uvicorn: что выбрать для Django?
- Как подключить Gunicorn к Nginx через Unix-сокет?
- Как обновить код без простоя сайта?
- Где смотреть ошибки Django на сервере?
- Итоговый чек-лист деплоя
Деплой 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: кто за что отвечает
| Компонент | Роль |
|---|---|
| Nginx | HTTPS, приём соединений, раздача /static/ и /media/, proxy_pass |
| Gunicorn | WSGI-сервер: запускает воркеры с 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, --bind | 127.0.0.1:8000 | Адрес и порт или Unix-сокет (unix:/путь/к.sock) |
-w, --workers | 1 | Число процессов-воркеров |
-k, --worker-class | sync | Тип воркера: sync, gthread, gevent и другие |
--threads | 1 | Потоки в воркере (для класса gthread) |
-t, --timeout | 30 | Через сколько секунд «зависший» воркер перезапускается |
--max-requests | 0 (выключено) | Перезапуск воркера после 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