Flask — небольшой и понятный фреймворк, поэтому многие пишут на нём своё первое веб-приложение. На локальной машине строка app.run(debug=True) в конце файла мгновенно запускает приложение и перезагружает его при изменении кода. Но этот встроенный сервер не предназначен для продакшена, а режим debug, оставленный открытым в интернете, превращается в серьёзную дыру в безопасности. Разберём, что нужно изменить, чтобы правильно разместить Flask на обычном хостинге или на VPS.
Почему app.run() не используют на сервере
app.run() запускает сервер разработки из библиотеки Werkzeug. Он удобен для проверки на одной машине, но не рассчитан на нагрузку с множеством запросов, восстановление после падения процесса и строгие требования безопасности. Главная опасность связана с debug=True: в этом режиме на странице ошибки появляется интерактивная консоль, через которую можно выполнить на сервере произвольный Python-код. Хотя консоль защищена PIN-кодом, оставлять её доступной из интернета категорически не рекомендуется. Поэтому на сервере приложение всегда запускается через отдельный WSGI-сервер, а debug выключается.
В качестве примера возьмём сайт misol.uz свадебного зала в Фергане. На сайте есть фотографии залов, страница с ценами и форма бронирования с выбором даты, а данные хранятся в небольшой базе SQLite. Разработчик написал приложение на Flask за неделю, и теперь его нужно вывести в интернет, ничего не сломав.
Приводим приложение к точке входа
Любой WSGI-сервер должен знать, из какого файла и под каким именем брать приложение. Самый аккуратный способ — фабрика приложения и отдельный файл wsgi.py. Код приложения лежит в пакете app, а точка входа — в корне проекта:
# app/__init__.py
import os
from flask import Flask
def create_app():
app = Flask(__name__, instance_relative_config=True)
app.config["SECRET_KEY"] = os.environ["FLASK_SECRET_KEY"]
app.config["DATABASE"] = os.path.join(app.instance_path, "booking.sqlite")
os.makedirs(app.instance_path, exist_ok=True)
from . import views
app.register_blueprint(views.bp)
return app
# wsgi.py
from app import create_app
app = create_app()
SECRET_KEY используется для подписи сессий и форм, поэтому он должен быть длинным, случайным и храниться вне репозитория. Сгенерировать его можно командой python -c "import secrets; print(secrets.token_hex(32))". Файл SQLite хранится в папке instance: база, положенная в static, может оказаться доступной для скачивания через браузер.
Запуск через Passenger на обычном хостинге
Некоторые хостинг-панели запускают Python-приложения с помощью Phusion Passenger. В такой среде Passenger ищет в корне проекта файл passenger_wsgi.py и берёт из него объект с именем application. Файл получается совсем коротким:
# passenger_wsgi.py
import os
import sys
sys.path.insert(0, os.path.dirname(__file__))
from app import create_app
application = create_app()
Зависимости обычно устанавливаются в виртуальное окружение, созданное для этого проекта, и приложение работает с Python из этого окружения. Чтобы после обновления кода сообщить Passenger о необходимости перезагрузить приложение, достаточно создать файл tmp/restart.txt в проекте или обновить его время изменения:
mkdir -p tmp
touch tmp/restart.txt
Не каждый тариф хостинга поддерживает Python, а там, где поддерживает, способ настройки может отличаться. Есть ли на вашем тарифе запуск Python-приложений и какие версии доступны, уточните в кабинете sayt.uz или через техподдержку. Если тариф поддерживает только PHP, для Flask правильнее выбрать VPS.
Запуск на VPS с Gunicorn и Nginx
На VPS у вас полный контроль, и настройка почти такая же, как для других Python-фреймворков. В виртуальное окружение ставится Gunicorn, а приложение запускается через объект app из wsgi.py:
python3 -m venv venv
venv/bin/pip install -r requirements.txt gunicorn
venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 wsgi:app
Эту команду оформляют как сервис systemd, чтобы она сама поднималась после перезагрузки и перезапускалась при падении. Впереди стоит Nginx: он отвечает за HTTPS, раздаёт файлы из /static/ с диска, а остальные запросы передаёт в Gunicorn через proxy_pass http://127.0.0.1:8000;.
Правильный адрес и протокол за прокси
Когда приложение работает за Nginx или другим прокси, Flask видит каждый запрос как пришедший с 127.0.0.1 по обычному HTTP. В результате url_for(..., _external=True) строит неверные ссылки с http://, а в логах все клиенты выглядят с одним и тем же IP. Это исправляет прослойка ProxyFix из Werkzeug, которая читает заголовки, переданные прокси:
from werkzeug.middleware.proxy_fix import ProxyFix
app = create_app()
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1)
Числа означают, сколько прокси стоит перед приложением. Не ставьте их больше реального значения, иначе клиент сможет подделать заголовок и скрыть свой IP. Кроме того, после включения HTTPS сделайте так, чтобы cookie сессии отправлялись только по защищённому соединению: SESSION_COOKIE_SECURE = True.
Чек-лист после размещения
Когда сайт открылся, проверьте несколько вещей. Откройте заведомо несуществующую страницу и убедитесь, что на странице ошибки не видно фрагментов кода или консоли. Отправьте форму бронирования: если у приложения нет права записи в файл SQLite, появится ошибка attempt to write a readonly database, и решается она исправлением прав на папку. Посмотрите также, загружаются ли изображения и CSS и перенаправляется ли HTTP-адрес на HTTPS автоматически. В примере со свадебным залом эти проверки занимают несколько минут, но избавляют от потерянных бронирований в начале сезона.