Перейти к содержанию

Веб-приложение как сервис systemd

PHP-FPM управлял исполнением PHP через собственный пул. Теперь рассмотрим приложение, которое слушает локальный HTTP-порт и живёт независимо от отдельного запроса. Для учебного release-lab-app используем Python WSGI и Gunicorn, а управление процессом поручим systemd. Это дополнительная ветка стенда, не новый способ генерации Markdown и не перевод ProfessorWeb на серверный Python.

Приложение слушает 127.0.0.1:8000. Nginx принимает внешние HTTPS-запросы и передаёт выбранные прикладные пути этому процессу. Пользователь releaseapp читает код, но не управляет каталогами выпусков. Короткая учебная программа нужна, чтобы видеть весь маршрут и не объяснять настройки служб на несуществующем приложении.

Приложение и сервер приложения

WSGI задаёт интерфейс между Python-программой и обслуживающим её сервером. Наша функция получает сведения о запросе, выбирает статус и возвращает байты. Для production-процесса берём Gunicorn, а не временный сервер разработки. Конкретную поддерживаемую версию зависимости фиксирует читатель при подготовке стенда: тег «последняя» не является паспортом окружения. Официальное руководство запуска Gunicorn.

# app.py внутри прикладного выпуска
import json
from pathlib import Path

RELEASE = json.loads(Path("release.json").read_text())["release"]

def application(environ, start_response):
    path = environ.get("PATH_INFO", "/")
    if path in ("/", "/health"):
        status = "200 OK"
        payload = {"component": "release-lab-app", "release": RELEASE}
    else:
        status = "404 Not Found"
        payload = {"error": "not found"}
    body = json.dumps(payload).encode("utf-8")
    start_response(status, [
        ("Content-Type", "application/json; charset=utf-8"),
        ("Content-Length", str(len(body))),
        ("Cache-Control", "no-store"),
    ])
    return [body]

Рядом существует release.json с номером, например app-r1. Программа читает его при запуске, поэтому ответ сообщает выпуск процесса, а не содержимое ссылки в произвольный поздний момент. Виртуальное окружение .venv готовят по конечному пути каждого выпуска до переключения. Его не копируют без разбора с ноутбука: пути интерпретатора, система и двоичные зависимости могут отличаться. Исходный код приложения и необходимые зависимости имеют собственный паспорт подготовки.

Зачем нужен менеджер процесса

Запуск из открытой SSH-сессии может закончиться при её закрытии и не определяет поведение после перезагрузки машины. Systemd описывает сервис, пользователя и порядок управления. В этом случае процесс не должен самостоятельно уходить в фон: менеджер отслеживает его жизненный цикл. Журналы направляем в стандартные потоки, которые попадут в системный journal. Отдельный произвольный файл без ротации не нужен для минимального примера.

Подготовим unit /etc/systemd/system/release-lab-app.service. Все упомянутые каталоги, пользователь и виртуальное окружение должны уже существовать. Файл переменных находится вне кода и читается менеджером службы. Он может содержать настройки окружения; номер выпуска наша программа получает из паспорта, чтобы не требовать синхронной ручной правки двух разных мест.

[Unit]
Description=release-lab WSGI application
After=network.target

[Service]
Type=simple
User=releaseapp
Group=releaseapp
WorkingDirectory=/srv/release-lab-app/current
EnvironmentFile=/etc/release-lab/app.env
ExecStart=/srv/release-lab-app/current/.venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 2 --access-logfile - --error-logfile - app:application
Restart=on-failure
RestartSec=5
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Restart=on-failure задаёт восстановление после некоторых нештатных завершений, а не гарантию здоровья приложения. Процесс способен работать и отвечать ошибкой на каждый запрос. Интервал предотвращает мгновенный цикл попыток; systemd также имеет собственные ограничения частоты запуска. Установленные значения проверяют по версии системы, не добавляя неподдерживаемые новые параметры из текущей документации. Первичная справка systemd о сервисах.

Прокси и граница доступа

Порт слушает только loopback, поэтому внешний посетитель проходит через Nginx. Фрагмент ниже относится к отдельному прикладному виртуальному сайту. Он передаёт весь путь приложению и сохраняет значимые сведения о внешнем соединении. Если приложение использует такие заголовки для формирования URL, оно должно доверять только своему прокси, а не любому входящему клиенту.

location / {
    proxy_pass http://127.0.0.1:8000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Request-ID $request_id;
}

Это не замена корня статического release-lab: прикладной сайт изолирован. Если требуется отдавать часть ресурсов напрямую, для них добавляют отдельные маршруты с понятным приоритетом. Особенности proxy_pass, включая различие вариантов с URI и без него, влияют на переданный путь. Модуль проксирования Nginx.

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

Будущий читатель после установки unit выполняет перечитывание определений, запускает службу и проверяет локальный /health. Затем смотрит внешний ответ через Nginx. Такая последовательность разделяет уровни: если локальный ответ неверен, менять сертификат нет смысла; если локально всё работает, а снаружи 502, исследуют прокси и адрес upstream. Никакие из этих команд в процессе подготовки урока не выполнялись.

sudo systemctl daemon-reload
sudo systemctl enable --now release-lab-app
sudo systemctl status release-lab-app
sudo journalctl -u release-lab-app --since "10 minutes ago"
curl http://127.0.0.1:8000/health

enable связывает службу с будущим запуском системы, --now дополнительно запускает её сейчас. Статус active показывает жизненный цикл процесса, а HTTP-проверка — ограниченное поведение. Если unit не стартует, журнал может показать отсутствующее виртуальное окружение, неверный рабочий каталог или ошибку чтения паспорта. Эти причины исправляют в подготовке выпуска, не выдавая приложению административные права ради обхода отказа.

Завершение процесса и готовность

При остановке сервис получает сигнал завершения. У сервера приложения есть время закончить активные запросы, а у менеджера — предел ожидания. Эти интервалы нужно согласовать с допустимой длительностью работы. Если предел слишком короток, запросы оборвутся; если безмерно длинен, неудачный процесс задержит выпуск. В нашем приложении ответы короткие, поэтому сложная схема остановки не требуется. При появлении длительных операций их лучше вынести в отдельную задачу и не рассчитывать, что каждое обновление бесконечно ждёт посетителя. Нельзя убивать все Python-процессы по имени: служба определяет конкретную группу процессов, которой следует управлять.

Запущенный процесс также не всегда готов принимать полезные запросы сразу. Он может загружать зависимости или устанавливать связь с базой. Type=simple описывает запуск без специального протокола готовности, поэтому внешний допуск после команды должен подтверждаться наблюдаемым ответом. В более сложном проекте выбирают механизм readiness или отдельную последовательность проверки перед переключением прокси. Для учебного сервиса достаточно получить локальный номер и нужный статус, затем проверить внешний маршрут. Если приложение постоянно перезапускается из-за неверной настройки, это отражается в журнале; очередная автоматическая попытка не считается восстановлением, пока ожидаемый HTTP-ответ не получен. Так история процесса и здоровье приложения остаются разными, но связанными наблюдениями.

Обновление и возврат процесса

Смена current не заменяет уже загруженный Python-код. Для этого нужен согласованный перезапуск или механизм плавного обновления Gunicorn. В простом стенде допускаем короткий перезапуск и фиксируем окно; без дополнительной схемы двух экземпляров обещать отсутствие перерыва нельзя. После запуска новый /health должен показать новый прикладной номер. Возврат включает прежний каталог и повторный запуск совместимого окружения.

Два worker в примере — учебная начальная настройка, не расчёт мощности. У программы могут появиться база и долгие операции; тогда наблюдают память, длительность запросов и очередь. Следующий урок предложит альтернативное управление тем же приложением через Docker Compose. Эти варианты будут использовать один локальный порт по очереди, а не одновременно.