Настройка Django и Flask c Nginx и Gunicorn в Ubuntu
В этом туториале вы сами развернёте небольшой сайт в production-режиме с помощью Nginx + Gunicorn, совсем как крутые бородатые дядьки-программисты. Сайт тоже будет непростой: он будет состоять сразу из двух фреймворков, Django и Flask. То есть развернуть нужно будет как бы сразу два сайта
В итоге вы получите сайт, который показывает юзерам их IP-адрес:
Что надо знать
Конечно же, без определённых знаний и навыков запустить сайт на сервере не получится. Вот небольшой список того, что вы должны знать и уметь:
- Знаете что такое веб-сервер
- Знаете что такое HTTP запрос/ответ
- Умеете работать с вёрсткой
- Умеете работать с Systemd
- Запускать Gunicorn через Systemd
- Умеете подключаться к серверу по ssh
- Запускаете команды из консоли
Системные требования
- Сервер с ОС 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
Вместо ip 206.189.61.169
подставьте ip вашего сервера.
Откройте ваш сайт в браузере (в нашем случае это 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 он является клиентом. Логика его работы такая:
- Ждём запроса от клиента
- “Пересылаем” запрос клиента в Gunicorn
- Ждём ответа от Gunicorn
- “Пересылаем” ответ 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
В шапке сайта появится иконка и изменится фон: