Как развернуть Django-приложение на Ubuntu с Gunicorn, PostgreSQL, Nginx и SSL 

Валерий Волков

Время прочтения 38 минут

Для production-развертывания Django одного python manage.py runserver недостаточно. В рабочей схеме Django отвечает за логику приложения, PostgreSQL хранит данные, Gunicorn запускает Python-приложение, systemd управляет процессом, а Nginx принимает внешний HTTP/HTTPS-трафик и раздает static/media.

В этом руководстве мы:

  • Подготовили Ubuntu и установили Python, venv и системные зависимости;
  • Перенесли Django-проект на VPS;
  • Создали виртуальное окружение и установили зависимости;
  • Установили PostgreSQL, создали отдельную базу и пользователя;
  • Вынесли SECRET_KEY, пароль базы, DEBUG и ALLOWED_HOSTS в .env;
  • Применили миграции и собрали static через collectstatic;
  • Запустили Django через Gunicorn на 127.0.0.1:8000;
  • Оформили Gunicorn как systemd-сервис с автозапуском;
  • Настроили Nginx как reverse proxy;
  • Передали static и media напрямую Nginx;
  • Подключили домен;
  • Получили TLS-сертификат через Certbot и включили HTTPS;
  • Разобрали диагностику 502 Bad Gateway, проблем PostgreSQL, static/media и DisallowedHost.

В результате Django-приложение работает как нормальный production-сервис: поднимается после перезагрузки VPS, использует отдельную PostgreSQL-базу, не хранит секреты прямо в коде и доступно пользователям по HTTPS через Nginx.

От пустого VPS до production-Django: что мы вообще строим

Добро пожаловать!

Сегодня мы снова возвращаемся к кодингу и командам, но задача стоит уже посерьезнее.

На этот раз будем разворачивать Django-приложение на Ubuntu и постепенно доведем его от обычного проекта до нормальной production-схемы.

Для начала коротко разберемся с самим Django.

Django — это веб-фреймворк для Python, то есть готовый набор инструментов для разработки сайтов, API и других веб-приложений.

Он уже умеет многое из того, что иначе пришлось бы собирать вручную:

  • Обрабатывать URL и запросы пользователей;
  • Работать с базой данных через ORM;
  • Использовать шаблоны;
  • Создавать административную панель;
  • Управлять пользователями и авторизацией;
  • Подключать middleware и другие компоненты приложения.

Проще говоря, Python дает нам язык, а Django — готовый каркас, на котором можно строить веб-приложение.

Например:

Но сам факт того, что проект написан на Django и запускается локально, еще не делает его готовым к публичной работе.

Наша цель — пройти весь путь от пустого Ubuntu-сервера до схемы, которая уже похожа на нормальное production-развертывание.

Допустим, у нас есть готовый Django-проект.

На локальном компьютере он может прекрасно запускаться командой: python manage.py runserver

И на первый взгляд возникает вполне логичный вопрос: «Если сайт уже работает, зачем вообще добавлять PostgreSQL, Gunicorn, systemd, Nginx и SSL?»

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

На своем компьютере нам важно быстро запустить проект, увидеть изменения и продолжить писать код.

На production-сервере уже хочется другого. Чтобы:

  • Приложение переживало перезагрузку VPS;
  • База данных нормально хранила рабочие данные;
  • Сайт открывался через домен;
  • Соединение было защищено HTTPS;
  • Static и media корректно отдавались пользователю;
  • Приложение автоматически возвращалось в работу после сбоя;
  • Наружу не приходилось выставлять внутренний порт Django.

Поэтому итоговый стек получится многослойным.

Но пугаться количества компонентов не стоит. У каждого здесь одна вполне понятная роль, и дальше мы будем добавлять их постепенно.

Почему python manage.py runserver недостаточно

Начнем с привычного: python manage.py runserver

Эта команда запускает встроенный development server Django.

И ключевое слово здесь именно development.

Он создавался в первую очередь для разработки: быстро поднять проект, посмотреть страницу, проверить код и продолжить работу.

Например:

Для локальной машины это прекрасно.

Но на публичном сервере требования меняются.

Нам уже нужно принимать реальные запросы пользователей, нормально управлять процессом приложения и не зависеть от открытой SSH-сессии.

Представим, что мы просто зашли на VPS и выполнили: python manage.py runserver 0.0.0.0:8000

Сайт может открыться.

Но затем мы закрыли терминал, процесс завершился — и приложение исчезло вместе с ним.

Даже если запустить его каким-нибудь обходным способом в фоне, остается другой вопрос: кто поднимет Django после перезагрузки сервера или падения процесса?

Кроме того, сам Django прямо рассматривает runserver как инструмент разработки, а не production-сервер.

Поэтому в нашей схеме его место займет Gunicorn.

Условно:

  • Runserver → удобно разработчику
  • Gunicorn → предназначен для запуска Python web-приложения в production

Но и Gunicorn не будет решать вообще все задачи самостоятельно.

И здесь как раз становится понятно, зачем нам понадобятся остальные компоненты.

Кто за что отвечает: Django, Gunicorn, PostgreSQL, systemd и Nginx

Посмотрим на весь стек как на команду, где у каждого участника своя работа.

Django — про него мы уже говорили, это само приложение.

Именно здесь находится:

  • Бизнес-логика;
  • Маршруты;
  • Модели;
  • Шаблоны;
  • API;
  • Административная панель;
  • Работа с данными.

То есть Django отвечает на вопрос: «Что должен сделать сайт, когда пользователь отправил запрос?»

Но сам по себе framework не обязан заниматься всеми инфраструктурными задачами вокруг этого запроса.

Далее у нас идёт PostgreSQL. Он будет хранить постоянные данные приложения.

Например:

  • Пользователи
  • Заказы
  • Публикации
  • Настройки
  • Комментарии

Django обращается к PostgreSQL через ORM, а база уже отвечает за реальное хранение этих записей.

Вместо схемы:

Мы придем к:

Почему для production мы будем использовать PostgreSQL, а не оставим стандартный SQLite, отдельно разберем чуть позже.

После идёт Gunicorn. Он становится промежуточным слоем между внешним веб-сервером и Django.

Именно он будет запускать наше Python-приложение и принимать запросы, которые затем обрабатывает Django.

Если провести простую аналогию, Django можно представить как кухню ресторана, где реально готовится заказ.

Gunicorn в таком случае — официант, который принимает заказ и относит его на кухню.

Но посетителей мы всё равно не будем отправлять напрямую к официанту через черный вход.

Перед ним появится еще один слой — Nginx.

Nginx станет внешней точкой входа.

Он будет принимать запросы пользователей на стандартных портах:

  • 80
  • 443

А затем передавать динамические запросы Gunicorn.

Кроме того, Nginx возьмет на себя:

  • Работу с доменом;
  • HTTPS;
  • Static-файлы;
  • Media-файлы;
  • Proxy-заголовки.

То есть:

При этом Gunicorn можно вообще не выставлять напрямую в интернет.

Остается еще один участник — systemd.

Его задача не обрабатывать HTTP-запросы и не работать с базой.

Он будет следить за самим процессом Gunicorn.

Например:

Или Gunicorn неожиданно остановился — systemd позволяет централизованно управлять сервисом, смотреть его состояние и логи.

В итоге роли можно собрать в небольшую таблицу:

Компонент За что отвечает 
Django Логика приложения 
PostgreSQL Постоянные данные 
Gunicorn Запуск Django как production web-приложения 
systemd Управление процессом Gunicorn и автозапуск 
Nginx Внешний HTTP/HTTPS-трафик, proxy, static и media 

Если смотреть на компоненты по отдельности, стек кажется большим.

Но стоит разложить обязанности — всё становится куда понятнее.

Как будет выглядеть итоговая схема

Теперь соберем все части в одну цепочку.

Пользователь открывает: https://example.com

Запрос приходит в Nginx.

Если это обычный запрос к приложению, Nginx передает его Gunicorn:

Django выполняет нужную логику, при необходимости читает или изменяет данные в PostgreSQL и возвращает ответ обратно по той же цепочке.

Но со static и media путь будет немного другим.

Например, запрос к: /static/css/style.css необязательно вообще отправлять в Django.

Nginx сможет отдать такой файл самостоятельно:

То же самое позже настроим для пользовательских media.

А над процессом Gunicorn будет стоять systemd:

Именно к такой архитектуре мы будем двигаться постепенно.

Не будем устанавливать все компоненты одной огромной простыней команд. Сначала подготовим Ubuntu, затем перенесем проект, изолируем Python-зависимости, подключим PostgreSQL и только после этого начнем собирать production-цепочку вокруг Django.

А первый практический шаг самый простой — подготовить сам VPS к развертыванию.

Подготавливаем Ubuntu к развертыванию

Для этого руководства я снова использую VPS от ServerMall. Просто потому, что уже привык с ними работать. У него и интерфейс знакомый, и серверы разворачиваются быстро.

Однако подойдет практически любой VPS, если на нем есть Ubuntu, публичный IP и возможность подключиться по SSH с правами sudo. Пользуйтесь провайдером к которому привыкли, а если такого нет – можете либо попробовать ServerMall, либо выбрать любого другого крупного. 

Вернёмся к теме. Наша отправная точка выглядит так:

А дальше уже будем постепенно превращать пустой сервер в production-окружение для Django.

Проверяем систему и обновляем пакеты

Первым делом посмотрим, с какой системой вообще работаем: 

cat /etc/os-release

В выводе будут версия Ubuntu, ее кодовое имя и другая базовая информация.

Например:

NAME="Ubuntu"

VERSION="24.04 LTS (Noble Numbat)"

VERSION_ID="24.04"

Заодно можно проверить архитектуру: dpkg --print-architecture

На большинстве обычных VPS увидим: amd64

Теперь обновим индекс пакетов: sudo apt update

APT получит свежую информацию о доступных пакетах из подключенных репозиториев Ubuntu.

Если на сервере давно ничего не обновлялось, можно заодно установить доступные обновления: sudo apt upgrade -y

Почему лучше сделать это сейчас?

Потому что дальше мы начнем ставить Python, PostgreSQL, Nginx и другие компоненты. Гораздо удобнее собирать окружение поверх актуальной системы, чем уже посреди развертывания внезапно обнаружить старые зависимости или давно ожидающие обновления.

После крупных системных обновлений иногда требуется reboot. Проверить это можно так: 

    test -f /var/run/reboot-required && echo "Reboot required"

Если система действительно просит перезагрузку, лучше сделать это сейчас: sudo reboot , а затем снова подключиться по SSH.

Теперь сам фундамент готов.

Устанавливаем Python, pip, venv и системные зависимости

Django написан на Python, поэтому первым делом нам понадобится сам интерпретатор и инструменты вокруг него.

Установим базовый набор:

    sudo apt install -y python3 python3-pip python3-venv python3-dev build-essential libpq-dev

Здесь уже появляется несколько незнакомых пакетов, поэтому разберем, зачем они вообще нужны:

Пакет Для чего нужен 
python3 Сам интерпретатор Python 
python3-pip Установка Python-пакетов 
python3-venv Создание виртуальных окружений 
python3-dev Заголовочные файлы Python для сборки некоторых зависимостей 
build-essential Компилятор и базовые инструменты сборки 
libpq-dev Библиотеки разработки PostgreSQL, которые могут понадобиться Python-драйверу 

На первый взгляд python3-dev, build-essential и libpq-dev могут выглядеть лишними.

Но Python-пакеты бывают не только полностью написанными на Python.

Некоторые зависимости содержат части на C или должны собираться под конкретную систему во время установки. Если необходимых инструментов нет, обычный: pip install ... ,может внезапно закончиться ошибкой компиляции.

Поэтому удобнее подготовить сервер заранее.

Проверим версии:

python3 --version

pip3 --version

И убедимся, что модуль venv доступен: python3 -m venv --help

Если справка открывается, всё в порядке.

К самому виртуальному окружению мы еще вернемся отдельно. Пока достаточно понимать его смысл: мы не будем устанавливать все зависимости Django глобально в Ubuntu.

Позже создадим отдельную Python-среду именно для нашего проекта.

Примерно:

Так зависимости проекта меньше вмешиваются в системное окружение и другие Python-приложения.

Создаем каталог проекта

Теперь нужно решить, где будет жить код приложения.

Для production-проекта лучше заранее выбрать отдельный понятный каталог, а не складывать файлы куда придется.

Создадим, например: sudo mkdir -p /var/www/django-app

Передадим каталог текущему пользователю: sudo chown -R $USER:$USER /var/www/django-app

И перейдем внутрь: cd /var/www/django-app

Проверим: pwd

Ожидаем: /var/www/django-app

Почему именно /var/www?

Это не жесткое требование Django. Проект вполне может жить и в другом каталоге.

Но /var/www традиционно используется для файлов веб-проектов, поэтому структура сервера получается довольно очевидной:

Если через полгода придется снова зайти на сервер, вероятность спросить себя «куда я вообще засунул проект?» становится чуть ниже.

На этом этапе код мы еще не переносили.

Мы только подготовили место, куда он попадет в следующей главе.

Переносим Django-проект на сервер

На этом этапе наша задача довольно простая: положить код на VPS, проверить, что структура проекта выглядит ожидаемо, и подготовиться к созданию виртуального окружения.

Здесь важно не спешить с запуском. Сначала лучше убедиться, что на сервер действительно приехал весь проект, а не только половина файлов.

Куда складывать production-проект

В прошлой главе мы уже создали каталог: /var/www/django-app

Именно туда и будем помещать код.

Это не обязательное требование Django, а просто удобная организация файлов.

Можно представить сервер так:

Позже внутри этого же проекта появятся:

То есть все основные элементы приложения будут находиться в одном понятном месте.

Это особенно удобно, когда на VPS работает не один проект.

Например:

Каждый сервис получает свой отдельный каталог, и через несколько месяцев не приходится вспоминать, что именно лежит в /home/ubuntu/test_final_new_2.

Загружаем код на VPS

Способов перенести проект несколько.

1. Если код хранится в Git-репозитории, самый удобный вариант — клонировать его прямо на сервер.

Сначала установим Git, если его еще нет: sudo apt install -y git

Переходим в подготовленный каталог: cd /var/www/django-app

Если каталог пустой, проект можно клонировать сюда: git clone https://github.com/USER/PROJECT.git .

Точка в конце означает: «Поместить содержимое репозитория прямо в текущий каталог, а не создавать внутри еще одну вложенную папку.»

Без точки получилось бы примерно так: /var/www/django-app/PROJECT/

А с точкой:

2. Если используется приватный репозиторий, лучше не вставлять логин, пароль или access token прямо в команду.

Для production-сервера обычно используют SSH-ключ или deploy key.

Например: git clone git@github.com:USER/PROJECT.git .

3. Если Git вообще не используется, проект можно передать через scp, SFTP или другой привычный способ.

Например, с локального компьютера:

    scp -r ./django-project/* ubuntu@203.0.113.10:/var/www/django-app/

Какой способ выбрать — не принципиально.

Главное, чтобы в итоге на VPS оказался исходный код проекта, а не локальное виртуальное окружение, кэш Python или случайные секреты с компьютера разработчика.

Именно поэтому в Git обычно не кладут:

venv/

__pycache__/

.env

*.pyc

Эти вещи либо создаются заново на сервере, либо содержат данные, которые вообще не стоит хранить в репозитории.

Проверяем структуру проекта

Код загружен. Теперь не будем сразу выполнять pip install и migrate.

Сначала посмотрим, что вообще приехало: ls -la

Для типичного Django-проекта ожидаем увидеть что-то вроде:

manage.py

requirements.txt

project/

app/

Названия каталогов могут отличаться, но ключевой ориентир — файл: manage.py

Он обычно находится в корне Django-проекта и используется для административных команд:

python manage.py migrate

python manage.py collectstatic

python manage.py createsuperuser

Также должен быть файл со списком зависимостей.

Чаще всего: requirements.txt

Проверить его можно так: cat requirements.txt

Внутри могут находиться, например:

  • Django
  • gunicorn
  • psycopg
  • python-dotenv

А также другие библиотеки конкретного проекта.

Теперь заглянем в каталог самого Django-проекта.

Например: ls project/

Там обычно находятся:

  • settings.py
  • urls.py
  • wsgi.py
  • asgi.py

Особенно нас позже заинтересует: wsgi.py

Именно через него Gunicorn будет подключаться к Django-приложению.

На этом этапе полезно заранее понять имя Django-проекта.

Например, если структура такая:

В таком случае позже запуск Gunicorn будет ссылаться именно на: myproject.wsgi

И это мелочь, которая часто становится причиной ошибок вида: ModuleNotFoundError, если в конфигурации просто скопировали чужое имя проекта.

Создаем виртуальное окружение Python

Код проекта уже на сервере. Следующий шаг — подготовить для него отдельное Python-окружение.

Это важный момент, потому что production-сервер со временем редко остается местом только для одного приложения. Сегодня здесь один Django-проект, завтра может появиться второй, а вместе с ним — другой набор библиотек и другие версии зависимостей. Глазом моргнуть не успеете, как там образовалась своя экосистема.

Если всё устанавливать глобально в системный Python, начинается знакомая история:

  • Проект A → Django 5.x
  • Проект B → другая версия Django
  • Проект C → свой набор библиотек

И все они пытаются жить в одном общем окружении.

Чтобы не превращать сервер в коммунальную квартиру для Python-пакетов, используют venv.

Зачем Django отдельный venv

Venv — это виртуальное окружение Python.

Оно создаёт отдельный каталог, внутри которого находятся собственные:

  • Python-пакеты;
  • Исполняемые файлы;
  • Зависимости проекта;
  • Установленный Django;
  • Gunicorn;
  • Драйвер PostgreSQL;
  • Остальные библиотеки из requirements.txt.

Важно понимать: venv — это не виртуальная машина и не контейнер.

Он не изолирует сеть, процессы или файловую систему целиком.

Он просто отделяет Python-зависимости одного проекта от системного Python и других проектов:

В результате обновление библиотеки в одном проекте не должно неожиданно ломать другой.

Это особенно удобно в production: окружение проекта становится более предсказуемым и воспроизводимым.

В качестве альтернативы можно и упаковать приложение в контейнер, но в рамках профессионального интереса мы будем использовать "классическую" установку с виртуальным окружением.

Создаем и активируем окружение

Перейдем в каталог проекта: cd /var/www/django-app

Теперь создадим виртуальное окружение: python3 -m venv venv

После этого внутри проекта появится каталог: /var/www/django-app/venv/

В нем и будут жить Python и пакеты именно этого проекта.

Активируем окружение: source venv/bin/activate

После активации приглашение терминала обычно меняется примерно так: (venv) ubuntu@server:/var/www/django-app$

Это удобная визуальная подсказка: сейчас команды python и pip относятся уже к нашему виртуальному окружению.

Проверим:

which python

which pip

Ожидаем пути примерно такого вида:

/var/www/django-app/venv/bin/python

/var/www/django-app/venv/bin/pip

То есть теперь мы работаем не с системным Python, а с отдельной копией окружения проекта.

Выйти из него позже можно командой: deactivate

Но пока оставляем venv активным.

Устанавливаем зависимости проекта

Перед установкой пакетов полезно обновить сам pip внутри виртуального окружения: python -m pip install --upgrade pip

Теперь устанавливаем зависимости проекта: pip install -r requirements.txt

Pip прочитает файл requirements.txt и установит указанные там библиотеки именно в текущий venv.

Например, внутри могут быть:

  • Django
  • gunicorn
  • psycopg
  • python-dotenv

Или более строго зафиксированные версии:

  • Django==5.2.6
  • gunicorn==23.0.0
  • psycopg==3.2.9

Фиксация версий делает развертывание более предсказуемым.

Без неё команда: pip install -r requirements.txt

сегодня и через полгода теоретически может установить разные релизы библиотек.

Для production это не всегда желательно.

Если в requirements.txt по какой-то причине нет Gunicorn, его можно добавить отдельно: pip install gunicorn

А для работы с PostgreSQL нужен совместимый драйвер, например: pip install psycopg

Но лучше, чтобы все необходимые зависимости были описаны в requirements.txt, а не устанавливались вручную «по памяти».

Иначе сервер начинает работать, а через месяц никто уже не помнит, какие дополнительные пакеты туда ставили.

Проверяем Django и Gunicorn

После установки не будем сразу переходить к PostgreSQL.

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

Проверим Django: python -m django --version

В ответ должна появиться установленная версия.

Теперь Gunicorn: gunicorn --version

И сам список пакетов: pip list

Для более точной проверки можно убедиться, откуда запускается Gunicorn: which gunicorn

Ожидаем: /var/www/django-app/venv/bin/gunicorn

Это важная мелочь.

Если команда вдруг показывает что-то вроде: /usr/bin/gunicorn, значит мы, скорее всего, используем системную установку, а не версию из виртуального окружения.

В production потом это может привести к довольно забавной ситуации: вручную проект запускается одной версией Python и библиотек, а systemd — совсем другой.

Поэтому сейчас лучше один раз убедиться, что весь стек действительно находится внутри нужного venv.

Поднимаем PostgreSQL для Django

Теперь пора подключить то, без чего большинство реальных Django-проектов долго не живут, — полноценную базу данных.

По умолчанию новый Django-проект обычно стартует с SQLite. Для локальной разработки это удобно: отдельный сервер базы не нужен, всё хранится в одном файле, а начать можно буквально за минуту.

Но production — уже другая история.

Почему production лучше не оставлять на SQLite

SQLite хранит всю базу в одном файле.

Условно: db.sqlite3

Для локальной разработки это даже плюс.

Проект можно быстро запустить, перенести, удалить и создать заново без отдельной настройки СУБД.

Но когда появляются реальные пользователи, параллельные запросы, миграции и постоянная нагрузка, такой подход начинает ограничивать.

PostgreSQL лучше подходит для production-сценариев, потому что это полноценная серверная СУБД.

Она умеет нормально работать с:

  • Несколькими одновременными подключениями;
  • Транзакциями;
  • Блокировками;
  • Сложными запросами;
  • Индексами;
  • Большими объемами данных;
  • Отдельными пользователями и правами доступа.

Проще говоря:

  • SQLite → удобно начать
  • PostgreSQL → удобнее жить дальше в production

Это не значит, что SQLite плохая база.

У нее просто другая зона комфорта.

Если Django-проект — это небольшой внутренний инструмент с минимальной нагрузкой, SQLite может прекрасно справляться и дальше.

Но для публичного production-приложения PostgreSQL обычно дает гораздо больше пространства для роста.

Получится так:

Причем Django не будет подключаться к базе под административным пользователем PostgreSQL.

Как и в прошлой статье с MySQL, создадим отдельную базу и отдельную учетную запись именно для приложения.

Устанавливаем PostgreSQL

PostgreSQL доступен в стандартных репозиториях Ubuntu.

Установим сервер и дополнительные утилиты:

sudo apt update

sudo apt install -y postgresql postgresql-contrib

После установки PostgreSQL обычно автоматически запускается через systemd.

Проверим: sudo systemctl status postgresql --no-pager

Нас интересует Active: active (exited) или состояние работающего PostgreSQL-кластера, в зависимости от версии и организации systemd units.

Дополнительно можно проверить готовность самого сервера: sudo -u postgres pg_isready

Ожидаем ответ примерно такого вида: /var/run/postgresql:5432 - accepting connections

Эта проверка говорит, что PostgreSQL действительно принимает подключения.

Посмотрим, какой порт используется: sudo ss -lntp | grep 5432

По умолчанию PostgreSQL работает на: 5432

Но наружу этот порт нам сейчас вообще не нужен.

Django и PostgreSQL находятся на одном VPS, поэтому база может спокойно оставаться доступной только локально.

Создаем базу и отдельного пользователя

После установки PostgreSQL в системе появляется административная учетная запись: postgres

Это и Linux-пользователь для обслуживания PostgreSQL, и одноименная административная роль внутри самой СУБД.

Для повседневной работы Django использовать ее не будем.

Зайдем в PostgreSQL: sudo -u postgres psql

Приглашение изменится примерно на: postgres=#

Теперь создадим базу для нашего проекта: CREATE DATABASE django_db;

И отдельного пользователя: CREATE USER django_user WITH PASSWORD 'STRONG_PASSWORD';

Схематически это выглядит так:

Пользователь postgres остается администратору, а приложение получает собственную учетную запись.

Это полезно по той же причине, по которой мы не запускали приложение от MySQL root: если приложение однажды будет скомпрометировано, его учетная запись не должна автоматически получать полный контроль над всеми базами PostgreSQL на сервере.

Выдаем права только на нужную базу

Теперь дадим django_user доступ к нашей базе: GRANT ALL PRIVILEGES ON DATABASE django_db TO django_user;

Но здесь есть нюанс PostgreSQL.

Права на саму базу и права на объекты внутри нее — не всегда одно и то же.

Django во время миграций должен создавать таблицы, индексы и другие объекты.

Поэтому подключимся к нужной базе: \c django_db

И дадим пользователю возможность работать со схемой public: GRANT ALL ON SCHEMA public TO django_user;

В современных конфигурациях PostgreSQL это особенно важно, потому что обычному пользователю может не хватать прав на создание объектов в public.

Теперь Django сможет выполнять миграции и создавать свои таблицы.

При желании можно сделать пользователя владельцем базы: ALTER DATABASE django_db OWNER TO django_user;

Для отдельной базы одного приложения это часто удобно: django_user становится владельцем именно своей базы, но не получает административных прав на весь PostgreSQL Server.

После этого выйдем: \q

Главная мысль здесь такая:

  • Postgres → администрирует сервер PostgreSQL
  • Django_user → работает только с django_db

И роли больше не смешиваются.

Проверяем подключение

Теперь проверим учетную запись еще до того, как начнем настраивать Django.

Подключимся локально: psql -h 127.0.0.1 -U django_user -d django_db

PostgreSQL попросит пароль.

Если всё настроено правильно, приглашение станет примерно таким: django_db=>

Проверим текущего пользователя: SELECT current_user;

Ожидаем: django_user

А текущую базу: SELECT current_database();

Получим: django_db

Для окончательной проверки можно временно создать небольшую таблицу:

CREATE TABLE connection_test (

    id SERIAL PRIMARY KEY,

    message TEXT NOT NULL

);

Добавим запись:

INSERT INTO connection_test (message)

VALUES ('PostgreSQL works');

И прочитаем: SELECT * FROM connection_test;

Если данные возвращаются, значит пользователь действительно может создавать таблицы и работать с базой.

После проверки тестовую таблицу можно удалить: DROP TABLE connection_test;

Теперь база полностью готова со своей стороны.

Но Django пока ничего о ней не знает. Более того, нам точно не хочется вписывать пароль PostgreSQL, SECRET_KEY и production-настройки прямо в settings.py, а затем случайно отправить всё это в Git.

Поэтому следующим шагом вынесем секреты и параметры окружения из кода и уже через них подключим Django к PostgreSQL.

Убираем секреты из settings.py

Почему SECRET_KEY и пароль базы нельзя хранить прямо в коде

В settings.py Django хранит критически важные параметры проекта.

Например:

SECRET_KEY = "some-secret-value"

DEBUG = True

и конфигурацию базы:

DATABASES = {

    "default": {

        "ENGINE": "django.db.backends.postgresql",

        "NAME": "django_db",

        "USER": "django_user",

        "PASSWORD": "STRONG_PASSWORD",

        "HOST": "127.0.0.1",

        "PORT": "5432",

    }

}

Технически Django прекрасно заработает.

Проблема начинается позже.

Допустим, проект хранится в Git.

Вы выполняете:

git add .

git commit -m "production settings"

git push

и вместе с кодом в репозиторий уезжает пароль PostgreSQL.

Если репозиторий публичный — ситуация очевидно плохая.

Но даже в приватном репозитории секреты лучше не смешивать с кодом. Доступ к Git могут получать разработчики, CI/CD, сторонние сервисы и другие системы, которым совершенно не обязательно знать пароль production-базы.

С SECRET_KEY история похожая.

Django использует его для криптографических операций внутри framework. Поэтому это не декоративная строка, которую можно спокойно публиковать вместе с кодом.

Удобнее разделить:

Тогда один и тот же код можно использовать на разных стендах:

Development

→ своя база

→ DEBUG=True

Staging

→ другая база

→ свои домены

Production

→ production PostgreSQL

→ DEBUG=False

→ реальный домен

Код один, а значения меняются в зависимости от окружения.

Именно для этого мы создадим .env.

Создаем .env

Перейдем в каталог проекта: cd /var/www/django-app

Создадим файл: nano .env

Добавим туда значения для нашего production-окружения:

SECRET_KEY=CHANGE_ME_TO_A_LONG_RANDOM_VALUE

DEBUG=False

DB_NAME=django_db

DB_USER=django_user

DB_PASSWORD=STRONG_PASSWORD

DB_HOST=127.0.0.1

DB_PORT=5432

ALLOWED_HOSTS=example.com,www.example.com

Теперь секреты находятся отдельно от settings.py.

Но сам файл .env тоже нужно защищать.

Во-первых, его не стоит отправлять в Git.

Проверим .gitignore: nano .gitignore

И добавим: .env

Если .env уже когда-то был добавлен в репозиторий, одного .gitignore недостаточно: Git продолжит отслеживать файл, пока его отдельно не уберут из индекса.

Во-вторых, ограничим права на самом сервере: chmod 600 .env

Теперь читать и изменять файл сможет только его владелец.

Передаем параметры PostgreSQL через переменные окружения

Теперь нужно научить Django читать эти значения.

Способ зависит от проекта.

Если в requirements.txt уже используется, например, python-dotenv, можно загрузить .env через него.

В начале settings.py:

import os

from pathlib import Path

from dotenv import load_dotenv

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

load_dotenv(BASE_DIR / ".env")

После этого получаем значения через:

os.getenv("VARIABLE_NAME")

Например: SECRET_KEY = os.getenv("SECRET_KEY")

Теперь настроим PostgreSQL:

DATABASES = {

    "default": {

        "ENGINE": "django.db.backends.postgresql",

        "NAME": os.getenv("DB_NAME"),

        "USER": os.getenv("DB_USER"),

        "PASSWORD": os.getenv("DB_PASSWORD"),

        "HOST": os.getenv("DB_HOST", "127.0.0.1"),

        "PORT": os.getenv("DB_PORT", "5432"),

    }

}

Обратите внимание на: os.getenv("DB_HOST", "127.0.0.1")

Второе значение — это fallback.

Если переменная DB_HOST отсутствует, Django использует: 127.0.0.1

То же самое мы сделали для PostgreSQL-порта.

В итоге settings.py больше не знает реальный пароль.

Он знает только: «Возьми пароль из окружения».

А конкретное значение находится уже на сервере.

Настраиваем DEBUG и ALLOWED_HOSTS

Теперь разберемся еще с двумя production-настройками.

Первая: DEBUG

На локальной машине обычно используется: DEBUG = True

Это удобно: Django показывает подробные страницы ошибок с traceback, переменными и другой информацией для разработчика.

В production такую информацию посетителю показывать не нужно.

Поэтому в .env мы написали: DEBUG=False

Но здесь есть маленькая ловушка.

Если сделать: DEBUG = os.getenv("DEBUG")

значение: False

придет как строка, а непустая строка в Python считается истинным значением.

То есть можно написать False и неожиданно получить поведение, похожее на включенный флаг.

Поэтому лучше преобразовать значение явно: DEBUG = os.getenv("DEBUG", "False").lower() == "true"

Теперь:

True  → True

true  → True

False → False

Следующий параметр: ALLOWED_HOSTS

Django использует его, чтобы понимать, для каких host-имен приложение вообще должно обслуживать запросы.

В .env: ALLOWED_HOSTS=example.com,www.example.com

А в settings.py:

ALLOWED_HOSTS = [

    host.strip()

    for host in os.getenv("ALLOWED_HOSTS", "").split(",")

    if host.strip()

]

В итоге строка: example.com,www.example.com

превращается в: ["example.com", "www.example.com"]

Пока реальный домен еще может быть не подключен, для временной проверки можно использовать нужный host отдельно.

Но вариант: ALLOWED_HOSTS = ["*"], лучше не оставлять как постоянную production-настройку просто ради того, чтобы «всё заработало».

Мы ещё вернемся к ALLOWED_HOSTS, когда подключим домен.

Проверяем, что Django видит окружение

Теперь важно не просто сохранить файлы, а убедиться, что Django действительно получил значения.

У нас должен быть активен venv:

cd /var/www/django-app

source venv/bin/activate

Сначала можно проверить саму конфигурацию Django: python manage.py check

Если всё в порядке, ожидаем: System check identified no issues

Теперь проверим подключение к PostgreSQL через сам Django.

Откроем shell: python manage.py shell

И выполним:

from django.db import connection

connection.ensure_connection()

print(connection.settings_dict["NAME"])

Если подключение установилось, увидим: django_db

После этого: exit()

Есть и более простой практический тест, который мы всё равно скоро выполним: python manage.py migrate

Если Django успешно подключается к PostgreSQL и начинает работать с миграциями, значит параметры базы читаются корректно.

Но миграции мы оставим для следующей главы.

Сейчас главное проверить три вещи:

Есть еще один важный момент.

Мы защитили секреты от Git, но позже Gunicorn будет запускаться не из нашей текущей shell-сессии, а через systemd.

А значит, systemd тоже должен знать, откуда брать эти переменные.

К этому вернемся, когда будем создавать gunicorn.service.

Пока же Django уже знает свою production-базу и основные настройки окружения.

Следующий шаг — подготовить само приложение к первому production-запуску: выполнить миграции, собрать static и проверить проект перед подключением Gunicorn.

Подготавливаем Django к production-запуску

Как и писал ранее, здесь нас ждёт три обязательных шага:

  • Применить миграции;
  • Собрать static-файлы;
  • Проверить, что сам Django запускается без ошибок.

При необходимости заодно создадим административного пользователя.

Запускаем миграции

Начнем с базы.

Django хранит структуру моделей в коде, но одной записи класса в models.py недостаточно, чтобы соответствующая таблица появилась в PostgreSQL.

Для этого существуют миграции.

Примерно:

Если проект уже содержит готовые миграции, нам нужно просто применить их к production-базе.

Активируем виртуальное окружение:

cd /var/www/django-app

source venv/bin/activate

Проверим, какие миграции Django видит: python manage.py showmigrations

Не примененные миграции будут отмечены примерно так: [ ]

А примененные: [X]

Теперь запускаем: python manage.py migrate

Django подключится к PostgreSQL через настройки, которые мы задали в .env, и создаст необходимые таблицы.

В успешном выводе появятся строки вроде:

Applying contenttypes.0001_initial... OK

Applying auth.0001_initial... OK

Applying sessions.0001_initial... OK

Если миграции заканчиваются ошибкой подключения к PostgreSQL, возвращаемся к предыдущей главе и проверяем DB_NAME, DB_USER, пароль, host и права пользователя.

Если же видим OK, значит связка:

уже работает на практике, а не только в конфигурации.

Создаем superuser при необходимости

Если проект использует стандартную административную панель Django, можно сразу создать администратора.

Для этого: python manage.py createsuperuser

Django попросит указать имя пользователя, email и пароль.

После этого учетная запись сможет входить в /admin/, когда сайт уже будет доступен через Nginx.

Создавать superuser обязательно не всегда.

Если проект вообще не использует Django Admin или административные аккаунты создаются другим способом, этот шаг можно спокойно пропустить.

И здесь есть небольшая, но важная мысль: superuser внутри Django — это не Linux root и не пользователь PostgreSQL.

Это отдельный уровень доступа уже внутри самого веб-приложения.

То есть у нас постепенно появляется несколько разных ролей:

Linux user

→ управляет файлами и процессами на VPS

PostgreSQL user

→ работает с базой

Django superuser

→ управляет данными через Django Admin

Названия и права у них разные, и смешивать эти уровни не стоит.

Собираем static через collectstatic

Следующий шаг часто вызывает вопросы у тех, кто впервые переносит Django в production.

В проекте могут быть CSS, JavaScript, изображения административной панели и другие static-файлы.

Во время разработки Django умеет удобно работать с ними сам.

В production мы пойдем другим путем: соберем static в отдельный каталог, а позже Nginx будет раздавать их напрямую.

Сначала убедимся, что в settings.py задан STATIC_ROOT.

Например:

STATIC_URL = "/static/"

STATIC_ROOT = BASE_DIR / "staticfiles"

Теперь выполним: python manage.py collectstatic

Django найдет static-файлы всех приложений и сложит их в один каталог: /var/www/django-app/staticfiles/

По смыслу происходит так:

Если Django попросит подтверждение перезаписи файлов, для автоматического production-развертывания можно использовать: python manage.py collectstatic --noinput

После выполнения можно проверить каталог: ls -lah staticfiles/

Важно понимать: collectstatic не создает CSS или JavaScript из воздуха.

Он собирает уже существующие static-файлы проекта и установленных Django-приложений в одно место, откуда их удобно отдавать веб-серверу.

К media это не относится.

Пользовательские загрузки вроде аватаров, документов и фотографий позже будут храниться отдельно.

Проверяем приложение локально

Теперь перед Gunicorn проведем последнюю проверку самого Django.

Сначала: python manage.py check

Ожидаемый результат: System check identified no issues

Эта команда проверяет конфигурацию проекта и помогает поймать часть проблем еще до запуска.

Но можно пойти чуть дальше и временно поднять встроенный сервер Django локально: python manage.py runserver 127.0.0.1:8000

Обратите внимание: мы специально используем: 127.0.0.1, а не: 0.0.0.0

Сейчас нам не нужно публиковать development server в интернет.

Мы просто хотим убедиться, что проект действительно стартует на самом VPS.

В другой SSH-сессии проверим: curl http://127.0.0.1:8000

Если приходит HTML приложения или ожидаемый HTTP-ответ, значит Django способен запуститься с текущими настройками.

После проверки остановим runserver: Ctrl + C

И больше к нему в production возвращаться не будем.

На этом сам Django подготовлен:

Но пока приложение всё еще стартует через development server вручную.

Следующий шаг — заменить его Gunicorn и посмотреть, как Django будет работать уже через production WSGI-сервер.

Запускаем Django через Gunicorn

Теперь между Nginx и самим Django появится Gunicorn — отдельный WSGI-сервер, который будет запускать приложение и принимать запросы от веб-сервера.

Почему runserver не используют в production

Команда python manage.py runserver создана прежде всего для разработки.

Она удобна, потому что:

  • Быстро запускает проект;
  • Показывает ошибки;
  • Автоматически перезапускается при изменениях кода;
  • Не требует отдельной настройки.

Но в production от сервера ждут уже другого поведения.

Нам важно, чтобы приложение:

  • Стабильно обрабатывало реальные запросы;
  • Могло использовать несколько worker-процессов;
  • Управлялось отдельным системным сервисом;
  • Нормально работало за reverse proxy;
  • Не зависело от открытого терминала.

Поэтому runserver здесь заканчивает свою работу.

Как Gunicorn связан с Django

Как говорили ранее, Gunicorn — это WSGI HTTP Server для Python-приложений.

Но особо внимательные заметили, что здесь появляется еще одна аббревиатура — WSGI.

Если максимально упростить, WSGI — это стандарт взаимодействия между Python web-приложением и сервером, который его запускает.

То есть Gunicorn не заменяет Django.

Он предоставляет среду, в которой Django может принимать HTTP-запросы.

Условно:

Внутри обычного Django-проекта уже существует файл: myproject/wsgi.py

Мы проверяли его наличие еще после загрузки проекта на VPS.

Обычно внутри находится примерно такая конфигурация:

import os

from django.core.wsgi import get_wsgi_application

os.environ.setdefault(

    "DJANGO_SETTINGS_MODULE",

    "myproject.settings"

)

application = get_wsgi_application()

Нас особенно интересует объект: application

Именно к нему затем подключается Gunicorn.

Поэтому команда запуска обычно заканчивается конструкцией: myproject.wsgi:application

Здесь:

  • myproject.wsgi — Python-модуль;
  • application — WSGI-приложение внутри него.

Если каталог вашего Django-проекта называется иначе, это имя тоже нужно изменить.

Например: shop/wsgi.py означает: shop.wsgi:application

Копировать чужое имя проекта вслепую здесь определенно не стоит.

Проверяем ручной запуск Gunicorn

Gunicorn мы уже установили вместе с Python-зависимостями.

Активируем виртуальное окружение:

cd /var/www/django-app

source venv/bin/activate

И для начала убедимся, что запускается именно версия из нашего venv: which gunicorn

Ожидаем примерно: /var/www/django-app/venv/bin/gunicorn

Теперь запустим Django вручную: gunicorn --bind 127.0.0.1:8000 myproject.wsgi:application

Если имя проекта другое, заменяем: myproject на свое.

Например: gunicorn --bind 127.0.0.1:8000 shop.wsgi:application

После запуска Gunicorn покажет сообщения примерно такого смысла:

Starting gunicorn

Listening at: http://127.0.0.1:8000

Using worker

Booting worker

Теперь приложение уже запущено не через manage.py runserver, а через Gunicorn.

Откроем вторую SSH-сессию и проверим: curl http://127.0.0.1:8000

Если приходит HTML страницы или ожидаемый ответ приложения, значит цепочка работает:

Можно также посмотреть только HTTP-заголовки: curl -I http://127.0.0.1:8000

Например, нас устроит: HTTP/1.1 200 OK

или другой ожидаемый код конкретного маршрута.

После проверки остановим ручной запуск: Ctrl + C

Пока Gunicorn всё еще зависит от нашей SSH-сессии.

Чуть позже это исправит systemd.

Выбираем Unix socket или локальный TCP-порт

Теперь нужно решить, как Nginx позже будет связываться с Gunicorn.

Есть два распространенных варианта.

Первый — локальный TCP-порт:

Второй — Unix socket:

И оба варианта рабочие.

Начнём с первого. Локальный TCP-порт.

Например: gunicorn --bind 127.0.0.1:8000 myproject.wsgi:application

Главное здесь — именно: 127.0.0.1

Gunicorn слушает только loopback-интерфейс сервера.

То есть пользователь из интернета не сможет напрямую обратиться к: SERVER_IP:8000

Запросы будут идти через Nginx.

Это дает простую и понятную схему:

Для учебного развертывания это особенно удобно: порт легко проверить через curl, ss и другие стандартные инструменты.

Вторым идёт Unix socket.

Вместо TCP Gunicorn может создать специальный файл-сокет.

Например: /run/gunicorn/django-app.sock

Тогда Nginx и Gunicorn общаются через него внутри одной Linux-системы.

По итогу выйдет так:

Здесь вообще не используется TCP-порт между двумя локальными процессами.

Unix socket вполне привычен для подобных production-конфигураций, но появляется дополнительный слой работы с правами доступа:

  • Кто владеет socket?
  • Может ли Nginx его открыть?
  • Существует ли каталог?
  • Какие у него permissions?

И если где-то ошибиться, вместо сайта можно получить 502 Bad Gateway, хотя сам Django при этом совершенно здоров.

Поэтому в этом руководстве выберем более наглядный вариант: 127.0.0.1:8000

Для нашей схемы его вполне достаточно.

Gunicorn не торчит наружу, а Nginx позже сможет обращаться к нему через: proxy_pass http://127.0.0.1:8000;

При этом полезно помнить простое правило:

  • 127.0.0.1:8000 → доступен только самому VPS
  • 0.0.0.0:8000 → слушает все IPv4-интерфейсы

Поэтому без необходимости использовать: --bind 0.0.0.0:8000, в production-схеме за Nginx нам не нужно.

Теперь мы уже доказали, что приложение работает через production WSGI-сервер.

Но ручной запуск пока остается ручным: закроем SSH, перезагрузим VPS — и Gunicorn исчезнет.

Следующим шагом передадим управление процессом systemd, чтобы приложение запускалось автоматически и вело себя как нормальный системный сервис.

Делаем Gunicorn системным сервисом

Зачем Gunicorn нужен systemd

Systemd — это система управления службами в Linux.

Она уже управляет многими компонентами нашего сервера.

Например:

  • PostgreSQL
  • Nginx
  • SSH

Работать с Gunicorn вручную каждый раз было бы странно, поэтому сделаем из него такой же нормальный системный сервис.

В таком случае systemd сможет:

  • Запускать Gunicorn при старте VPS;
  • Останавливать и перезапускать сервис;
  • Показывать его состояние;
  • Хранить логи;
  • При необходимости автоматически перезапускать процесс после сбоя.

То есть вместо команды:

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

мы позже будем использовать:

sudo systemctl start gunicorn

sudo systemctl restart gunicorn

sudo systemctl status gunicorn

С точки зрения эксплуатации это гораздо удобнее.

Создаем gunicorn.service

Файлы пользовательских systemd-сервисов обычно размещают в: /etc/systemd/system/

Создадим новый unit: sudo nano /etc/systemd/system/gunicorn.service

Добавим:

[Unit]

Description=Gunicorn for Django application

After=network.target postgresql.service

[Service]

User=ubuntu

Group=www-data

WorkingDirectory=/var/www/django-app

ExecStart=/var/www/django-app/venv/bin/gunicorn \

    --workers 3 \

    --bind 127.0.0.1:8000 \

    myproject.wsgi:application

Restart=on-failure

[Install]

WantedBy=multi-user.target

Здесь уже много новых параметров, поэтому разберем их подробнее.

Указываем рабочий каталог и окружение

Начнем с блока:

[Unit]

Description=Gunicorn for Django application

After=network.target postgresql.service

Description — просто понятное описание сервиса.

After=network.target postgresql.service — говорит systemd запускать Gunicorn после того, как базовые сетевые службы и PostgreSQL уже поднялись.

Это не означает, что systemd магически проверит работоспособность самой базы, но порядок старта становится логичнее.

Теперь блок: [Service]

Здесь находится основная конфигурация процесса.

User

User=ubuntu

Gunicorn будет работать от обычного пользователя, а не от root.

И это правильнее с точки зрения безопасности.

Если приложение скомпрометируют, процесс не должен автоматически получать полный административный доступ ко всему серверу.

Разумеется, если ваш пользователь называется иначе, нужно указать реальное имя. Оптимально будет завести отдельного пользователя для этого.

Проверить его можно так: whoami

Group

Group=www-data

www-data обычно используется Nginx и другими веб-сервисами в Ubuntu.

В нашей конфигурации Nginx будет обращаться к Gunicorn через локальный TCP-порт, поэтому прямой общий доступ к Unix socket нам не требуется.

Тем не менее такая группа остается вполне обычным вариантом для веб-приложения.

WorkingDirectory

WorkingDirectory=/var/www/django-app

Это рабочий каталог процесса.

Отсюда Gunicorn сможет корректно импортировать myproject.wsgi и находить файлы проекта.

Если забыть WorkingDirectory, systemd может запустить команду из другого каталога, после чего появляются ошибки вроде: ModuleNotFoundError: No module named 'myproject'

Хотя сам проект при этом лежит на сервере и прекрасно запускается вручную.

ExecStart

Самая важная строка:

ExecStart=/var/www/django-app/venv/bin/gunicorn \

    --workers 3 \

    --bind 127.0.0.1:8000 \

    myproject.wsgi:application

Обратите внимание: здесь мы не пишем просто: gunicorn

Мы указываем полный путь: /var/www/django-app/venv/bin/gunicorn

Это гарантирует, что systemd запускает Gunicorn именно из виртуального окружения нашего проекта.

Иначе можно получить неприятную ситуацию:

в SSH

→ используется Gunicorn из venv

→ приложение работает

Через systemd

→ используется другой Python/Gunicorn

→ зависимости не найдены

Теперь: --workers 3 задает количество worker-процессов.

Worker — это отдельный процесс Gunicorn, который может обрабатывать запросы.

Схематично:

Три worker здесь — просто понятный стартовый пример, а не универсальное число для любого VPS.

Оптимальное количество зависит от:

  • Числа CPU;
  • Характера нагрузки;
  • Объема памяти;
  • Того, насколько приложение CPU- или I/O-зависимо.

Поэтому не стоит воспринимать 3 как магическую production-константу.

Дальше: --bind 127.0.0.1:8000 оставляет Gunicorn доступным только внутри VPS.

А: myproject.wsgi:application указывает на WSGI-приложение Django.

Если проект называется иначе, эту часть нужно заменить.

А что с .env?

Здесь есть важный момент.

В предыдущей главе мы использовали python-dotenv и загрузили файл: load_dotenv(BASE_DIR / ".env") прямо внутри settings.py.

Поэтому при запуске через Gunicorn Django сам прочитает: /var/www/django-app/.env

Но при условии, что:

  • Файл существует;
  • BASE_DIR определен корректно;
  • Пользователь ubuntu, от которого запускается Gunicorn, имеет право его читать.

Проверим права: ls -l /var/www/django-app/.env

Мы ранее задавали: chmod 600 .env

Если владельцем остается тот же ubuntu, всё нормально.

Получается такая цепочка:

То есть дополнительно прописывать пароль PostgreSQL прямо в gunicorn.service нам не нужно.

И это хорошо: секреты снова не размазываются по нескольким конфигурационным файлам.

Запускаем сервис и включаем автозапуск

Сохраняем unit-файл.

После создания или изменения systemd-конфигурации нужно перечитать units: sudo systemctl daemon-reload

Теперь запускаем Gunicorn: sudo systemctl start gunicorn

Проверяем: sudo systemctl status gunicorn --no-pager

Если всё хорошо, увидим состояние: Active: active (running)

Теперь включим автозапуск: sudo systemctl enable gunicorn

Можно сразу объединить запуск и включение: sudo systemctl enable --now gunicorn

Разница простая:

  • start → запускает сейчас
  • enable → запускает автоматически после загрузки системы
  • enable --now → делает оба действия

Теперь даже после перезагрузки VPS systemd сможет поднять Gunicorn снова.

Проверить, включен ли автозапуск: sudo systemctl is-enabled gunicorn

Ожидаем: enabled

Проверяем статус и логи

Самый первый инструмент диагностики: sudo systemctl status gunicorn --no-pager

Но статус показывает только часть информации.

Если Gunicorn не запускается, смотрим журнал: sudo journalctl -u gunicorn

Последние записи: sudo journalctl -u gunicorn -n 50 --no-pager

А если хотим наблюдать журнал в реальном времени: sudo journalctl -u gunicorn -f

Это особенно полезно при запуске приложения и поиске ошибок импорта, подключения к PostgreSQL или конфигурации Django.

Например, если ошиблись в myproject.wsgi:application в журнале может появиться: ModuleNotFoundError

Если Gunicorn не может прочитать .env, Django может сообщить об отсутствующем SECRET_KEY или настройках базы.

Если порт уже занят: 127.0.0.1:8000, увидим ошибку bind.

Проверить сам порт можно отдельно: sudo ss -lntp | grep 8000

При работающем Gunicorn ожидаем что-то на: 127.0.0.1:8000

И последняя практическая проверка: curl http://127.0.0.1:8000

Если приложение отвечает, значит теперь оно работает уже не из нашего терминала, а как полноценная системная служба.

Можно даже закрыть SSH-сессию, подключиться заново и повторить: sudo systemctl status gunicorn --no-pager

Gunicorn останется на месте.

Следующим слоем поставим Nginx — именно он станет публичной точкой входа в наше Django-приложение.

Ставим Nginx перед Django

Почему Gunicorn не стоит выставлять напрямую в интернет

Технически можно было бы запустить Gunicorn так: gunicorn --bind 0.0.0.0:8000 myproject.wsgi:application и открыть порт 8000 во внешнем firewall.

Сайт даже заработал бы по адресу вроде: http://203.0.113.10:8000

Но для production-схемы это такое себе.

Gunicorn нужен прежде всего для запуска Python-приложения. А внешнюю работу с HTTP-трафиком удобнее отдать специализированному веб-серверу.

Nginx лучше подходит для таких задач, как:

  • Прием запросов на портах 80 и 443;
  • Работа с доменами;
  • TLS и HTTPS;
  • Раздача static и media;
  • Проксирование;
  • Обработка HTTP-заголовков;
  • Ограничения на размер запросов;
  • Ведение access/error logs.

Поэтому Gunicorn оставляем внутри VPS: 127.0.0.1:8000, а пользователю вообще не нужно знать, что такой порт существует.

Это еще и упрощает архитектуру безопасности: наружу открываются только те сервисы, которые действительно должны быть публичными.

Создаем конфигурацию сайта

Установим Nginx:

sudo apt update

sudo apt install -y nginx

Проверим сервис: sudo systemctl status nginx --no-pager

Если всё в порядке, увидим: Active: active (running)

Заодно включим автозапуск, если он по какой-то причине еще не включен: sudo systemctl enable nginx

Теперь создадим отдельную конфигурацию для Django-проекта: sudo nano /etc/nginx/sites-available/django-app

Для начала она может выглядеть так:

server {

    listen 80;

    listen [::]:80;

    server_name example.com www.example.com;

    location / {

        proxy_pass http://127.0.0.1:8000;

    }

}

Пока здесь всего несколько строк:

  • listen 80 говорит Nginx принимать обычные HTTP-запросы.
  • server_name example.com www.example.com; определяет, для какого домена предназначен этот server block.

Реальный домен подключим чуть позже, поэтому сейчас используется документационный пример: example.com

А внутри:

location / {

    ...

}

обрабатываются все обычные запросы к сайту.

Настраиваем проксирование на Gunicorn

Главная строка здесь: proxy_pass http://127.0.0.1:8000;

Она говорит Nginx: если запрос попал в этот location, передай его приложению, которое работает локально на порту 8000.

Это тот же адрес, который мы прописали в gunicorn.service: --bind 127.0.0.1:8000

Поэтому обе стороны должны совпадать.

Если Gunicorn слушает: 127.0.0.1:8000

а в Nginx случайно написать: proxy_pass http://127.0.0.1:9000;

Nginx не найдет приложение и, скорее всего, вернет: 502 Bad Gateway

Перед включением конфигурации полезно еще раз проверить Gunicorn: sudo ss -lntp | grep 8000 и curl http://127.0.0.1:8000

Если локально Django отвечает, значит backend для Nginx уже готов.

Передаем нужные HTTP-заголовки

Одного proxy_pass достаточно, чтобы получить базовое проксирование, но Django полезно передавать информацию об исходном запросе.

Расширим конфигурацию:

server {

    listen 80;

    listen [::]:80;

    server_name example.com www.example.com;

    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;

    }

}

Разберем эти строки:

Директива Что передает Зачем это нужно 
proxy_set_header Host $host; Исходный host запроса Django использует его в том числе при проверке ALLOWED_HOSTS.
proxy_set_header X-Real-IP $remote_addr; IP-адрес клиента Позволяет приложению и логам видеть адрес пользователя, а не только локальный адрес Nginx. 
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; Цепочку прокси и исходный IP клиента Полезно для логирования, аналитики и случаев, когда запрос проходит через несколько proxy-серверов. 
proxy_set_header X-Forwarded-Proto $scheme; Схему исходного запроса: http или httpsОсобенно важно после подключения TLS: приложение за reverse proxy должно понимать, что пользователь пришел по HTTPS. 

Без корректного proxy header приложение за reverse proxy иногда может считать, что пользователь пришел по HTTP, хотя снаружи уже используется HTTPS.

Включаем конфигурацию и проверяем nginx -t

Файл в sites-available сам по себе еще не активен.

Создадим символическую ссылку:

    sudo ln -s /etc/nginx/sites-available/django-app /etc/nginx/sites-enabled/

Теперь конфигурация появилась среди включенных сайтов.

Перед перезагрузкой Nginx обязательно проверим синтаксис: sudo nginx -t

При корректной конфигурации получим примерно:

syntax is ok

test is successful

И только после этого применяем изменения: sudo systemctl reload nginx

Именно reload, а не обязательно полноценный restart.

При reload Nginx перечитывает конфигурацию без обычной полной остановки сервиса.

Если на VPS оставлена стандартная конфигурация: /etc/nginx/sites-enabled/default, она иногда может мешать проверке по IP или перехватывать запросы, которые не подходят под наш server_name.

При необходимости стандартный сайт можно отключить: sudo rm /etc/nginx/sites-enabled/default

После чего снова выполнить:

sudo nginx -t

sudo systemctl reload nginx

Теперь проверим Nginx локально.

Например: curl -H "Host: example.com" http://127.0.0.1

Параметр: -H "Host: example.com" позволяет проверить нужный server_name еще до того, как реальный DNS будет окончательно настроен.

Если получаем страницу Django, значит Nginx успешно принимает запрос и передает его Gunicorn.

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

Следующий шаг — разобраться со static и media. Их мы не будем без необходимости прогонять через Django и Gunicorn: Nginx сможет отдавать такие файлы самостоятельно.

Раздаем static и media через Nginx

Чем static отличается от media

На первый взгляд и там и там лежат обычные файлы.

Но назначение у них разное.

Static — это файлы самого приложения:

  • CSS;
  • JavaScript;
  • иконки;
  • изображения интерфейса;
  • шрифты;
  • static-файлы Django Admin.

Обычно они поставляются вместе с кодом проекта.

Например:

/static/css/style.css

/static/js/app.js

/static/admin/css/base.css

А media — это файлы, которые появляются уже во время работы приложения.

Например:

  • Аватары пользователей;
  • Фотографии товаров;
  • Загруженные документы;
  • Вложения;
  • Изображения из форм.

То есть:

static

→ часть приложения

media

→ пользовательский или генерируемый контент

Эта разница важна еще и при развертывании.

Static можно заново собрать из проекта через collectstatic.

А media так просто восстановить нельзя: это уже реальные данные пользователей, и для них нужен отдельный backup.

Почему их не должен раздавать Gunicorn

Технически Django может отдавать файлы сам.

Во время разработки именно так часто и происходит.

Но в production заставлять Gunicorn и Django заниматься каждым CSS-файлом или картинкой нет особого смысла.

Представим запрос: GET /static/css/style.css

Если отправлять его в Django, приложению придется принять запрос, обработать его через Python и только потом вернуть файл.

Хотя Nginx умеет просто взять нужный файл с диска и отправить клиенту напрямую.

Для веб-сервера это вполне естественная задача.

Поэтому мы разделяем обязанности:

  • Динамические URL передаем Gunicorn;
  • /static/ обслуживает Nginx;
  • /media/ тоже обслуживает Nginx.

Так Python-процессы не тратятся на работу, которую гораздо проще выполнить веб-серверу.

Настраиваем STATIC_ROOT и MEDIA_ROOT

С STATIC_ROOT мы уже немного познакомились перед выполнением collectstatic.

Откроем: myproject/settings.py

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

STATIC_URL = "/static/"

STATIC_ROOT = BASE_DIR / "staticfiles"

MEDIA_URL = "/media/"

MEDIA_ROOT = BASE_DIR / "media"

Здесь важно не путать URL и путь на диске.

Например: STATIC_URL = "/static/" означает адрес, который увидит браузер: https://example.com/static/css/style.css

А: STATIC_ROOT = BASE_DIR / "staticfiles" означает реальный каталог на сервере: /var/www/django-app/staticfiles/

С media то же самое.

MEDIA_URL = "/media/" дает URL: https://example.com/media/avatar.jpg

а: MEDIA_ROOT = BASE_DIR / "media" указывает на: /var/www/django-app/media/

После изменения static-настроек снова можно выполнить:

cd /var/www/django-app

source venv/bin/activate

python manage.py collectstatic --noinput

Проверим каталоги:

ls -lah /var/www/django-app/staticfiles

ls -lah /var/www/django-app/media

Если media пока пустой или вообще еще не создан, это нормально.

Создадим его: mkdir -p /var/www/django-app/media

Теперь Nginx можно явно указать, откуда брать оба типа файлов.

Добавляем location /static/

Откроем конфигурацию сайта: sudo nano /etc/nginx/sites-available/django-app

Добавим отдельный location:

location /static/ {

    alias /var/www/django-app/staticfiles/;

}

Здесь используется именно: alias

Nginx берет остаток URL после /static/ и ищет соответствующий файл внутри указанного каталога.

Например запрос: /static/css/style.css будет соответствовать файлу: /var/www/django-app/staticfiles/css/style.css

Обратите внимание на завершающий /: alias /var/www/django-app/staticfiles/;

В подобных конфигурациях лучше внимательно следить за слешами и не писать их наугад: сочетание location и alias напрямую влияет на итоговый путь к файлу.

Добавляем location /media/

Для пользовательских файлов настройка почти такая же:

location /media/ {

    alias /var/www/django-app/media/;

}

Теперь полная конфигурация может выглядеть примерно так:

server {

    listen 80;

    listen [::]:80;

    server_name example.com www.example.com;

    location /static/ {

        alias /var/www/django-app/staticfiles/;

    }

    location /media/ {

        alias /var/www/django-app/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;

    }

}

Теперь запросы разделяются уже на уровне Nginx.

Static и media он отдает самостоятельно, а всё остальное передает приложению.

Есть здесь и важная оговорка.

Если в media лежат приватные файлы, которые должны быть доступны только авторизованным пользователям, бездумно открывать весь каталог через обычный location /media/ нельзя.

Такая конфигурация подходит для публичных загрузок — например аватаров или изображений товаров.

Для приватных документов нужна отдельная схема контроля доступа.

Проверяем файлы через браузер

Перед применением изменений проверим Nginx: sudo nginx -t

Если видим:

syntax is ok

test is successful

перечитываем конфигурацию: sudo systemctl reload nginx

Теперь найдем любой существующий static-файл.

Например, после collectstatic практически наверняка будут файлы Django Admin: find /var/www/django-app/staticfiles -type f | head

Можно увидеть что-то вроде: /var/www/django-app/staticfiles/admin/css/base.css

Тогда проверяем: http://example.com/static/admin/css/base.css

Или через curl:

curl -I -H "Host: example.com" \

http://127.0.0.1/static/admin/css/base.css

Нас интересует успешный HTTP-код: HTTP/1.1 200 OK

Для media можно временно создать тестовый файл: echo "media works" > /var/www/django-app/media/test.txt

И запросить:

curl -H "Host: example.com" \

http://127.0.0.1/media/test.txt

Если получаем: media works, значит Nginx видит каталог правильно.

После проверки файл можно удалить: rm /var/www/django-app/media/test.txt

Если вместо этого получаем: 403 Forbidden или 404 Not Found стоит проверить две вещи.

Во-первых, правильность пути в alias.

Во-вторых, права на сами каталоги:

namei -l /var/www/django-app/staticfiles

namei -l /var/www/django-app/media

Nginx должен иметь возможность пройти по родительским каталогам и прочитать нужные файлы.

Теперь Nginx обслуживает не только динамические запросы к Django, но и файловую часть приложения.

Остается сделать сайт удобным уже не для самого сервера, а для реального пользователя: подключить домен, а затем заменить обычный HTTP на HTTPS.

Подключаем домен

Создаем DNS A-запись

Начинаем с DNS.

У регистратора домена или DNS-провайдера нужно создать A-запись, которая укажет домен на публичный IPv4-адрес VPS.

Например:

Тип Имя Значение 
А@203.0.113.10 
Аwww203.0.113.10 

Здесь:

  • @ означает корневой домен;
  • www — поддомен www.example.com;
  • 203.0.113.10 — пример адреса VPS.

После сохранения изменения DNS применяются не всегда мгновенно.

Проверить, куда сейчас указывает домен, можно через: dig +short example.com или: nslookup example.com

В результате должен появиться IP нашего сервера: 203.0.113.10

То же самое проверим для www: dig +short www.example.com

Если старый IP все еще возвращается, Nginx трогать пока бессмысленно: запросы просто не доходят до нужного VPS.

Время обновления зависит в том числе от TTL записи и DNS-кэшей, поэтому иногда приходится немного подождать.

Настраиваем server_name и ALLOWED_HOSTS

Когда DNS уже смотрит на наш VPS, нужно настроить два разных уровня.

Первый — Nginx.

Открываем: sudo nano /etc/nginx/sites-available/django-app

И указываем домен проекта в server_name. В нашем примере: server_name example.com www.example.com

Так Nginx понимает, какой server block использовать для запросов к этим именам.

Теперь Django.

В .env ранее было: ALLOWED_HOSTS=example.com,www.example.com

Для своего проекта здесь нужно указать те же доменные имена, которые используются в server_name. В нашем примере оставляем: ALLOWED_HOSTS=example.com,www.example.com 

Django читает эту строку в settings.py и превращает ее в список разрешенных host-имен.

Здесь полезно понимать разницу:

Настройка Кто использует За что отвечает 
server_name Nginx Выбирает конфигурацию для конкретного домена 
ALLOWED_HOSTS Django Разрешает Django обслуживать запросы с этим Host

То есть одного server_name недостаточно.

Nginx может совершенно правильно передать запрос в Gunicorn, но Django затем ответит: DisallowedHost, если домен отсутствует в ALLOWED_HOSTS.

После изменения .env перезапустим Gunicorn: sudo systemctl restart gunicorn

А после редактирования Nginx: sudo nginx -t

Если конфигурация корректна:

syntax is ok

test is successful

применяем: sudo systemctl reload nginx

Проверяем сайт по HTTP

Теперь можно проверить уже всю публичную цепочку.

В браузере открываем: http://example.com

И отдельно: http://www.example.com

Либо используем curl: curl -I http://example.com

Если приложение работает, получим ожидаемый HTTP-ответ, например: HTTP/1.1 200 OK

Можно проверить и подробности: curl -v http://example.com

Если домен открывает Django-приложение, значит сразу несколько уровней уже настроены правильно:

  • DNS ведет на нужный VPS;
  • Порт 80 доступен;
  • Nginx принимает запрос;
  • Server_name совпадает;
  • Gunicorn работает;
  • Django принимает домен через ALLOWED_HOSTS.

Оставлять сайт на обычном HTTP мы, разумеется, не будем.

Подключаем HTTPS через Certbot.

При обычном HTTP данные между браузером и сервером передаются без TLS-шифрования. Это особенно критично для форм авторизации, cookie, административной панели и любых пользовательских данных.

После подключения HTTPS адрес станет выглядеть так:

https://example.com

Для получения бесплатного TLS-сертификата воспользуемся Certbot и Let's Encrypt.

Устанавливаем Certbot и получаем сертификат

Установим Certbot вместе с плагином для Nginx:

sudo apt update

sudo apt install -y certbot python3-certbot-nginx

Проверим: certbot --version

Теперь запросим сертификат:

    sudo certbot --nginx -d example.com -d www.example.com

Здесь: -d example.com и -d www.example.com означают доменные имена, которые должны попасть в сертификат.

Certbot попросит указать email, принять условия использования и затем попробует подтвердить владение доменом.

Поэтому к этому моменту DNS уже должен вести на наш VPS, а Nginx — нормально отвечать по HTTP.

При успешном выпуске Certbot сообщит, что сертификат получен, и сохранит его примерно в: /etc/letsencrypt/live/example.com/

Там находятся, в частности:

fullchain.pem

privkey.pem

Вручную копировать эти файлы в каталог Django не нужно.

Certbot умеет сам настроить Nginx на их использование.

После этого проверим: sudo nginx -t и sudo systemctl reload nginx

Теперь сайт должен открываться через: https://example.com

Проверить можно и из терминала: curl -I https://example.com

Настраиваем редирект на HTTPS

После подключения TLS желательно не оставлять две независимые версии сайта: http://example.com и https://example.com

Обычные HTTP-запросы будем перенаправлять на защищенную версию.

Certbot с плагином Nginx обычно умеет добавить такой redirect автоматически.

В конфигурации в итоге появится логика примерно такого вида:

server {

    listen 80;

    listen [::]:80;

    server_name example.com www.example.com;

    return 301 https://$host$request_uri;

}

Теперь запрос: http://example.com/admin/

перенаправляется на: https://example.com/admin/

Проверим: curl -I http://example.com

Ожидаем код перенаправления: HTTP/1.1 301 Moved Permanently и заголовок: Location: https://example.com/

Здесь стоит сделать еще одну важную настройку на стороне Django.

Поскольку TLS завершается на Nginx, сам Gunicorn получает локальный проксированный запрос. Поэтому Django нужно корректно понимать переданный X-Forwarded-Proto.

В settings.py можно указать:

SECURE_PROXY_SSL_HEADER = (

    "HTTP_X_FORWARDED_PROTO",

    "https",

)

Ранее в Nginx мы уже добавили: proxy_set_header X-Forwarded-Proto $scheme;

Эта настройка особенно полезна, если дальше будут использоваться HTTPS-зависимые механизмы Django.

Для production также стоит включить secure-флаги cookie:

SESSION_COOKIE_SECURE = True

CSRF_COOKIE_SECURE = True

А если приложение принимает POST-запросы через формы или административную панель, укажем доверенный HTTPS-origin:

CSRF_TRUSTED_ORIGINS = [

    "https://example.com",

    "https://www.example.com",

]

После изменения Django-настроек: sudo systemctl restart gunicorn

Проверяем автопродление

Сертификаты Let's Encrypt имеют ограниченный срок действия, поэтому вручную перевыпускать их каждые несколько месяцев было бы сомнительным удовольствием.

Certbot устанавливает механизм автоматического продления.

Проверим соответствующий timer: systemctl status certbot.timer --no-pager

Также можно посмотреть расписание: systemctl list-timers | grep certbot

Но главный тест — имитация продления: sudo certbot renew --dry-run

--dry-run позволяет проверить процесс без реального перевыпуска рабочего сертификата.

Если всё настроено правильно, Certbot успешно пройдет тест.

Можно также посмотреть уже установленные сертификаты: sudo certbot certificates

Там будут показаны домены и срок действия сертификата.

После этого финально проверяем сайт curl -I https://example.com и обычный HTTP: curl -I http://example.com

Первый должен нормально обслуживаться по HTTPS, второй — перенаправляться на него.

Осталась менее приятная, но очень полезная часть — разобрать, куда смотреть, если один из элементов всей этой конструкции внезапно решил не работать.

Если приложение не заработало: разбираем типовые проблемы

502 Bad Gateway: проверяем Nginx и Gunicorn

502 Bad Gateway обычно означает, что Nginx сам работает, но не может получить нормальный ответ от backend.

В нашей конфигурации backend — это Gunicorn на: 127.0.0.1:8000

Начнем с него.

Проверим сервис: sudo systemctl status gunicorn --no-pager

Если Gunicorn остановлен: sudo systemctl start gunicorn или: sudo systemctl restart gunicorn

После этого проверим порт: sudo ss -lntp | grep 8000

Если Gunicorn действительно слушает нужный адрес, увидим 127.0.0.1:8000.

Теперь самый полезный тест: curl http://127.0.0.1:8000

Если Django отвечает напрямую, а через Nginx все равно приходит 502, значит проблема уже ближе к конфигурации proxy.

Проверяем: proxy_pass http://127.0.0.1:8000;

Адрес и порт должны совпадать с тем, что указан в gunicorn.service.

Например, если Gunicorn слушает 8000, а Nginx отправляет запросы на 8080, синтаксис конфигурации может быть абсолютно корректным, но приложение работать не будет.

Именно поэтому: sudo nginx -t

проверяет конфигурацию Nginx на синтаксические ошибки, но не гарантирует, что указанный backend реально существует.

Django не подключается к PostgreSQL

Если Gunicorn запускается, но приложение падает с ошибкой базы данных, возвращаемся к PostgreSQL.

Сначала проверяем сам сервер: sudo -u postgres pg_isready

Затем: sudo systemctl status postgresql --no-pager

Если PostgreSQL работает, пробуем подключиться под тем же пользователем, который указан в .env: psql -h 127.0.0.1 -U django_user -d django_db

Если это подключение не проходит, проблема уже не в Django.

Проверяем:

DB_NAME=django_db

DB_USER=django_user

DB_PASSWORD=...

DB_HOST=127.0.0.1

DB_PORT=5432

Особенно часто встречаются:

  • Неправильный пароль;
  • Ошибка в имени базы;
  • Другой пользователь;
  • Недостаточные права;
  • PostgreSQL не запущен.

Если через psql подключение работает, а Django все равно жалуется, стоит проверить, что .env действительно читается приложением и Gunicorn использует нужное виртуальное окружение.

Static или media возвращают 404

Если само приложение открывается, но CSS пропал, Django Admin выглядит как привет из 2003 года или пользовательские изображения возвращают 404, скорее всего, проблема уже не в Gunicorn.

Для static сначала проверяем, выполнялся ли: python manage.py collectstatic --noinput

и существуют ли файлы: ls -lah /var/www/django-app/staticfiles

Затем смотрим Nginx:

location /static/ {

    alias /var/www/django-app/staticfiles/;

}

Для media:

location /media/ {

    alias /var/www/django-app/media/;

}

Проверяем реальный файл: find /var/www/django-app/staticfiles -type f | head

и запрашиваем его напрямую: curl -I http://example.com/static/admin/css/base.css

Если файл существует на диске, но Nginx его не отдает, смотрим путь в alias и права на каталоги: namei -l /var/www/django-app/staticfiles

Для media логика та же.

Важно помнить и еще одну вещь: collectstatic работает только со static.

Если пользовательский файл отсутствует в MEDIA_ROOT, никакая настройка Nginx его не создаст.

DisallowedHost и ошибки домена

Ошибка вида:

DisallowedHost

Invalid HTTP_HOST header

означает, что запрос дошел до Django, но приложение не принимает указанное доменное имя.

Проверяем .env: ALLOWED_HOSTS=example.com,www.example.com

и соответствующую настройку:

ALLOWED_HOSTS = [

    host.strip()

    for host in os.getenv("ALLOWED_HOSTS", "").split(",")

    if host.strip()

]

После изменения .env перезапускаем Gunicorn: sudo systemctl restart gunicorn

Заодно проверяем Nginx: server_name example.com www.example.com;

И DNS: dig +short example.com

Домен должен вести именно на IP текущего VPS.

Если HTTP работает, а проблемы начинаются только после подключения HTTPS, дополнительно проверяем:

CSRF_TRUSTED_ORIGINS = [

    "https://example.com",

    "https://www.example.com",

]

Это особенно актуально для POST-запросов, форм и Django Admin.

Где смотреть логи

Когда по внешнему виду ошибки ничего не понятно, лучше сразу идти в логи.

Для Gunicorn: sudo journalctl -u gunicorn -n 50 --no-pager

или в реальном времени: sudo journalctl -u gunicorn -f

Для Nginx: sudo tail -f /var/log/nginx/error.log и sudo tail -f /var/log/nginx/access.log

Для PostgreSQL можно начать с: sudo journalctl -u postgresql -n 50 --no-pager

Если ошибка возникает именно внутри Django, она часто всплывет в журнале Gunicorn, потому что именно этот процесс запускает приложение.

Получается довольно практичное правило:

Симптом Куда смотреть сначала 
502 Bad Gateway Gunicorn и proxy_pass
Ошибка подключения к БД PostgreSQL и .env
Static/media дают 404STATIC_ROOT, MEDIA_ROOT, alias
DisallowedHost ALLOWED_HOSTS, server_name, DNS
Непонятная внутренняя ошибка journalctl и Nginx logs

Чаще всего проблема находится именно там, где заканчивается последний успешно работающий слой.

Если Nginx отвечает, но backend недоступен — смотрим Gunicorn. Если Gunicorn жив, но Django падает на запросе — проверяем настройки приложения и PostgreSQL. Если HTML открывается, а CSS нет — уже не трогаем базу и идем в static.

Так диагностика занимает заметно меньше времени, чем попытка одновременно перезапустить вообще всё на сервере.

Заключение

На этом наше Django-приложение прошло путь от обычного проекта на пустом VPS до полноценного production-развертывания.

Мы подготовили Ubuntu, создали отдельное Python-окружение, подключили PostgreSQL и вынесли чувствительные параметры из settings.py. После этого подготовили миграции и static, заменили development server на Gunicorn и передали управление процессом systemd.

Nginx, в свою очередь, стал внешней точкой входа: он проксирует динамические запросы в Gunicorn, самостоятельно раздает static и media, работает с доменом и принимает HTTPS-трафик.

В результате каждый компонент занимается своей задачей, а приложение больше не зависит от открытого SSH-терминала или ручного запуска python manage.py runserver.

При этом production-развертывание на этом не заканчивается навсегда. По мере роста проекта к такой схеме могут добавляться резервное копирование PostgreSQL и media, Redis, Celery, мониторинг, централизованные логи, CI/CD или несколько экземпляров приложения за балансировщиком.

Но фундамент уже готов. А главное — теперь понятно не только какие команды выполнять, но и зачем в этой конструкции вообще понадобился каждый ее элемент.

FAQ

Можно ли развернуть Django без Nginx и открыть Gunicorn напрямую?

Технически — да. Gunicorn можно привязать к публичному интерфейсу и открыть соответствующий порт.

Для обычного production-развертывания удобнее оставить Gunicorn на 127.0.0.1 и поставить перед ним Nginx. Тогда веб-сервер берет на себя HTTPS, домен, static, media и другую работу с внешним HTTP-трафиком.

Нужно ли перезапускать Gunicorn после изменения кода?

Да. Уже запущенные worker-процессы не обязаны автоматически подхватывать измененный production-код.

После обновления проекта обычно выполняют: sudo systemctl restart gunicorn

Если при обновлении изменились модели или static, дополнительно могут понадобиться:

python manage.py migrate

python manage.py collectstatic --noinput

Именно такие последовательности позже часто автоматизируют через CI/CD.

Можно ли оставить SQLite, если сайт небольшой?

Можно.

SQLite не становится запрещенной только потому, что проект оказался на VPS. Для небольшого внутреннего сервиса, прототипа или приложения с очень небольшой нагрузкой ее возможностей может быть достаточно.

PostgreSQL становится особенно полезен, когда появляются параллельные запросы, более серьезная работа с данными, несколько процессов приложения и требования к дальнейшему масштабированию.

Почему после DEBUG=False сайт неожиданно перестал открываться?

Одна из самых частых причин — ALLOWED_HOSTS.

При отключенном debug Django ожидает корректный список разрешенных host-имен. Поэтому production-домен нужно добавить, например, через используемую нами переменную:

ALLOWED_HOSTS=example.com,www.example.com

После изменения окружения не забудьте перезапустить Gunicorn.

Почему Django Admin открывается без стилей?

Обычно это означает, что проблема не в самой административной панели, а в static.

Стоит проверить: python manage.py collectstatic --noinput на наличие файлов в STATIC_ROOT и соответствующий location /static/ в Nginx.

Django Admin использует собственные CSS и JavaScript, поэтому проблемы с раздачей static там становятся особенно заметны.

Нужно ли делать backup каталога staticfiles?

Как правило, staticfiles можно заново получить командой: python manage.py collectstatic

Поэтому гораздо важнее резервировать то, что нельзя восстановить из исходного кода: PostgreSQL, пользовательские media, необходимые секреты и другую уникальную информацию приложения.

Что произойдет, если сертификат Let's Encrypt закончится?

При нормально настроенном Certbot сертификат должен продлеваться автоматически до истечения срока.

Именно поэтому после настройки HTTPS полезно проверить механизм заранее: sudo certbot renew --dry-run

Если тест проходит успешно, вручную перевыпускать сертификат каждый раз не требуется.

Что проверять после каждого обновления Django-приложения?

Минимальный набор зависит от самого обновления, но полезно убедиться, что Gunicorn работает, миграции применены, static актуальны, а приложение отвечает через Nginx.

После более серьезных изменений стоит также посмотреть логи и протестировать основные пользовательские сценарии — открыть сайт недостаточно, если, например, форма авторизации или запись в PostgreSQL уже перестали работать.

Список источников

  1. Django Documentation — Deployment checklist
  2. PostgreSQL Documentation — CREATE ROLE
  3. NGINX Documentation — ngx_http_proxy_module
  4. Certbot Documentation — Nginx instructions

Подпишитесь на нашу рассылку и получайте статьи и новости

    Ознакомьтесь с другими нашими материалами