Настройка Django и Flask c Nginx и Gunicorn в Ubuntu

В этом туториале вы сами развернёте небольшой сайт в production-режиме с помощью Nginx + Gunicorn, совсем как крутые бородатые дядьки-программисты. Сайт тоже будет непростой: он будет состоять сразу из двух фреймворков, Django и Flask. То есть развернуть нужно будет как бы сразу два сайта 🙂

В итоге вы получите сайт, который показывает юзерам их IP-адрес:

Что надо знать

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

Системные требования

  • Сервер с ОС Ubuntu 20.04 LTS
  • Python3

1. Запустите проект на сервере

Перед тем как работать с Nginx/Gunicorn, стоит хоть раз запустить сайт на сервере. А то вдруг вам подсунули нерабочие исходники?

Подключитесь к своему серверу и подготовьте его к установке пакетов Python:

$ ssh root@206.189.61.169  # Замените IP-адрес на свой
# apt update
# apt install python3-pip
# pip3 install virtualenv

Скачайте на сервер код проекта и установите его зависимости:

# cd /opt/
# git clone git@github.com:devmanorg/myip-multi-framework.git
# cd /opt/myip-multi-framework/
# virtualenv --python=python3 venv
# source venv/bin/activate
# pip install -r requirements.txt

2. Запустите сайт на Flask

В репозитории с кодом есть насколько папок. В одной из них лежит проект на Flask. Раз уж вы собрались запускать всё по-взрослому, то давайте использовать не отладочный сервер, а Gunicorn:

# cd /opt/myip-multi-framework/flask_project/
# /opt/myip-multi-framework/venv/bin/gunicorn -b "206.189.61.169:8080" app:app

Откройте ваш сайт в браузере (в нашем случае это http://206.189.61.169:8080). Вы увидите страничку, как на скриншоте:

3. Демонизируйте Flask

Есть ещё один нюанс: стоит вам только закрыть терминал, и сайт сразу упадёт. Неужели теперь нельзя выключать компьютер? И что толку от сервера? Нужно ваш Flask демонизировать с помощью systemd.

Для этого создайте новый юнит-файл: /etc/systemd/system/flask.service . Вот готовый конфиг для этого юнита. Осталось только его скопировать.

Включите flask.service:

# systemctl start flask.service
# systemctl enable flask.service

Зайдите на сайт, и проверьте, что он продолжает работу:

4. Запустите Django

Сайт не ограничивается Flask’ом, нужно запустить ещё и Django. Причём, параллельно, одновременно с Flask.

Откройте новый терминал и снова подключитесь к серверу:

# ssh root@206.189.61.169
# cd /opt/myip-multi-framework/
# source venv/bin/activate
# cd /opt/myip-multi-framework/django_project/
# ./manage.py migrate  # создаёт базу данных SQLite

Так как порт 8080 уже занят частью сайта на Flask, Django будет жить на 8081:

# ./manage.py runserver "206.189.61.169:8081"

Откройте в браузере http://206.189.61.169:8081 (подставьте свой ip). Скорее всего вы получите такую ошибку:

Django увидела, что сайт запущен на IP-адресе, которого нет в ALLOWED_HOSTS. Django поднимает тревогу: либо настройки неправильные, либо кто-то пытается взломать ваш сайт!

В README проекта указана дополнительная настройка DJ_ALLOWED_HOSTS. Нужно её заполнить, чтобы Django не переживала:

# DJ_ALLOWED_HOSTS="206.189.61.169" ./manage.py runserver "206.189.61.169:8081"

Django отвечает за API сайта, оно находится по ссылке 206.189.61.169:8081/api/ip/. Проверьте, что оно работает:

5. Демонизируйте Django

Так же как и с Flask, стоит запустить Django из systemd, чтобы она не выключилась, когда вы выйдите с сервера. Создайте файл с конфигом для /etc/systemd/system/django.service и добавьте в него второй конфиг, уже для Django.

После этого остаётся только включить django.service:

# systemctl start django.service
# systemctl enable django.service

Зайдите на сайт и проверьте, что он работает: 206.189.61.169:8081/api/ip/.

6. Добавьте Nginx

Установите на сервер Nginx

# apt install nginx

Nginx запустится сам сразу по окончанию установки. Он займёт стандартный для HTTP порт 80. Проверьте его работу, откройте в браузере страницу по адресу http://206.189.61.169

Итак, мы запустили три HTTP-сервера, каждый из них слушает свой порт:

  • 206.189.61.169:80 – Nginx
  • 206.189.61.169:8080 – Flask
  • 206.189.61.169:8081 – Django

Получился странный сайт: пользователям надо вбивать какие-то порты, чтобы попасть на нужный веб-сервер. Очевидно, в жизни так никто делать не будет. Нужно как-то их “объединить”, чтобы пользователь просто заходил на сайт, а всю возню с портами за него делал кто-то другой.

Nginx возьмёт на себя эту работу. Он будет “главным” веб-сервером, будет сам распределять что и на какой порт отправить.

Для начала “объединим” два первых сервера, Nginx и Flask, в один. Сделаем так, чтобы все пользователи общались только с Nginx, а о существовании Flask даже не догадывались. Такая схема работы называется reverse proxy:

Nginx сразу выступает в двух ролях: для браузера он является сервером и отвечает на HTTP запросы, а для Gunicorn он является клиентом. Логика его работы такая:

  1. Ждём запроса от клиента
  2. “Пересылаем” запрос клиента в Gunicorn
  3. Ждём ответа от Gunicorn
  4. “Пересылаем” ответ Gunicorn клиенту

Слово “пересылаем” здесь взято в кавычки, потому что на деле Nginx создаёт новый HTTP запрос, очень похожий на старый, но всё же немного другой. Подробнее об отличиях мы поговорим ниже в этом туториале.

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

Реализуем схему, как на иллюстрации выше. Для этого создадим новый файл конфигурации Nginx /etc/nginx/sites-enabled/myip-multi-framework.

server {
  listen 206.189.61.169:80;  # ! Замените адрес на свой
  location / {
    proxy_pass http://206.189.61.169:8080/;  # ! Замените адрес на свой
  }
}

Конфиг состоит буквально из нескольких строк кода. Давайте разберём их по частям.

Первая строка сообщает Nginx о том, чтобы он применил все эти настройки к запросам, прилетающим на порт 80 по адресу 206.189.61.169:

listen 206.189.61.169:80;

Следующая строка открывает вложенный блок настроек. Он предназначен для HTTP-запросов, начинающихся со слеша /, а это без исключения все входящие запросы:

location / {

Последняя строка заставляет Nginx “пересылать” все запросы на порт 8080 к Gunicorn и Flask:

proxy_pass http://206.189.61.169:8080/;

Настройки для Nginx готовы. Применим их:

# systemctl reload nginx.service

Теперь на стандартном 80 порту отображается не приветствие Nginx, а сервис MyIP:

7. Почините reverse proxy

Обратите внимание на то, какой IP адрес отображает страница сайта. Это же не ваш IP, это адрес сервера! Давайте разберёмся с этим.

Сервер Nginx выступает посредником между клиентом и Gunicorn. Он копирует каждый входящий HTTP-запрос и отправляет копию в Gunicorn. И хотя копия эта максимально точна, но и отличия в ней есть. Если Nginx получает запрос напрямую от клиента и знает IP-адрес отправителя, то Gunicorn общается лишь с Nginx и сам узнать адрес клиента не может.

Проблему потери данных можно решить дополнительными HTTP-заголовками. Nginx можно так настроить, чтобы он не только копировал данные из входящего запроса, но и добавлял ещё несколько полей:

  • Host – куда клиент изначально отправлял запрос
  • X-Real-IP – IP адрес клиента
  • X-Forwarded-For – IP адреса клиента и всех прокси-серверов, через которых прошёл запрос
  • X-Forwarded-Proto – исходный протокол, был то http или https

В стандартной поставке у Nginx уже есть такие настройки, достаточно их подключить:

server {
  listen 206.189.61.169:80;
  location / {
    include '/etc/nginx/proxy_params';  # добавляем прокси-заголовки
    proxy_pass http://206.189.61.169:8080/;
  }
}
# systemctl reload nginx.service

Теперь на стороне Flask добавляем поддержку нового HTTP-заголовка X-Real-IP. Редактируем файл flask_project/app.py:

# Файл /opt/myip-multi-framework/flask_project/app.py
...

@app.route('/')
def index():
    ip_address = request.headers.get('X-Real-IP', request.remote_addr)
    return render_template('index.html', ip_address=ip_address)
# systemctl restart flask.service

Проверяем работу сайта. Теперь IP клиента будет отличаться от адреса сервера:

8. Подключите Django

Теперь тот же трюк проворачиваем с Django. Из всех запросов к Nginx выделим те запросы, чьи адреса начинаются с префикса /api/, и “перешлём” их к Django.

Добавляем в конфиг Nginx новый раздел для Django:

# файл /etc/nginx/sites-enabled/myip-multi-framework
server {
  listen 206.189.61.169:80;  # ! Замените адрес на свой

  location / {
    include '/etc/nginx/proxy_params';
    proxy_pass http://206.189.61.169:8080/;  # ! Замените адрес на свой
  }

  location /api/ {  # настройки для запросов вида /api/...
    include '/etc/nginx/proxy_params';
    proxy_pass http://206.189.61.169:8081/api/;  # ! Замените адрес на свой
  }
}
# systemctl reload nginx.service

Обратите внимание, что для входящего запроса Nginx выбирает подходящий location, ориентируясь не на порядок следования настроек, а на их специфичность. Чем точнее в location описана схема адресов, тем выше у неё приоритет. Блок настроек внутри location / будет срабатывать только если не найдётся ничего другого, более подходящего. Подробнее про специфичность читайте в документации Nginx.

В настройках есть ещё одна особенность. В proxy_pass для Gunicorn указан не только хост и порт 206.189.61.169:8081, но и префикс адреса /api/:

proxy_pass http://206.189.61.169:8081/api/;

Вот что здесь происходит. Положим, клиент прислал в Nginx запрос на адрес /api/docs/. Подходящие настройки для запроса нашлись внутри location /api/, и поэтому Nginx сразу отсёк от исходного адреса указанный префикс /api/: /api/docs/ —> /docs/. Если ничего не предпринять, то Gunicorn получит запрос на новый адрес /docs/ вместо исходного /api/docs/.

На сайте должно было заработать API. Перейдите по ссылке в верхнем меню, чтобы проверить работу Django:

{"ip": "206.189.61.169"}

Здесь снова всплывает старая проблема, Django видит только IP адрес Nginx, но не клиента. Редактируем файл django_project/django_project/views.py и перезапускаем Django. Здесь всё похоже на Flask за тем исключением, что Django меняет названия HTTP-заголовка X-Real-IP —> HTTP_X_REAL_IP:

# Файл /opt/myip-multi-framework/django_project/django_project/views.py
from django.http import JsonResponse


def get_my_ip(request):
    return JsonResponse({
        'ip': request.META.get('HTTP_X_REAL_IP') or request.META.get('REMOTE_ADDR'),
    })
# systemctl restart django.service

Заходим на страницу с IP и проверяем, что адрес отображается наш, а не сервера:

{"ip": "117.21.216.251"}

9. Спрячьте Gunicorn

На сервере крутятся сразу три веб-сервера – один Nginx и два Gunicorn. И все они доступны из внешнего мира. Пользователь сайта может сам выбрать к кому из них обратиться: к Nginx на порту 80, или к Gunicorn на портах 8080 и 8081. Пользы от такой свободы нет, а вот риски возрастают:

  • Gunicorn куда более уязвим к DoS атакам, чем Nginx
  • Путаница в адресах приведёт к путанице в ссылках
  • Механизмы защиты, встроенные в Nginx можно легко обойти, обратившись напрямую к Gunicorn

Gunicorn можно полностью спрятать от пользователя, перевесив его с публичного адреса 206.189.61.169 на внутренний 127.0.0.1.

127.0.0.1 - это IP-адрес, также называемый «локальный хост». Этот адрес используется для установления IP-соединения с той же машиной или компьютером, на которой запущена программа. Если у компьютера отключить интернет, то адрес 127.0.0.1 всё равно будет доступен.

Порты оставим те же, заменим только адреса 206.189.61.169 —> 127.0.0.1. Изменения коснутся трёх файлов c настройками:

  • /etc/systemd/system/flask.service
  • /etc/systemd/system/django.service
  • /etc/nginx/sites-enabled/myip-multi-framework

Поменяйте эти настройки и перезапустите веб-серверы:

# systemctl daemon-reload
# systemctl restart flask.service
# systemctl restart django.service
# systemctl reload nginx.service

Теперь проверьте работу сайта. На порту 80 должны работать и главная страница сайта, и API, а на портах 8080 и 8081 браузеру никто не ответит:

10. Подключите статику

На сайте MyIP есть несколько картинок, но загрузить их с сервера браузер не может. Проблема хорошо заметна в консоли браузера:

Проблемы возникают при загрузке двух файлов:

http://206.189.61.169/static/logo.png
http://206.189.61.169/static/background.svg

Эти файлы logo.png и background.svg есть в репозитории проекта. Лежат они в каталоге static.

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

Добавьте в настройки Nginx ещё один location для обработки файлов статики:

server {
  listen 206.189.61.169:80;  # ! Замените адрес на свой

  location / {
    include '/etc/nginx/proxy_params';
    proxy_pass http://127.0.0.1:8080/;
  }

  location /api/ {
    include '/etc/nginx/proxy_params';
    proxy_pass http://127.0.0.1:8081/api/;
  }

  location /static/ {  # обслуживаем файлы статики
    root '/opt/myip-multi-framework/';
  }
}
# systemctl reload nginx.service

В шапке сайта появится иконка и изменится фон:

Другие статьи на эту тему


Попробуйте бесплатные уроки по Python

Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.