Django, PostgreSQL, Gunicorn/uWSGI, Nginx

Существует некоторое количество статей и инструкций по развертыванию проекта Django с Nginx, Gunicorn\uwsgi. Я столкнулся с проблемой, что не во всех статьях подробно описаны все шаги и не все статьи объясняют смысл шагов при деплое проекта. В данной статье я постараюсь агрегировать полученные теоретические и практические знания по деплою Django проекта. В статье не будет описано создание Django-приложения, подразумевается, что оно уже у вас есть и вам необходимо его разместить на стороннем сервере, например, на арендованной VPS.

Перед началом написания пошаговой подробной инструкции по деплою проекта я бы хотел рассказать про протокол WSGI, серверы uWsgi\Gunicorn, Nginx и немного про Docker\docker-compose.

WSGI

WSGI — стандарт (протокол) взаимодействия между Python-программой, выполняющейся на стороне сервера, и самим веб-сервером, например Apache или Nginx. WSGI-серверы появились в силу того, что веб-серверы в то время не умели взаимодействовать с приложениями, написанными на языке Python. В отличие, например, от приложений, написанных на PHP, где достаточно иметь index.php и сконфигурировать Apache и Nginx. Иными словами, WSGI-приложения исполняют код Python.

Существует некоторое количество WSGI-серверов. В данной статье я рассмотрю настройку наиболее популярных — uWSGI и Gunicorn.

Здесь я сделаю небольшое лирическое отступление. Было бы ошибочно не упомянуть о существовании протокола ASGI. Это более современный асинхронный протокол для фреймворков, которые поддерживают более современные технологии веб-разработки, а именно асинхронность, например, FastAPI. Конечно, Django тоже может поддерживать асинхронщину, но все же в первую очередь Django — тяжеловесный веб-фреймворк, с мощной админкой, который разрабатывался для создания блогов, форумов, интернет-магазинов и так далее, когда еще асинхроннасть не была во главе всего.

Nginx

Итак, с протоколом WSGI разобрались, поняли, для чего нам uWsgi или Gunicorn сервер, но зачем же для деплоя Django проекта еще исппользовать Nginx, если уже есть веб-сервер?

Nginx — это HTTP-сервер, обратный прокси-сервер, почтовый прокси-сервер, а также TCP/UDP прокси-сервер общего назначения.

В контексте нашей задачи будем использовать Nginx как обратный прокси-сервер (тип прокси-сервера, который ретранслирует запросы клиентов из внешней сети на один или несколько серверов, логически расположенных во внутренней сети.) То есть, Nginx будет принимать HTTP запросы от клиента (браузера) и направлять их на сервер uWsgi или Gunicorn, смотря что выбираем. Сервер WSGI создает динамический контент, выполняя Python код, который написал разработчик. Далее сервер WSGI возвращает ответ, который часто имеет формат HTML, JSON или XML, а обратный прокси-сервер (Nginx) отправляет его клиенту.

Связка Client-Nginx-wsgi

Связка Client-Nginx-wsgi

Также Nginx еще будет работать в качестве веб-сервера для предоставления статических ресурсов, таких как изображения, CSS-стили и JavaScript-файлов.

Deploy

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

Итак, начнем подробное описание всех шагов деплоя.

В статье рассмотрен пример деплоя на сервер под управлением ОС «Ubuntu 20.x»

Начнем с загрузки и установки всех необходимых элементов из репозиториев Ubuntu. Обновим индекс пакетного менеджера apt.

sudo apt update

Установим все необходимые пакеты.

sudo apt install python3-pip python3-dev libpq-dev postgresql postgresql-contrib nginx curl

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

adduser forum_user
usermod -aG sudo forum_user
su - forum_user

Как уже было сказано выше, в данной статье мы рассматриваем ситуацию, в которой у нас уже есть готовое Django-приложение где-то в недрах нашего ssd диска или на github, например.

Представим, что у нас есть некий проект Django, который называется «forum» и представляет из cебя тематический форум с подобным расположением файлов

└── my_super_forum
    ├── config
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── forum
    │   ├── accounts
    │   │  	├── models.py
    │   │  	├── urls.py
    │   │  	├── views.py
    │   │  	└── templates
    │   ├── answers
    │   │  	├── models.py
    │   │  	├── urls.py
    │   │  	├── views.py
    │   │  	└── templates
    │   ├── questions
    │   │  	├── models.py
    │   │  	├── urls.py
    │   │  	├── views.py
    │   │  	└── templates
    │   └── api
    │   │  	├── serializer.py
    │   │  	├── urls.py
    │   │  	└── views.py
    ├── static
    ├── media
    ├── manage.py
    ├── forum_env
    └── requirements.txt

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

└── app
    ├── my_django_app
    │   ├── __init__.py
    │   ├── asgi.py
    │   ├── settings.py
    │   ├── urls.py
    │   └── wsgi.py
    ├── manage.py
    └── requirements.txt

Проект заливается на сервер через scp, либо клонируется из github (могут быть конечно и другие варианты)

scp -r my_super_forum username@remote-server-ip:/home/your_path
cd /home/forum_user
mkdir forum
git clone https://github.com/your-username/your-project-name

Настройка хранилища

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

SQLite — это быстрая и легкая встраиваемая однофайловая СУБД на языке C, которая не имеет сервера и позволяет хранить всю базу локально на одном устройстве. Для работы SQLite не нужны сторонние библиотеки или службы.

НО! Использовать SQLite в «боевом» проекте Django — моветон, поэтому ниже я опишу, как установить, настроить и «подружить» базу данных PostgreSQL с Django.

Почему PostgreSQL? Эта СУБД сейчас достаточно популярна, она более мощная, более гибкая, разрабатывать на ней приятнее, чем на MySQL. Ну и практически во всех вакансиях требуются знания postgres, есть смысл потренироваться)

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

Во время установки Postgres можно создать пользователя операционной системы, например, postgres, который будет являться пользователем-администратором в СУБД. Этого пользователя необходимо использовать для выполнения административных задач. Чтобы войти в сеанс Postgres, используйте sudo и передайте имя пользователя с ключем -u

sudo -u postgres psql

Нам открывается командная строка базы данных для настройки

  1. Создаем базу данных для нашего проекта

CREATE DATABASE forum;
  1. Создаем пользователя базы данных для проекта и выбираем безопасный пароль

CREATE USER forumuser WITH PASSWORD 'password';
  1. Меняем несколько параметров подключения для ускорения операций с базой данных, также настроим часовой пояс

ALTER ROLE forumuser SET default_transaction_isolation TO 'read committed';
ALTER ROLE forumuser SET timezone 'Europe/Moscow';
  1. Предоставляем новому пользователю доступ для администрирования новой БД

GRANT ALL PRIVILEGES ON DATABASE forum TO forumuser;
  1. Выходим из командной строки

\q

Настройка проекта

Перейдем к проверке настроек проекта.

Запускаем виртуальное окружение (если его нет, то создаем командой ниже)

# Создание виртуального окружения
sudo apt install python3.10-venv
python3 venv -v forum_env
source forum_env/bin/activate

Установим необходимые пакеты

pip3 install django gunicorn uwsgi psycopg2-binary
# или
pip3 install -r requirements.txt
deactivate

Выходим из виртуального окружения

deactivate

Устанавливаем и uwsgi и gunicorn, так как далее я буду показывать, как деплоить проект с обеими веб-серверами. Вы уже выберете, какой вам больше подходит. Если вы уже твердо решили, какой вам подходит, тот и оставляйте в команде выше.

Следующим шагом прописываем необходимые настройки проекта в файле settings.py, в моем случае он находится в папке config в корне проекта.

В строчку ALLOWED_HOSTS добавим следующее

ALLOWED_HOSTS = ['your_server_domain_or_IP', 'second_domain_or_IP', . . .,
                 'localhost']

В настройках базы данных укажем Postgres

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql_psycopg2',
        'NAME': 'forum',
        'USER': 'forum tuser',
        'PASSWORD': 'password',
        'HOST': 'localhost',
        'PORT': '',
    }
}

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

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

STATIC_URL = '/static/’
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles/')
STATICFILES_DIRS = [os.path.join(BASE_DIR, "static"),]

Немного расскажу, за что отвечает каждая переменная.

STATIC_ROOT: Абсолютный путь к каталогу, в котором будут собираться статические файлы уже во время деплоя проекта. Файлы собираются командой python3 manage.py collectstatic. Все статические файлы Django скопирует в STATIC_ROOT. В дальнейшем мы будем обслуживать статические файлы с помощью Nginx, то есть в настройках nginx пропишем путь до папки, указанной в STATIC_ROOT. То есть при обращении по адресу http://yourdomain/static/css/base.css Nginx будет искать «css/base.css» в директории, указанной в STATIC_ROOT .

STATIC_URL — это переменная, в которой хранится адрес до статических файлов, в основном используется методом static в шаблонах Django.

Пример:

в нашем случае будет обрабатываться как

И, так как этот href начинается с /, он добавит ваш домен для доступа к статическим файлам: http://yourdomain/static/css/base.css

STATICFILES_DIRS используется для включения каталогов для поиска статики при выполнении python3 manage.py collectstatic

Итого: кладем статику в выбранную директорию (ии), указываем ее\их в STATICFILES_DIRS (сюда будет смотреть django при отладочном режиме), придумываем по какому адресу будут идти обращения на статику — указываем его в STATIC_URL и не забываем указать в дальнейшем в настройках nginx, и наконец указываем директорию в STATIC_ROOT, в которой будут храниться статические файлы (в режиме деплоя) после применения python3 manage.py collectstatic, также не забываем ее указать в настройках nginx.

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

Применяем миграции.

python3 manage.py makemigrations
python3 manage.py migrate

Создаем администратора проекта.

python3 manage.py createsuperuser

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

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

python3 manage.py collectstatic

Чтобы протестировать сервер разработки, вам необходимо разрешить доступ к порту, который вы будете использовать. В этом случае создайте исключение для порта 8000:

sudo ufw allow 8000

Наконец, протестируйте свой проект, запустив сервер разработки Django с помощью следующей команды:

python3 manage.py runserver 0.0.0.0:8000

Откройте браузер на компьютере и проверьте доступность вашего проекта
http://server_domain_or_IP:8000

Настройка Gunicorn.

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

Для того, чтобы все работало на 443 или хотя бы на 80 порту, нам необходим Nginx и gunicorn (их необходимость описана выше)

В первую очередь настроим системные сокеты и служебные файлы для Gunicorn.

Идем в каталог /etc/systemd/system/ и создаем два файла: gunicorn.service и gunicorn.socket (вместо имени «gunicorn», можно использовать любое другое):

cd /etc/systemd/system/
sudo nano /etc/systemd/system/gunicorn.socket

Вставляем следующие строки в файл gunicorn.socket

[Unit]
Description=gunicorn socket

[Socket]
ListenStream=/run/gunicorn.sock

[Install]
WantedBy=sockets.target

[Unit] для описания сокета,
[Socket] для определения местоположения сокета
[Install] чтобы убедиться, что сокет создан в нужное время

sudo nano /etc/systemd/system/gunicorn.service

Вставляем следующие строки в файл gunicorn.service

[Unit]
Description=gunicorn daemon
Requires=gunicorn.socket
After=network.target
[Service]
# имя вашего пользователя
User=forum_user 
#путь до каталога с файлом manage.py
WorkingDirectory=/home/forum_user/forum 
ExecStart=/home/forum_user/forum/forum_env/bin/gunicorn --workers 5 --bind unix:/run/gunicorn.sock config.wsgi:application
#путь до файла gunicorn в виртуальном окружении (также не забудьте указать верный путь до файла wsgi (обычно лежит рядом с settings.py))
[Install]
WantedBy=multi-user.target

[Unit] используется для указания метаданных и зависимостей. Добавляем сюда описание службы и указываем системе инициализации, чтобы она запускала эту службу только после достижения сетевой цели. Поскольку служба использует сокет из файла сокета, необходимо включить директиву Requires, чтобы указать эту связь:

[Service] указываем пользователя и группу, под которыми будет запускаться процесс. Указываем, что обычная учетная запись пользователя является владельцем процесса, поскольку ему принадлежат все соответствующие файлы. Затем пределяем рабочий каталог и указываем команду, которой будет запускаться служба. В этом случае указываем полный путь к исполняемому файлу Gunicorn, который установлен в виртуальной среде. Далее привязываем процесс к сокету Unix, который создался в каталоге /run, чтобы процесс мог взаимодействовать с Nginx. Записываем все данные в стандартный вывод, чтобы процесс Journald мог собирать журналы Gunicorn. Здесь также можем указать любые дополнительные настройки Gunicorn. Для нашего примера мы указали 3 рабочих процесса

[Install] сообщает systemd, с чем связать эту службу, если включен ее запуск при загрузке. Укажем, чтобы эта служба запускалась при запуске и работе обычной многопользовательской системы.

Запускаем Gunicorn

sudo systemctl start gunicorn.socket
sudo systemctl enable gunicorn.socket
sudo systemctl status gunicorn.socket

Наблюдаем следующий вывод

● gunicorn.socket - gunicorn socket
     Loaded: loaded (/etc/systemd/system/gunicorn.socket; enabled; vendor prese>
     Active: active (listening) since Thu 2021-12-02 19:58:48 UTC; 14s ago
   Triggers: ● gunicorn.service
     Listen: /run/gunicorn.sock (Stream)
      Tasks: 0 (limit: 1136)
     Memory: 0B
     CGroup: /system.slice/gunicorn.socket

Dec 02 19:58:48 gunicorn systemd[1]: Listening on gunicorn socket.

Проверяем создание сокета.

file /run/gunicorn.sock

При успешном создании наблюдаем следующее:

/run/gunicorn.sock: socket

Если что-то пошло не так — в первую очередь проверяем журнал

sudo journalctl -u gunicorn.socket

Далее проверяем работу gunicorn

sudo systemctl status gunicorn

Должно появиться следующее

Если обнаружены ошибки — смотри журнал и логи.

sudo journalctl -u gunicorn
nano var/log/syslog

Если меняете в файлах настройки Gunicorn конфигурации, то обязательно рестартим Gunicorn

sudo systemctl daemon-reload
sudo systemctl restart gunicorn

Настройка uWSGI.

Запустить uWSGI можно в командной строке для тестирования конфигурации, но для деплоя будем запускать uWSGI в режиме «императора» (Emperor), который позволяет главному процессу автоматически управлять отдельными приложениями с учетом набора файлов конфигурации.

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

mkdir /etc/uwsgi/sites

Создаем файл настройки конкретно для нашего проекта forum

nano /etc/uwsgi/sites/forum.ini

Добавим следующие настройки в файл forum.ini.

[uwsgi]
project = forum
uid = forum_user
base = /home/%(uid)

chdir = %(base)/%(project)
home = %(base)/%(project)/forum_env
module = %(project)/cinfig.wsgi:application

master = true
processes = 5

socket = /run/uwsgi/%(project).sock
chown-socket = %(uid):www-data
chmod-socket = 660
vacuum = true

Необходимо создать systemd файл для управления процессом uWSGI Emperor и автоматическим запуском uWSGI при загрузке.

sudo nano /etc/systemd/system/uwsgi.service

Вставляем следующие строки в файл:

[Unit]
Description=uWSGI Emperor service

[Service]
ExecStartPre=/bin/bash -c 'mkdir -p /run/uwsgi; chown forum_user:www-data /run/uwsgi'
ExecStart=/usr/local/bin/uwsgi --emperor /etc/uwsgi/sites
Restart=always
KillSignal=SIGQUIT
Type=notify
NotifyAccess=all

[Install]
WantedBy=multi-user.target

На этом этапе мы не сможем успешно запустить службу, поскольку она зависит от доступности пользователя www-data. Нам придется подождать, чтобы запустить службу uWSGI, пока не будет установлен и настроен Nginx.

Настройка Nginx.

В нашем случае Nginx настраивается как обратный прокси-сервер, для перенаправления запросов на Gunicorn, также nginx будет отдавать статику по запросам.

В первую очередь создадим файл настройки.

sudo nano /etc/nginx/sites-available/forum

Внутри откроем новый серверный блок. Начнем с указания, что этот блок должен прослушивать обычный порт 80 и отвечать на доменное имя или IP-адрес вашего сервера:

server {
    listen 80;
    server_name server_domain_or_IP;
}

Далее пропишем, где хранятся статические ресурсы, которые собираются в папку staticfiles

server {
    listen 80;
    server_name server_domain_or_IP;
    
    location /static/ {
        root /home/forum/staticfiles;
    }
}

Создаем самый основной блок для соответствия всем остальным запросам. Внутри этого блока включаем стандартный файл proxy_params из установки Nginx, а затем передаем трафик непосредственно в сокет Gunicorn

server {
    listen 80;
    server_name server_domain_or_IP;
   
    location /static/ {
        root /home/forum/staticfiles;
    }
    location / {
        include proxy_params;
        proxy_pass http://unix:/run/gunicorn.sock;
    }
}

Создаем симлинк в sites-enabled

sudo ln -s /etc/nginx/sites-available/forum /etc/nginx/sites-enabled

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

  1. nginx.conf — общие конфиги всего сервера и всех обслуживаемых сайтов под ним, там подключается всё, что лежит в site-enabled

  2. site-available — конфиги отдельных приложений, туда, например, можно закинуть один для вебдева, другой для вебсокетов, третий для сайта, четвёртый для другого сайта, а пятый вообще для php-fpm и т.д.

  3. sites-enabled — просто включённые сайты — сюда складываются симлинки из site-available, для того, чтобы быстро включить, переключить или отключить какой-то конфиг. Что-то вроде горячей замены.

Проводим проверку синтаксиса

sudo nginx -t

Перезапускаем nginx

sudo systemctl restart nginx

Закрываем ранее открытый 8000 порт

sudo ufw delete allow 8000

Применяем для фаервола настройки nginx

sudo ufw allow 'Nginx Full'

Итого, если ошибок никаких нет, то по указанному в настройках nginx доменному имени можно наблюдать задеплоенный сайт)

P.S. Все ошибки смотрим в логах системы:

sudo nano /var/log/syslog

Итог

В этой статье я попытался расписать, как деплоить ваш Django-проект, а также для чего необходимо выполнять указанные в статье шаги. Эта статья будет полезной для вас, если вы собираетесь приступить к деплою проекта на Django. Каждый шаг деплоя протестирован: если выполнять все шаги друг за другом — ошибки возникать не будут. Всем удачного деплоя!

P.S.: Настройки для https, а также деплой в докере будут описаны в следующей статье.

© Habrahabr.ru