Как выложить сайт на Django без своего сервера

Django локально поднимается одной командой — python manage.py runserver, — а публикация в интернет превращается в отдельную задачу: сервер, Python с пакетами, gunicorn, nginx перед ним, вынесенная статика, база, миграции, сертификат. Вместе это вечер работы и ещё один — на разбор белого экрана вместо сайта. На Dockbox эту часть берёт платформа: вы загружаете код, она определяет Python-проект, ставит зависимости по requirements.txt, запускает приложение и выдаёт публичную HTTPS-ссылку. Ниже — что подготовить в проекте, чтобы всё заработало с первого раза.

Почему деплой Django превращается в отдельный проект

Привычный путь — длинный чек-лист: арендовать сервер, собрать нужную версию Python, пройти pip install -r requirements.txt, запустить gunicorn, поставить перед ним nginx, отдельно отдать статику, выпустить сертификат, поднять базу и не забыть про миграции.

runserver для продакшена не годится. Это отладочный сервер без обработки внешних запросов. В боевом режиме приложение поднимает gunicorn (или uvicorn для ASGI), а перед ним встаёт nginx — иначе сайт не отдаёт HTTPS.

DEBUG=True включён по умолчанию. На ошибке Django показывает посетителю трассировку, пути к файлам и куски настроек.

Статика живёт отдельно. В продакшене CSS, скрипты и картинки отдаёт web-сервер, а не Django: не собрать их заранее — стили на опубликованном сайте исчезнут.

Порт и перезапуск. Приложение должно слушать выданный ему порт и подниматься само после падения. Платформа деплоя снимает всю эту часть и отдаёт публичную ссылку.

Шаг 1. requirements.txt в корне, venv — не в архив

requirements.txt в корне проекта — это признак, по которому платформа понимает, что перед ней Python-проект, и по которому ставит зависимости при сборке. В списке обязательны сам Django и gunicorn — сервер, который держит приложение. Типичный набор:

Django==5.0.6
gunicorn==22.0.0
psycopg[binary]==3.1.19

Если проект работает с картинками, очередями или внешними API, добавьте Pillow, celery, httpx — по факту импортов. Папку venv в архив не кладите: платформа поставит те же библиотеки сама, а лишние мегабайты замедлят загрузку.

Шаг 2. Секреты — в переменные окружения, а не в settings.py

Плохой вариант — так Django создаётся по умолчанию:

SECRET_KEY = "django-insecure-1a2b3c4d5e"
DATABASE_URL = "postgres://app:secret@db:5432/app"

Хороший вариант — значения приходят извне, а код только читает их:

import os
SECRET_KEY = os.environ["SECRET_KEY"]
DATABASE_URL = os.environ["DATABASE_URL"]

SECRET_KEY — ключ, которым Django подписывает сессии и токены CSRF: с ним можно подделать чужую сессию. Строка подключения к базе — прямой пароль от ваших данных. Код вы будете показывать или заливать в Git, поэтому секрет в коде = секрет утёк. На платформе значения задаются отдельно и подставляются переменными окружения:

SECRET_KEY=***
DEBUG=False
ALLOWED_HOSTS=example.com
DATABASE_URL=postgres://app:***@db:5432/app

В панели видны имена переменных, а значения — никому.

Шаг 3. DEBUG=False и свой домен в ALLOWED_HOSTS

Отладку выключите до публикации. DEBUG=True в продакшене показывает посетителю стек вызовов и содержимое настроек, а иногда и строку подключения к базе.

Пустой ALLOWED_HOSTS — это ошибка 400. При выключенном DEBUG Django раздаёт запросы только тем доменам, что там перечислены. Самый надёжный вариант — читать список из окружения:

ALLOWED_HOSTS = [h for h in os.environ.get("ALLOWED_HOSTS", "").split(",") if h]

На первом запуске можно временно указать ["*"], чтобы убедиться, что сайт открывается, а затем сузить список до домена. В продакшене "*" оставлять нельзя: так Django принимает запросы с любым заголовком Host.

Шаг 4. Порт — из PORT, запуск — под gunicorn

Порт не хардкодьте. Платформа передаёт нужный порт через переменную PORT, и это единственное место, куда приходит трафик. Жёстко прописанный 127.0.0.1:8000 — причина номер один, по которой приложение работает, а ссылка не открывается.

Плохой вариант:

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

Рабочий — слушать 0.0.0.0 и брать порт из окружения:

gunicorn config.wsgi:application --bind 0.0.0.0:$PORT --workers 3

Для ASGI-проекта вместо gunicorn укажите uvicorn с теми же параметрами.

Шаг 5. Статика не появится сама: STATIC_ROOT и collectstatic

Django не раздаёт статику в продакшене: CSS, скрипты и картинки отдаёт web-сервер, и собрать их нужно в одну папку. В settings.py задаётся, куда:

STATIC_URL = "/static/"
STATIC_ROOT = BASE_DIR / "staticfiles"

Сборка выполняется при каждом деплое, иначе новый стиль в коде не доедет до сайта:

python manage.py collectstatic --noinput

Флаг --noinput обязателен: без него команда спрашивает про перезапись файлов и ждёт ответа, которого в неинтерактивном запуске не будет. Загруженные пользователями файлы (MEDIA) так не собираются — им нужно постоянное хранилище.

Шаг 6. База, миграции и загрузка кода

Базу можно поднять рядом с приложением — сервисом в docker-compose внутри проекта.

services:
  app:
    build: .
  db:
    image: postgres:16
    volumes:
      - db_data:/var/lib/postgresql/data
volumes:
  db_data:

Именованный том у базы обязателен. Без него данные обнуляются при первом же обновлении кода. Второй вариант — внешняя база: её строку подключения указываете в DATABASE_URL.

Миграции выполняются при старте. Терминала внутри проекта нет, поэтому их прогоняют из стартовой команды до запуска сервера:

python manage.py migrate --noinput && python manage.py collectstatic --noinput && gunicorn config.wsgi:application --bind 0.0.0.0:$PORT

Файлы пользователей — в постоянное хранилище: всё, что приложение пишет внутрь контейнера, при обновлении кода пересоздаётся.

Дальше проект попадает на платформу ZIP-архивом или импортом репозитория GitHub — приватный тоже подойдёт. Платформа сама ставит зависимости, собирает и запускает приложение, а логи сборки видны в панели в реальном времени. Собственный домен подключается в настройках проекта.

Сколько это стоит и как работают контейнеры

Администрировать сервер не нужно — контейнеры работают постоянно, без ограничения по часам и без «засыпания». Упавший контейнер платформа перезапускает сама.

Starter — 990 ₽/мес. 1 vCPU, 2 GB RAM, 20 GB SSD, один проект. Первый месяц бесплатный. Для одного Django-сайта этого достаточно, и лимита по времени работы нет.

Pro — 2 490 ₽/мес. 2 vCPU, 4 GB RAM, до пяти проектов — если рядом живут база, очередь задач и воркер.

Team — 4 990 ₽/мес — для команд, которым нужен запас ресурсов и SLA.

Тарифы и подробности — на dockbox.host.

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

Приложение работает, а ссылка не открывается — почему?

Почти всегда дело в порте: gunicorn слушает 127.0.0.1:8000 вместо порта из PORT и адреса 0.0.0.0. Платформа направляет трафик только на выданный ею порт, поэтому жёсткий номер — причина номер один. Вторая — ответ 400, если в ALLOWED_HOSTS нет вашего домена.

Что делать с SECRET_KEY и строкой подключения к базе?

Не хранить их в settings.py, а перенести в переменные окружения проекта — SECRET_KEY и DATABASE_URL: платформа держит их зашифрованными и подставляет при запуске, а код читает через os.environ. Если SECRET_KEY уже лежал в репозитории, считайте его утёкшим и сгенерируйте новый.

Как прогнать миграции после деплоя?

Терминала внутри проекта нет, поэтому миграции выполняются при старте: python manage.py migrate --noinput в начале стартовой команды, перед gunicorn. Так таблицы обновляются на каждом деплое, а не при первом визите пользователя.

Стили и картинки пропали на опубликованном сайте — в чём дело?

Django не раздаёт статику в продакшене: её собирают в STATIC_ROOT командой python manage.py collectstatic --noinput при каждом деплое. Локально всё видно, потому что в разработке файлы отдаёт сам Django, — этой разницей объясняются почти все «слетевшие» вёрстки.

Сайт отдаёт 500 сразу после деплоя — с чего начать?

Три частые причины: не заданы переменные окружения (SECRET_KEY, DATABASE_URL), база недоступна или миграции не прошли, либо код остался с DEBUG=True. Настоящая ошибка написана в логах запуска — откройте их в панели.