Публикация Python сайта на базе Flask на веб сервере nginx

Существуют готовые Python хостинги, такие как Heroku и PythonAnywhere, которые позволяют запустить Python код за считанные минуты, но это не наша цель сегодня.

Примечание: виртуальный хостинг подойдет только в том случае, если там глобально установлен Python и для запуска скриптов пользователям доступна обертка FastCGI и SSH доступ. Такой сценарий описан в некоторых статьях хостеров: ActiveCloud и TimeWeb.

Почему мы рассматриваем Flask, а не Django? Он проще и идеально подходит для небольших проектов и тестирования. Django — это профессиональный фреймворк для промышленных проектов.

В мире Python существует довольно много различных супервизоров, предназначенных для запуска веб-приложений Python: uWSGI, gunicorn, gevent, twisted web и т.д. Они выступают подсредником между веб сервером Apache или nginx и обработчиком Python, которые проксируют запросы на супервизор. Однако, пожалуй, наиболее производительным и гибким супервизором является uWSGI. Опять же, для небольших проектов я рекомендую gunicorn, который проще в настройке. Хотя есть масса примеров успешного использования gunicorn в высоконагруженных проектах.

Таким образом в данной статье мы будем использовать связку CentOS + Python + gunicorn + Flask.

План развертывания Flask приложения на VPS сервере

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

  1. Установка python 3 и pip 3 на ОС
  2. Развертывание виртуальной среды virtualenv
  3. Установка Python, Flask и gunicron и обновление pip в virtualenv
  4. Копируем проект Python с локальной машины на хостинг в папку виртуального проекта
  5. Разрешаем порт 5000 в файрволе наружу
  6. Запускаем python с Flask проектом и проверяем его доступность на 5000 порту
  7. Создаем точку входа WSGI
  8. Запускаем gunicorn на 5000 порту и проверяем доступность приложения через супервизор
  9. Деактивируем виртуальную среду
  10. Создаем системный сервис для gunicorn с автозапуском и стартуем его
  11. Настраиваем nginx для проксирования запросов в gunicorn
  12. Перезапускаем nginx

Развертывание Flask по шагам

Шаг 1. Установка python 3 и pip 3 на ОС

В CentOS 7 по умолчанию установлен Python 2.7.5 (можно проверить командой python -V). Нам необходимо установить 3 версию совместно со 2й. Вы можете заменить стандартную версию, изменив символьную ссылку python с /usr/bin/python2 на /usr/bin/python36. Но если кому-то нужна будет старая версия, начнутся проблемы. Например, перестанет работать yum :) Поэтому я рекомендую системный python не трогать.

Существует несколько способов установить python: собрать из исходников (выкачать последнюю версию с python.org), из репозитария epel и из репозитария ius. Сейчас в epel версии 3.6 python и pip доступны такие же, как в ius (раньше были старее), поэтому не вижу смысла заморачиваться в ius. Из исходников стоит ставить только, если вам нужна самая свежая версия 3.8 на момент написания статьи.

yum install -y python36
python3 -V
yum install -y python3-pip
yum install -y python3-devel

Возможно, вам дополнительно потребуются пакеты python3-dev, build-essential, libssl-dev, libffi-dev, python3-setuptools. Их установка на ваше усмотрение.

Шаг 2. Настройка виртуальной среды

Установим пакет virtualenv:

yum install -y python36-virtualenv

Создадим и перейдем в папку, где хотим создать наш проект. Укажите такое название папки, которое у вас используется в самом проекте в PyCharm. У меня это не корневая папка корневой директории аккаунта (так сложнее, но необходимо, когда у вас несколько сайтов на одной аккаунте).

cd /home/user/public_html/py/testsite/

Создаем тестовый проект в виртуальной среде:

python3 -m venv testprj

Локальные копии Python и pip будут установлены в каталог testprj в каталоге вашего проекта.

Прежде чем устанавливать приложения в виртуальной среде, ее нужно активировать. Для этого нужно ввести следующую команду:

source testprj/bin/activate

Командная строка изменится, показывая, что теперь вы работаете в виртуальной среде. Она будет выглядеть примерно так: (testprj) user@host:~/testsite$.

Шаг 3. Установка пакетов в виртуальной среде

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

Вначале мы установим wheel с локальным экземпляром pip, чтобы убедиться, что наши пакеты будут устанавливаться даже при отсутствии архивов wheel, апгрейдим pip и устанавливаем Flask и gunicorn:

pip install wheel
pip install --upgrade pip
pip install gunicorn flask flask-sqlalchemy

Примечание: В виртуальной у нас стоит только 3 версия python и pip, поэтому мы используем команды python и pip, а не python 3 и pip3.

Шаги 4-6. Заливаем и тестируем приложение в виртуальной среде

Копируем по SFTP/FTP файлы в папку вашей виртуальной среды, в моем случае это /home/ipnets/public_html/py/testsite/ вместе с папками static и templates, которые обязательны для Flask проекта.

Также необходимо убедиться и исправить ваш код Python, чтобы он запускался не только на localhost (по умолчанию это так), но и на любом IP. Для этого в коде главной станицы проекта, с помощью которой вы запускаете приложение, укажите host:

if __name__ == "__main__":
    app.run(host='0.0.0.0')

Проверяем запуск приложения и открываем его через браузер. Мы помним, что Flask по умолчанию слушает на 5000 порту. Для того, чтобы открыть сайт из интернета на этом порту, необходимо разрешить это порт в файрволе. В iptables это делается так:

iptables -A INPUT -p tcp -m tcp --sport 5000 -j ACCEPT
iptables -A OUTPUT -p tcp -m tcp --dport 5000 -j ACCEPT

Структура папок проекта Flask

Приложение у меня вызывается файлом app.py:

python app.py

После запуска вы должны увидеть следующее:

* Serving Flask app "app" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

Если все открылось по http://your_server_ip:5000 — python и Flask проект настроены верно в виртуальном окружении. Когда вы закончите, нажмите CTRL+C в окне терминала, чтобы остановить сервер разработки.

Шаг 7. Создание точки входа WSGI

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

Мы назовем этот файл wsgi.py:

vim wsgi.py

Содержимое файла:

from app import app
 
if __name__ == "__main__":
app.run()

Здесь важно, что первый app — это название вашего приложения, а второй app — это экземпляр объекта, который мы импортируем из нашего приложения (по сути, объявленная в приложении переменная app). У меня они совпадают, но это случайность.

Шаг 8-9. Настраиваем gunicorn в виртуальной среде

Нужно убедиться, что Gunicorn может правильно обслуживать приложение. Для этого нужно просто передать имя нашей точки входа. Оно составляется из имени файла модуля (без расширения .py) и имени вызываемого элемента приложения. В нашем случае это wsgi:app.

Также мы укажем интерфейс и порт 5000 для привязки, чтобы приложение запускалось через общедоступный интерфейс. Во время тестирования я обнаружил, что если мы делаем более одного обращения к Gunicorn, то он зависает и выдает 502 ошибку Gateway Timeout. Для решения этой проблемы я просто добавил воркеров, иначе gunicorn уходит в бесконечный цикл.

gunicorn --bind 0.0.0.0:5000 wsgi:app --workers=3

Опять же, если приложение открывается через браузер на 5000 порту, то все хорошо. Можно закрыть gunicorn, нажав CTRL+C в окне терминала и деактивировать среду:

deactivate

Теперь любые команды Python снова будут использовать системную среду Python.

Шаг 10. Создаем системный сервис для gunicorn

Мы создадим файл служб systemd. Создание файла элементов systemd позволит системе инициализации Ubuntu автоматически запускать Gunicorn и обслуживать приложение Flask при загрузке сервера.

Для начала создаем файл с расширением .service в каталоге /etc/systemd/system:

vim /etc/systemd/system/gunicorn.service

Содержимое файла будет следующим:

1
2
3
4
5
6
7
8
9
10
11
12
13
[Unit]
Description=Gunicorn instance to serve testrj
After=network.target
 
[Service]
User=ipnets
Group=nginx
WorkingDirectory=/home/ipnets/public_html/py/testsite
Environment="PATH=/home/ipnets/public_html/py/testsite/testprj/bin"
ExecStart=/home/ipnets/public_html/py/testsite/testprj/bin/gunicorn --workers 3 --bind 127.0.0.1:5000 wsgi:app
 
[Install]
WantedBy=multi-user.target

Мы начнем с раздела [Unit] этого файла, где указываются метаданные и зависимости. Здесь мы разместим описание службы и предпишем системе инициализации запускать ее только после достижения сетевой цели.

В разделе [Service] указывается пользователь и группа, от имени которых мы хотим запустить данный процесс. Сделаем владельцем процесса учетную запись обычного пользователя, поскольку этот пользователь является владельцем всех соответствующих файлов. Также назначим владельцем группу nginx, чтобы упростить коммуникацию Nginx с процессом Gunicorn (в некоторых конфигурациях это будет группа www-data или nobody).

Теперь укажем рабочий каталог приложения и зададим переменную среды PATH, чтобы система инициализации знала, что исполняемые файлы этого процесса находятся в нашей виртуальной среде. Systemd требует, чтобы мы указывали полный путь исполняемого файла Gunicorn, установленного в нашей виртуальной среде. Также укажем команду для запуска службы gunicorn. Эта команда будет выполнять следующее:

Когда Gunicorn запускается, он создает TCP сокет или Unix сокет файл и использует его, чтобы слушать запросы от nginx. Соответственно, существует два способа биндинга (привязки) gunicorn к nginx: через TCP порт или через sock файл. Стандартным по умолчанию (и менее ресурсоемким) является способ через sock файл, однако с ним бывают проблемы. В частности, nginx не всегда может прочитать этот файл. В таком случае вы получите ошибку 502.

В частности, у меня были проблемы с unix сокет файлом, менно из-за этого я выбрал способ с TCP сокетами выше. Однако. если вы хотите использовать чаще рекомендуемый способ с Unix сокетами, то вам надо будет изменить строку запуска gunicorn на следующую:

ExecStart=/home/ipnets/public_html/py/testsite/testprj/bin/gunicorn --workers 3 --bind unix:testprj.sock -m 007 wsgi:app

В ней происходит создание и привязвка к файлу сокетов Unix <span class="highlight">testprj</span>.sock в каталоге нашего проекта. Мы зададим значение umask 007, чтобы при создании файла сокета предоставлялся доступ для владельца и для группы, а любой другой доступ ограничивался.

Важно! sock файл будет создан при запуске службы от имени пользователя и группы, указанной в файле конфигурации службы. Вы должны убедиться, что эти права позволят группе, запустившей nginx, читать и писать в этот файл, в противном случае будет ошибка 502.

Проверить, под каким пользователем запущен nginx можно в файле конфигурации: /etc/nginx/nginx.conf

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

Теперь мы запустим созданную службу Gunicorn, и активируем ее запуск при загрузке системы:

systemctl start gunicorn
systemctl enable gunicorn
systemctl status gunicorn

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

Шаг 12. Настраиваем nginx для проксирования запросов в gunicorn

Сервер приложений Gunicorn должен быть запущен и ожидать запросы файла сокета в каталоге проекта или на порту TCP сокета. Теперь мы настроим Nginx для передачи веб-запросов на этот сокет. Для этого мы сделаем небольшие добавления в его файл конфигурации.

В зависимости от вашей конфигурации nginx, файлы виртуальных хостов будут лежать по разному пути. Это может быть или /etc/nginx/sites-available/ или, как в моем случае (я использую связку nginx+apache), в /etc/nginx/conf.d/vhosts/. Для каждого сайта, хоста, поддомена в моем случае я использую отдельный conf файл, который подтягивается через агрегирующий файл /etc/nginx/conf.d/vhosts.conf.

В частности, для данного проекта я создал отдельный поддомен и отдельный conf файл с таким содержимым:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
	listen 10.20.30.40:80;	
	server_name my.domain.ru  www.my.domain.ru;
 
location / {
		proxy_set_header Host $http_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_pass http://127.0.0.1:5000;
	}
 
	location ~ /\.ht    {deny all;}
	location ~ /\.svn/  {deny all;}
	location ~ /\.git/  {deny all;}
	location ~ /\.hg/   {deny all;}
	location ~ /\.bzr/  {deny all;}
	disable_symlinks if_not_owner from=/home/ipnets/public_html/py;
 
}

В данном файле вам надо заменить IP в строке listen на ваш публичный, server_name на ваш домен и отредактировать строку proxy_pass. Она должна полностью соответствовать тому, что вы указали в биндинге gunicorn.

Если вы использовали Unix сокет, то ваша строка будет выглядеть так:

proxy_pass http://unix:/home/user/public_html/py/testprj.sock;

Если вы используете sock файл, то пользователя, которым им владеет, нужно добавить в группу, под которой запущен nginx:

usermod -a -G ipnets nginx

Проверим конфигурацию nginx на ошибки:

nginx -t

Если все хорошо, то перезапускаем nginx и проверяем, что сайт открывается по адресу поддомена или домена.

systemctl restart nginx
[Посещений: 546, из них сегодня: 8]

Понравилась публикация? Почему нет? Оставь коммент ниже или подпишись на feed и получай список новых статей автоматически через feeder.