DjangoPythonДеплой

Где разместить Django-приложение: варианты хостинга и деплой

6 мин чтения

Django-приложение запускается локально через python manage.py runserver. Эта команда — для разработки: её собственная документация прямо предупреждает, что встроенный сервер не предназначен для продакшена. В проде Django запускают по-другому, и у него есть несколько специфичных мест, на которых спотыкаются почти все. Разберём, как правильно запустить Django, что настроить и куда задеплоить.

WSGI- или ASGI-сервер

Django по умолчанию — WSGI-приложение, и в проде его запускает WSGI-сервер. Стандартный выбор — gunicorn:

gunicorn myproject.wsgi:application --bind 127.0.0.1:8000 --workers 4

--bind 127.0.0.1 — приложение слушает только localhost, наружу его отдаёт reverse proxy с SSL (как это настроить — в статье про nginx и Let's Encrypt). Число воркеров — ориентир 2 × ядра + 1.

Если в проекте есть асинхронные части или Django Channels (WebSocket), нужен ASGI-сервер — uvicorn или daphne с точкой входа myproject.asgi:application.

Три настройки, без которых прод не работает

Это место, где спотыкается большинство. Перед деплоем в settings обязательно:

  • DEBUG = False. С DEBUG = True в проде Django показывает полную трассировку с переменными окружения и SQL при любой ошибке — это утечка данных и дыра в безопасности. Плюс при DEBUG = True Django сам отдаёт статику, маскируя её неверную настройку.
  • ALLOWED_HOSTS. При DEBUG = False Django отвечает 400 Bad Request на любой запрос, чей Host не перечислен в ALLOWED_HOSTS. Сюда добавляют домен приложения.
  • SECRET_KEY из переменной окружения, а не из кода. По нему подписываются сессии и токены — в репозитории его быть не должно.
import os

DEBUG = False
ALLOWED_HOSTS = ["example.com", "www.example.com"]
SECRET_KEY = os.environ["DJANGO_SECRET_KEY"]

Статика: collectstatic

Django в продакшене не раздаёт статику сам. Команда collectstatic собирает все статические файлы (CSS, JS, админка) в один каталог STATIC_ROOT, откуда их отдаёт reverse proxy или middleware:

python manage.py collectstatic --noinput

Дальше два пути: либо nginx отдаёт STATIC_ROOT напрямую, либо в проект добавляют WhiteNoise — middleware, которое раздаёт статику силами самого Python-процесса. Для небольших проектов WhiteNoise проще: не нужно настраивать nginx под статику. Пропущенный collectstatic — самая частая причина «сайт работает, но без стилей».

Миграции и база

Схему базы накатывают миграциями, а не вручную, и делают это при каждом деплое — до старта приложения:

python manage.py migrate --noinput

Строку подключения к базе удобно держать в одной переменной окружения DATABASE_URL и разбирать пакетом dj-database-url. Подробнее о хранении таких переменных — в статье про секреты и .env.

За reverse proxy: HTTPS и CSRF

Когда Django стоит за reverse proxy, который терминирует TLS, само приложение получает запрос по HTTP и без подсказки считает соединение незащищённым. Из-за этого ломаются редиректы и CSRF-проверка форм. Лечится двумя настройками:

SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https")
CSRF_TRUSTED_ORIGINS = ["https://example.com"]

CSRF_TRUSTED_ORIGINS (Django 4 и новее) обязателен, иначе POST-формы и админка будут отбивать запросы с ошибкой CSRF.

Куда размещать

VPS (Beget, Timeweb, Hetzner). Полный контроль, но Python, gunicorn под supervisor, база, бэкапы, nginx, SSL и collectstatic при каждом релизе настраиваются вручную.

Managed-платформа (PaaS). Деплой по git push: платформа собирает проект, держит процесс живым, рядом managed-база, домен и SSL — автоматически. Миграции и collectstatic вешаются на стадию сборки.

Serverless (облачные функции). Django — классическое долгоживущее приложение с постоянным подключением к базе; serverless с его cold start и лимитами подходит плохо.

Деплой на VPS

git clone https://github.com/user/myproject.git /opt/myproject
cd /opt/myproject
python -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python manage.py migrate --noinput
.venv/bin/python manage.py collectstatic --noinput

Запускать gunicorn руками нельзя — процесс умрёт с закрытием сессии. Нужен супервизор с автозапуском; минимальный systemd-сервис:

[Service]
WorkingDirectory=/opt/myproject
ExecStart=/opt/myproject/.venv/bin/gunicorn myproject.wsgi:application --bind 127.0.0.1:8000 --workers 4
EnvironmentFile=/opt/myproject/.env
Restart=always

Полная настройка supervisor — в статье как держать процесс 24/7.

Деплой на managed-платформе

Подключить репозиторий, задать переменные окружения (DJANGO_SECRET_KEY, DATABASE_URL, ALLOWED_HOSTS) в панели, повесить migrate и collectstatic на стадию сборки и сделать git push. Платформа определит Python-проект, установит зависимости, запустит gunicorn и будет держать процесс живым; managed Postgres подключается через DATABASE_URL, домен и SSL поднимаются автоматически.

Частые грабли

  • DEBUG = True в проде. Любая ошибка показывает внутренности приложения постороннему.
  • Пустой ALLOWED_HOSTS. Сайт отвечает 400 на все запросы.
  • Забытый collectstatic. Страницы открываются без стилей, админка без оформления.
  • Статика 404. collectstatic сделан, но nginx или WhiteNoise не настроены на раздачу STATIC_ROOT.
  • CSRF за прокси. Формы и админка отбивают POST без CSRF_TRUSTED_ORIGINS и SECURE_PROXY_SSL_HEADER.
  • Забытый migrate. Код задеплоен, схема старая — приложение падает на первом обращении к новой таблице.

Чеклист

  • Прод запускается через gunicorn (или ASGI-сервер для Channels), без runserver.
  • DEBUG = False, домен в ALLOWED_HOSTS, SECRET_KEY из окружения.
  • collectstatic выполняется при деплое, статика раздаётся nginx или WhiteNoise.
  • migrate выполняется при деплое, до старта приложения.
  • За reverse proxy заданы SECURE_PROXY_SSL_HEADER и CSRF_TRUSTED_ORIGINS.
  • Процесс под супервизором с автоперезапуском, настроены бэкапы базы.

Managed-платформа закрывает большую часть этого списка по умолчанию: процесс держится сам, reverse proxy с SSL и managed Postgres — из коробки, а migrate и collectstatic вешаются на деплой. На Hostim Django-приложение разворачивается по git push, стек определяется автоматически, а база подключается через DATABASE_URL без ручной настройки сервера.

Читать дальше