Акция Панель управления ispmanager для VDS — первый месяц бесплатно
до 31.07.2026 Подробнее
Выберите продукт

Gunicorn под systemd на VDS: socket activation, watchdog и healthcheck без простоя

Настраиваем Gunicorn под systemd на VDS: делаем socket activation через .socket, пишем unit-файл, включаем reload без простоя через HUP и добавляем healthcheck на systemd timer. Разбираем watchdog и таймауты.
Gunicorn под systemd на VDS: socket activation, watchdog и healthcheck без простоя

Зачем вообще связывать Gunicorn с systemd

Gunicorn умеет работать как демон, но в продакшене важнее другое: как его обслуживать без простоев, с понятными логами, лимитами ресурсов и самовосстановлением. На VDS роль «супервизора» логично отдавать systemd: он уже умеет перезапускать сервисы, писать логи в journald, управлять зависимостями и держать сокеты.

Ниже соберём практичную схему для одного хоста: systemd socket activation для Gunicorn, unit-файл сервиса, graceful reload (HUP), базовый healthcheck и watchdog (где это возможно). Цель — чтобы деплой и обслуживание занимали минуты, а не превращались в лотерею.

Базовая модель: Gunicorn за сокетом (и почему это удобно)

Есть два типовых варианта публикации Gunicorn за Nginx:

  • слушать TCP-порт (например, 127.0.0.1:8000) и проксировать;
  • слушать Unix-сокет (например, /run/gunicorn/myapp.sock) и проксировать.

На одном сервере Unix-сокет обычно удобнее: меньше мороки с портами и ACL, плюс чуть меньше накладных расходов. А если сокет держит systemd (socket activation), то точка входа (файл сокета) «живёт» независимо от процесса Gunicorn: сервис можно перезапускать и перегружать, не удаляя сокет и не создавая гонки с Nginx.

Предварительные допущения

Дальше использую условные значения (замените на свои):

  • пользователь: www-data;
  • проект: myapp;
  • директория проекта: /srv/myapp;
  • виртуальное окружение: /srv/myapp/venv;
  • entrypoint: myapp.wsgi:application (Django) или myapp:app (WSGI-приложение).

Установка и подготовка пользователя/папок

Сначала создадим каталоги и права на runtime-папку для сокета. Важно: каталог /run обычно живёт в tmpfs и очищается после перезагрузки, поэтому полагаться на «один раз создал — и навсегда» нельзя. В этом гайде каталог создадим вручную, а затем закрепим через systemd (см. ниже).

sudo mkdir -p /srv/myapp
sudo mkdir -p /run/gunicorn
sudo chown -R www-data:www-data /srv/myapp
sudo chown -R www-data:www-data /run/gunicorn

Если есть возможность — заведите отдельного пользователя под приложение (это лучше с точки зрения безопасности). Но для понятности примера оставим www-data.

FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Схема socket activation: Nginx подключается к Unix-сокету, systemd поднимает Gunicorn и воркеры

systemd socket: socket activation для Gunicorn

Сокет создаётся и держится systemd. Gunicorn подключается к уже открытому файловому дескриптору. Это даёт два практических плюса:

  • можно рестартовать Gunicorn, не теряя «точку входа» (файл сокета создаёт systemd);
  • можно запускать Gunicorn по первому запросу (полезно для редко используемых сервисов).

/etc/systemd/system/gunicorn-myapp.socket

[Unit]
Description=Gunicorn socket for myapp

[Socket]
ListenStream=/run/gunicorn/myapp.sock
SocketUser=www-data
SocketGroup=www-data
SocketMode=0660
RemoveOnStop=true

[Install]
WantedBy=sockets.target

Права на сокет должны совпасть с тем, от кого работает Nginx. Если Nginx не под www-data, подстройте SocketGroup (или пользователя). Если сомневаетесь — сначала проверьте фактического пользователя Nginx командой ps aux | grep nginx.

systemd unit: сервис Gunicorn

Теперь сам сервис. Здесь важны: явный WorkingDirectory, запуск не от root, адекватные таймауты и корректный graceful shutdown/reload.

/etc/systemd/system/gunicorn-myapp.service

[Unit]
Description=Gunicorn for myapp
Requires=gunicorn-myapp.socket
After=network.target

[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/srv/myapp

Environment="PATH=/srv/myapp/venv/bin"
Environment="PYTHONUNBUFFERED=1"

# Важно: bind на fd://0, чтобы принять сокет от systemd
ExecStart=/srv/myapp/venv/bin/gunicorn --workers 3 --worker-tmp-dir /dev/shm --access-logfile - --error-logfile - --bind fd://0 --timeout 30 --graceful-timeout 30 --keep-alive 5 myapp.wsgi:application

ExecReload=/bin/kill -HUP $MAINPID
KillSignal=SIGTERM
TimeoutStopSec=60
Restart=on-failure
RestartSec=2

# Минимальные hardening-настройки (обычно не ломают веб-приложения)
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ProtectHome=true

# Чтобы /run/gunicorn существовал после ребута (если не создаёте каталог иным способом)
RuntimeDirectory=gunicorn
RuntimeDirectoryMode=0755

[Install]
WantedBy=multi-user.target

Почему тут Type=simple, а не Type=notify

На практике с Type=notify всё отлично, когда у вас гарантированно включены уведомления systemd в Gunicorn (и вы понимаете, какой именно механизм используется в вашей версии/сборке). Но если уведомлений нет, unit может «зависнуть» в состоянии activating или работать без ожидаемого watchdog.

Поэтому базовый, максимально переносимый вариант — Type=simple + socket activation + healthcheck (ниже). А если вы точно хотите watchdog через systemd — добавляйте Type=notify и проверяйте, что уведомления реально приходят (см. секцию про watchdog).

Про --worker-tmp-dir и /dev/shm

Перенос временных файлов воркеров в tmpfs иногда снижает дисковую нагрузку и уменьшает шанс упереться в медленный диск. Если RAM мало или наблюдаете давление на память — уберите параметр --worker-tmp-dir.

Graceful reload и «без простоя» на одном хосте

Классика Gunicorn: послать мастер-процессу HUP. Он поднимет новые воркеры и аккуратно выведет старые, дав им доработать текущие запросы (в пределах --graceful-timeout).

sudo systemctl reload gunicorn-myapp.service

«Без простоя» в рамках одного инстанса означает, что вы не делаете полную остановку процесса между версиями. Но если деплой перезаписывает или удаляет файлы, которые ещё нужны старым воркерам (venv, .pyc, шаблоны), ошибки возможны даже при graceful reload.

Практика для спокойных релизов: атомарный деплой (релизы + переключение симлинка) и reload после переключения. Если хотите углубиться именно в этот аспект, пригодится материал про graceful reload и readiness в systemd.

Запуск и проверка

При socket activation достаточно включить сокет: сервис поднимется при первом подключении.

sudo systemctl daemon-reload
sudo systemctl enable --now gunicorn-myapp.socket
sudo systemctl status gunicorn-myapp.socket

Проверим, что сокет слушается:

sudo ss -xlpn | grep myapp.sock

Дальше инициируйте запрос через Nginx (или временно через локальный прокси) и проверьте, что service поднялся:

sudo systemctl status gunicorn-myapp.service
sudo journalctl -u gunicorn-myapp.service -n 200 --no-pager

Healthcheck: что проверять и как реагировать

Факт «процесс жив» почти ничего не значит: воркер может зависнуть, приложение может стабильно отвечать 500, может закончиться место под кэш или логи. Поэтому нужен healthcheck. Минимум — HTTP endpoint /health, который отвечает быстро и предсказуемо.

Каким должен быть endpoint

  • Liveness: «я жив», без тяжёлых запросов и без походов в БД.
  • Readiness: «я готов принимать трафик», можно проверять БД/кеш/очередь, но со строгим таймаутом.

Если у вас один endpoint, делайте его ближе к liveness, иначе вы сами устроите флаппинг (healthcheck будет провоцировать рестарты в моменты деградации).

systemd timer для healthcheck с автоперезапуском

Один из рабочих подходов на небольших проектах: отдельный systemd service+timer, который раз в N секунд делает запрос и при проблеме перезапускает Gunicorn. Это не заменяет мониторинг, но закрывает класс «тихо зависло и висит сутки».

/usr/local/bin/myapp-healthcheck.sh

#!/bin/sh
set -eu

URL="http://127.0.0.1:8000/health"

if ! curl -fsS --max-time 2 "$URL" >/dev/null; then
  systemctl restart gunicorn-myapp.service
fi

Важно: скрипт предполагает, что healthcheck доступен на loopback-порту. Если у вас Gunicorn только на Unix-сокете, есть два безопасных варианта:

  • отдать /health через Nginx только на 127.0.0.1 и проверять его;
  • поднять отдельный loopback listener только под health (не публиковать наружу).

Дайте права на запуск:

sudo chmod 0755 /usr/local/bin/myapp-healthcheck.sh

/etc/systemd/system/myapp-healthcheck.service

[Unit]
Description=Healthcheck for myapp

[Service]
Type=oneshot
ExecStart=/usr/local/bin/myapp-healthcheck.sh

/etc/systemd/system/myapp-healthcheck.timer

[Unit]
Description=Run myapp healthcheck every 15 seconds

[Timer]  
AccuracySec=1
Unit=myapp-healthcheck.service

[Install]
WantedBy=timers.target

Запуск:

sudo systemctl daemon-reload
sudo systemctl enable --now myapp-healthcheck.timer
sudo systemctl list-timers --all | grep myapp-healthcheck

Чтобы не получить бесконечные рестарты при длительной аварии, добавьте предохранитель: например, проверку «сколько рестартов было за последние 5 минут» через systemctl show, или задержку перед рестартом, или отдельный lock-файл. Если вы используете PostgreSQL/Redis и бывают плановые окна, это особенно актуально.

Просмотр логов journald и параметров watchdog для systemd-сервиса Gunicorn

systemd watchdog: защита от зависаний (когда имеет смысл)

Watchdog полезен, когда процесс не падает, но «замирает» (deadlock, зависший event loop, вечный запрос). Механика такая: в unit задаётся WatchdogSec, а процесс обязан регулярно уведомлять systemd «я жив». Если уведомлений нет — systemd перезапускает сервис.

Для Gunicorn это реально использовать только когда вы уверены, что ваша версия или запуск умеют корректно работать с systemd notify. Если у вас нет стабильных уведомлений, watchdog будет либо бесполезен, либо вреден.

FastFox SSL
Надежные SSL-сертификаты
Мы предлагаем широкий спектр SSL-сертификатов от GlobalSign по самым низким ценам. Поможем с покупкой и установкой SSL бесплатно!

Как проверить, что уведомления и watchdog работают

Сначала включите notify-режим и watchdog в unit (только если понимаете последствия):

sudo systemctl edit gunicorn-myapp.service

И добавьте override (пример, при условии что notify реально поддерживается в вашем окружении):

[Service]
Type=notify
NotifyAccess=main
WatchdogSec=30

Перезагрузите unit и посмотрите свойства:

sudo systemctl daemon-reload
sudo systemctl restart gunicorn-myapp.service
systemctl show gunicorn-myapp.service -p Type -p WatchdogUSec -p NotifyAccess -p ActiveState -p SubState

Если сервис зависает в activating или ведёт себя нестабильно — откатите override и оставьте вариант с Type=simple + healthcheck-тимером.

Пара слов про Nginx и Unix-сокет

Полный конфиг Nginx здесь не привожу, но напомню две вещи, из-за которых чаще всего ловят 502/504:

  • Nginx должен иметь права на сокет (пользователь/группа и SocketMode в socket unit);
  • таймауты прокси должны быть согласованы с таймаутами Gunicorn, иначе Nginx «сдастся» раньше воркера.

Что обычно согласуют:

  • Gunicorn: --timeout, --graceful-timeout, --keep-alive;
  • Nginx: proxy timeouts (и при необходимости буферы под ответы).

Если параллельно занимаетесь доменом и переводите проект на HTTPS, удобно делать это без потери индексации и с корректными редиректами. В тему: миграция домена: 301, HSTS и TLS. Для продакшена обычно заранее планируют SSL-сертификаты, чтобы не держать проект на «временных» схемах.

Диагностика: быстрый чек-лист, когда «после рестарта не поднялось»

  • Статус: systemctl status gunicorn-myapp.service и systemctl status gunicorn-myapp.socket.
  • Логи: journalctl -u gunicorn-myapp.service -n 200 --no-pager.
  • Слушается ли сокет: ss -xlpn | grep myapp.sock.
  • Права на сокет: ls -l /run/gunicorn/myapp.sock.
  • Работает ли venv от нужного пользователя: sudo -u www-data /srv/myapp/venv/bin/python -c "import sys; print(sys.executable)".
  • Импорт приложения: sudo -u www-data /srv/myapp/venv/bin/python -c "import myapp".

Что улучшить дальше (по мере роста проекта)

Когда базовая схема устоялась, обычно добавляют:

  • раздельные liveness/readiness и маршрутизацию health так, чтобы он не был доступен извне;
  • ограничение ресурсов через MemoryMax, CPUQuota и вынос в отдельный slice;
  • алерты на рестарты и всплески ошибок (хотя бы по journald);
  • атомарный деплой релизами, чтобы graceful reload не ломался из-за удаления файлов.

Но даже «минималка» socket unit + service unit + reload + healthcheck уже делает продакшен заметно стабильнее, чем запуск Gunicorn в screen/tmux.

Итог

Связка Gunicorn и systemd на VDS — это про управляемость: socket activation даёт аккуратную точку входа, unit-файл фиксирует правила запуска, а reload через HUP позволяет обновляться без полной остановки. Добавив healthcheck на systemd timer (и watchdog там, где он действительно поддерживается), вы закрываете самый неприятный класс проблем: зависания и «тихие» деградации.

Поделиться статьей

Вам будет интересно

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: mount: wrong fs type, bad option, bad superblock — как быстро найти и исправить причину

Ошибка mount: wrong fs type, bad option, bad superblock в Debian/Ubuntu может означать и простую опечатку в имени раздела, и пробл ...
Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: XFS metadata corruption и emergency read-only — пошаговое восстановление

Если XFS-раздел внезапно стал доступен только для чтения, а сервер ушёл в emergency mode, главное — не спешить. Разберём безопасны ...
Debian/Ubuntu: как исправить Failed to fetch при apt update OpenAI Статья написана AI (GPT 5)

Debian/Ubuntu: как исправить Failed to fetch при apt update

Ошибка Failed to fetch при apt update в Debian и Ubuntu обычно связана не с самим APT, а с DNS, сетью, зеркалом, прокси, временем ...