Гайды

Автозапуск сервиса на Linux через systemd

Unit-файл, который просто запускает процесс, пишется за минуту. Разбираем то, что дописывают потом и в спешке: перезапуск при падении без бесконечного цикла, потолок памяти и корректная остановка фоновых задач.

Гайд «как сделать unit systemd» существует в тысяче экземпляров, и все они заканчиваются на systemctl enable --now myapp. После этого сервис запускается при загрузке — и на этом его живучесть заканчивается.

Живучесть — это другое. Это ответ на четыре вопроса: что происходит, когда процесс упал; что происходит, когда он не упал, но съел всю память; что происходит с его фоновыми задачами при остановке; и как вы вообще узнаете, что что-то из этого случилось. Ниже — unit, который на эти вопросы отвечает, и разбор каждой строки, которая в нём не декоративна.

#Что понадобится

  • Дистрибутив с systemd (все актуальные общего назначения; версия systemd влияет на доступность отдельных директив — проверяйте systemctl --version и справку своей системы).
  • Отдельный системный пользователь под сервис. Запуск от root — самая частая и самая дорогая экономия времени.
  • Приложение, которое запускается одной командой, не требует интерактива и пишет логи в стандартный вывод, а не в свой файл.
  • Абсолютные пути ко всему: интерпретатору, рабочему каталогу, каталогу данных.

Последний пункт стоит отдельного предупреждения. Приложение, читающее конфигурацию «из текущего каталога», под systemd получит другой текущий каталог и другой домашний каталог. Классический исход — сервис поднялся, ответил 200 и работает с пустой базой, которую только что создал рядом. Данные при этом целы, просто приложение смотрит не туда.

#Шаг 1. Минимальный unit, от которого отталкиваемся

Файл /etc/systemd/system/persona.service:

[Unit]
Description=Personal AI service
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=persona
Group=persona
WorkingDirectory=/opt/persona
Environment=PERSONA_DATA_DIR=/var/lib/persona
EnvironmentFile=-/etc/persona/env
ExecStart=/opt/persona/.venv/bin/python -m uvicorn app.web.main:app --host 127.0.0.1 --port 8000

[Install]
WantedBy=multi-user.target

Две детали, которые не декоративны уже здесь. After=network-online.target вместо network.target: первый означает «сеть реально поднялась и адрес есть», второй — «подсистема сети инициализирована», чего для исходящих соединений мало. И EnvironmentFile=-/etc/persona/env с дефисом: дефис означает «отсутствие файла не ошибка», без него сервис не стартует, пока файла нет.

Секреты кладутся в этот EnvironmentFile с правами 0600, а не в сам unit: unit читается любым пользователем системы.

#Шаг 2. Перезапуск при падении — и предохранитель от цикла

Restart=always
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5

Restart=always поднимает процесс после любого выхода, включая нулевой код. Restart=on-failure мягче, но у веб-сервера нет сценария «корректно завершился сам», так что always честнее.

Пара StartLimitIntervalSec / StartLimitBurst — это тот предохранитель, который обычно забывают. Без него сервис, падающий на старте из-за опечатки в конфиге, будет перезапускаться вечно, забивая журнал и создавая иллюзию активности. С ним после пяти падений за пять минут systemd переводит юнит в состояние failed и останавливается — и вы увидите честное «сервис лежит» вместо «сервис постоянно перезапускается, наверное само пройдёт».

Обратная сторона: из состояния failed сервис сам не вернётся. Поэтому systemctl is-failed обязан попасть в мониторинг — иначе вы обменяли шумный отказ на тихий. Какие ещё проверки нужны сервису без дежурной смены, разобрано в статье про мониторинг личного сервиса.

#Шаг 3. Потолок памяти вместо ночного OOM

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

MemoryMax=3G
MemoryHigh=2500M
TasksMax=512

MemoryHigh — мягкий порог: при его превышении ядро начинает агрессивно давить процесс, но не убивает. MemoryMax — жёсткий: превышение означает смерть вашего процесса, а не соседнего. Это и есть смысл настройки: вы заранее выбираете жертву. TasksMax ограничивает число потоков и процессов — страховка от библиотеки, решившей поднять пул по числу ядер на каждый запрос.

Конкретные значения выставляются от замера, а не из статьи: посмотрите systemctl status под нагрузкой и возьмите запас порядка трети сверху.

#Шаг 4. Корректная остановка фоновых задач

Это место, где обычный unit тихо портит данные.

По умолчанию systemctl stop шлёт SIGTERM главному процессу, ждёт TimeoutStopSec (в типовой конфигурации — полторы минуты) и добивает SIGKILL. Если у вашего приложения есть фоновые воркеры — распознавание текста, эмбеддинги, ночная обработка, — то при KillMode=control-group (значение по умолчанию) сигнал уйдёт всей группе, и это как раз хорошо. Плохо другое: воркер в середине транзакции получает SIGKILL через полторы минуты, а пакетная обработка может идти дольше.

KillMode=mixed
KillSignal=SIGTERM
TimeoutStopSec=90

KillMode=mixed шлёт SIGTERM только главному процессу, а SIGKILL — всей группе. Это даёт приложению шанс самому корректно погасить воркеров и закрыть транзакции. Условие работоспособности: приложение действительно обрабатывает SIGTERM и не игнорирует его.

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

#Шаг 5. Ужать права

NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/persona

ProtectSystem=strict делает всю файловую систему доступной только на чтение, кроме явно перечисленного в ReadWritePaths. Это не «безопасность ради безопасности»: чаще всего эта директива ловит не злоумышленника, а собственную ошибку — приложение, которое пытается писать временные файлы туда, где им не место, падает на стенде, а не в проде через полгода.

#Проверка результата

Проверять надо не «сервис запустился», а четыре свойства по отдельности.

sudo systemctl daemon-reload
sudo systemctl enable --now persona.service

# 1. Работает и слушает свой порт
systemctl is-active persona.service
ss -ltnp | grep 8000

# 2. Переживает падение: убить главный процесс и убедиться, что вернулся
sudo systemctl kill -s SIGKILL persona.service
sleep 8 && systemctl is-active persona.service     # ждём: active

# 3. Ограничения реально применились
systemctl show persona.service -p MemoryMax -p TasksMax -p Restart

# 4. Возвращается после перезагрузки
sudo reboot        # после входа: systemctl is-active persona.service

Критерий приёмки для четвёртого пункта — не «сервис запущен», а «сервис отвечает по HTTP и отдаёт нужную версию сборки». Открытый порт не доказывает, что за ним свежий код: осиротевший процесс с прошлой версии держит сокет и выглядит абсолютно живым. Ручка живости должна отдавать версию, и деплой должен её сверять — на Windows этот же провал выглядит ещё злее и разобран в статье про автозапуск на Windows.

#Что может пойти не так

Сервис в состоянии failed и не поднимается. Смотрите systemctl status и journalctl -u persona -n 100 --no-pager. Если причина — start-limit-hit, сервис исчерпал лимит перезапусков: сначала чините причину падений, потом systemctl reset-failed persona.

Приложение стартовало с пустой базой. Проверьте WorkingDirectory и переменную с путём к данным в systemctl show persona -p Environment. Почти всегда виноват относительный путь.

Логи не пишутся никуда. Приложение пишет в свой файл при работающем journald, либо наоборот. Пусть пишет в стандартный вывод, а journalctl разбирается дальше.

Пользовательский unit умирает вместе с сеансом. Если вы положили сервис в ~/.config/systemd/user/, он останавливается при выходе пользователя, пока для учётной записи не включён «задержанный» режим (loginctl enable-linger). Для сервиса, который должен работать всегда, надёжнее системный unit.

После обновления кода сервис отдаёт старое. systemctl restart перезапускает процесс, но не пересобирает окружение. Если у вас сборка фронтенда или кэш зависимостей — они не в зоне ответственности systemd. То же касается и контейнерного варианта — там своя механика, разобранная в гайде про запуск Persona в контейнере.

#Частые вопросы

#Чем Restart=always отличается от Restart=on-failure?

on-failure перезапускает только при ненулевом коде выхода или сигнале, always — при любом завершении. Для веб-сервера, у которого нет легитимного сценария самостоятельного завершения, always точнее описывает намерение. Для одноразовой задачи — наоборот, вредно.

#Нужен ли отдельный пользователь под сервис?

Да, и это дешевле любых других мер. Отдельный пользователь плюс ProtectSystem=strict ограничивают ущерб от произвольной ошибки в коде или зависимости. Запуск от root делает каждую такую ошибку потенциально фатальной для всей машины.

#Как задать секреты, не показывая их всей системе?

Через EnvironmentFile с правами 0600, принадлежащий пользователю сервиса. Не через Environment= в unit-файле: его содержимое видно любому в выводе systemctl show.

#Почему сервис стартует раньше сети и падает?

Потому что network.target означает не то, что от него ждут. Нужен After=network-online.target вместе с Wants=network-online.target — и целевой юнит ожидания должен быть включён в системе, иначе зависимость ни на что не влияет.

#Можно ли ограничить процессор так же, как память?

Да, директивами вида CPUQuota. Но для личного сервиса это обычно не нужно: процессор он делит с самим собой, и ограничение только замедлит ответы. Память ограничивать стоит всегда, процессор — только когда на машине есть второй важный жилец.

#Достаточно ли systemd, чтобы сервис считался поднятым?

Нет. systemd отвечает за жизнь процесса, а не за корректность ответа и не за свежесть кода. Полный маршрут — от автозапуска через домен, прокси и мониторинг — описан в статье о том, как поднять свой ИИ-сервис.

Хотите ИИ, который помнит вас?

Persona держит контекст между чатами и работает на вашем железе — если развернуть её у себя и подключить локальную модель.

Создать аккаунт