Почти у каждого администратора Linux есть самописный скрипт, который дописывался урывками и работает годами. Его обычно запускают через nohup, добавляют в cron или вписывают в rc.local, а потом с удивлением обнаруживают, что процесс тихо упал неделю тому и никто его не поднял. Менеджер systemd, стоящий на большинстве дистрибутивов больше десяти лет, превращает любой исполняемый файл в полноценную фоновую службу с автозапуском при загрузке, контролем состояния и автоматическим перезапуском при падении. Разберём весь путь на конкретном примере: возьмём небольшой Python-скрипт, напишем для него юнит-файл, настроим рестарт и посмотрим, как всё это проверять и укреплять.

Юнит-файл вместо nohup и cron

Знакомая картина: скрипт запущен в сессии screen или tmux, сессию закрыли, процесс ушёл вместе с ней. Вариант с cron и директивой @reboot надёжнее, но тоже одноразовый: если процесс упадёт в десять утра, следующий запуск случится только после ребута. rc.local в современных дистрибутивах и вовсе часто отключён, потому что systemd считает его устаревшим механизмом.

Юнит-файл закрывает весь список претензий разом. Менеджер знает, кто главный процесс, с каким кодом он завершился и что делать дальше. Он запускает скрипт строго после поднятия сети, если в юните указана зависимость от network-online.target. Он складывает stdout и stderr скрипта в общий журнал journald, не требуя от кода ни одной строки логирования. Он умеет ограничивать ресурсы, запускать службу от непривилегированного пользователя и перезапускать её. По сути, за двадцать минут разовой настройки вы получаете надзор, который раньше требовал отдельного скрипта-надзирателя.

Тестовый Python-скрипт, который следит за местом на диске

Для примера возьмём типичную утилиту администратора: раз в минуту смотрим, сколько процентов занято на корневом разделе, и ругаемся в stderr, если перешли порог.

#!/usr/bin/env python3
import shutil
import sys
import time

INTERVAL = 60      # период проверки, секунды
THRESHOLD = 85     # порог занятости, проценты

def check():
    u = shutil.disk_usage("/")
    percent = u.used * 100 // u.total
    if percent >= THRESHOLD:
        print(f"ВНИМАНИЕ: диск занят на {percent} процентов", file=sys.stderr, flush=True)
    else:
        print(f"Диск занят на {percent} процентов", flush=True)

def main():
    while True:
        try:
            check()
        except OSError as e:
            print(f"Не удалось опросить диск: {e}", file=sys.stderr, flush=True)
            sys.exit(1)
        time.sleep(INTERVAL)

if __name__ == "__main__":
    main()

Две детали здесь не случайны. Параметр flush=True обязателен: когда stdout Python-процесса уходит не в терминал, а в канал, интерпретатор буферизует вывод, и без принудительного сброса записи в журнале появлялись бы с опозданием на минуты. А sys.exit(1) при ошибке сделан специально, чтобы показать аварийное поведение: менеджер увидит код возврата 1 и перезапустит процесс. Скрипт положим в /opt/diskwatch и сразу создадим для него системного пользователя:

sudo mkdir -p /opt/diskwatch
sudo cp diskwatch.py /opt/diskwatch/diskwatch.py
sudo useradd -r -s /usr/sbin/nologin diskwatch
sudo chown -R diskwatch:diskwatch /opt/diskwatch
sudo chmod 755 /opt/diskwatch/diskwatch.py

Ключ useradd -r создаёт системного пользователя без пароля и без домашней папки, а shell nologin запрещает интерактивный вход. Права на файлы проекта должны позволять чтение этому пользователю, от которого теперь запускается процесс. Проверка перед оформлением выглядит так: sudo -u diskwatch /usr/bin/python3 /opt/diskwatch/diskwatch.py. Если строки появляются раз в минуту, прерывайте по Ctrl+C и переходите к юниту.

Анатомия юнит-файла от секции Unit до секции Install

Юнит это обычный ini-подобный текстовый файл с тремя секциями. Создаём /etc/systemd/system/diskwatch.service:

[Unit]
Description=Контроль свободного места на корневом разделе
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=60
StartLimitBurst=5

[Service]
Type=exec
User=diskwatch
Group=diskwatch
WorkingDirectory=/opt/diskwatch
ExecStart=/usr/bin/python3 /opt/diskwatch/diskwatch.py
Restart=on-failure
RestartSec=5s

[Install]
WantedBy=multi-user.target

Секция [Unit] описывает место службы в системе. Связка Wants и After подтягивает network-online.target, чтобы скрипт стартовал после того, как сеть действительно заработала, а не только после чтения её настроек: network.target означает лишь порядок старта, но не готовность интерфейсов. Лимитные настройки StartLimitIntervalSec и StartLimitBurst относятся именно к [Unit].

В секции [Service] живёт вся механика. Путь в ExecStart обязан быть абсолютным, относительные имена менеджер не примет. User и Group опускают процесс с прав root на аккаунт diskwatch, а WorkingDirectory задаёт текущую папку. Параметр Type= описывает, в какой момент служба считается запущенной. По умолчанию действует simple: менеджер считает процесс запущенным сразу после fork, поэтому опечатка в пути или несуществующий пользователь всплывут уже после отчёта systemctl start об успехе. Значение exec отличается тем, что переход в active происходит после фактического запуска бинарника, и ошибки старта менеджер ловит сразу; свежая документация прямо рекомендует exec для большинства случаев. Вариант oneshot предназначен для задач, которые отрабатывают и завершаются: им ставят RemainAfterExit=yes, а Restart=always и on-success для oneshot запрещены. Type=forking остался для старых служб, которые уходят в фон через fork и пишут PID-файл.

Секция [Install] умещается в одну строчку: WantedBy=multi-user.target говорит, куда вписаться при включении. multi-user.target это штатное серверное окружение без графики, нормальное место для системной службы.

Установка сервиса и автозапуск при загрузке системы

Ввод в эксплуатацию сводится к короткой последовательности:

  1. сохраняем файл в /etc/systemd/system/diskwatch.service и ставим права 644;
  2. перечитываем конфигурацию командой systemctl daemon-reload, иначе новый юнит менеджер не увидит;
  3. включаем автозапуск и стартуем одной командой systemctl enable --now diskwatch.service;
  4. проверяем состояние командой systemctl status diskwatch.service;
  5. заглядываем в журнал journalctl -u diskwatch.service -n 20.

systemctl enable не копирует файл, а создаёт символическую ссылку в каталоге /etc/systemd/system/multi-user.target.wants, и при загрузке менеджер находит там наш юнит. Команда daemon-reload нужна потому, что менеджер читает юниты при старте и по явной просьбе, за файловой системой он не следит, поэтому после каждого изменения юнита конфигурацию перечитывают заново.

Каталоги для юнитов выстроены по приоритету: файлы пакетов лежат в /usr/lib/systemd/system, временные в /run/systemd/system, а всё, что положил администратор в /etc/systemd/system, перекрывает оба списка. Правило простое: пакетные файлы не трогаем, свои службы держим в /etc. Статус после запуска:

sudo systemctl status diskwatch.service
● diskwatch.service - Контроль свободного места на корневом разделе
     Loaded: loaded (/etc/systemd/system/diskwatch.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-11-03 09:14:52 +07; 3min 11s ago
   Main PID: 4123 (python3)

Слово enabled в строке Loaded подтверждает, что автозапуск записан. Строка Main PID показывает главного процесса: именно его код возврата решает судьбу всей службы.

Перезапуск при падении и лимит повторных попыток

За спасение упавшей службы отвечает параметр Restart= в секции [Service]. Значений у него семь. no, значение по умолчанию, ничего не перезапускает. on-success поднимает процесс только после чистого завершения. on-failure реагирует на нечистый выход: ненулевой код возврата, аварийный сигнал, сработавший таймаут. on-watchdog срабатывает, когда процесс с включённым WatchdogSec перестал слать менеджеру уведомления о жизни. always перезапускает при любом исходе, даже чистом. Документация прямо рекомендует on-failure для долгоиграющих служб: если администратор сам остановил процесс через systemctl stop, перезапуска не будет, а аварийные ситуации покрытие получают полностью.

Рядом стоит RestartSec, пауза перед повторным стартом, по умолчанию равная 100 миллисекундам: служба перезапускается практически мгновенно, и если скрипт падает из-за временно занятого ресурса, мгновенные попытки лишь съедают лимит. Пять секунд, как в нашем юните, дают окружению время восстановиться.

Лимит попыток регулируется парой StartLimitIntervalSec и StartLimitBurst из секции [Unit]. По умолчанию это 10 секунд и 5 попыток: если за интервал служба не выжила больше раз, чем разрешено, менеджер переводит юнит в состояние failed и оставляет его в покое, а в строй его возвращает systemctl reset-failed и последующий start. В примере окно растянуто до 60 секунд при тех же 5 попытках: процесс поднимается каждые 5 секунд, а если за минуту пять попыток провалились, пора читать журнал.

Проверить связку можно руками:

sudo systemctl kill -s SIGKILL diskwatch.service
sleep 6
journalctl -u diskwatch.service -n 4 --no-pager

В журнале появятся строки менеджера вроде "Scheduled restart job, restart counter is at 1." и новая запись скрипта. Скриптам с собственными кодами ошибок пригодятся параметры SuccessExitStatus и RestartPreventExitStatus: они описывают, какие коды возврата считать нормальными или не подлежащими перезапуску.

Логи в journalctl и живая диагностика состояния

Главный инструмент диагностики это journalctl с фильтром по службе. Команды, которые стоит записать в шпаргалку:

journalctl -u diskwatch.service -f
journalctl -u diskwatch.service --since "10 min ago"
journalctl -u diskwatch.service -p err
systemctl show -p MainPID --value diskwatch.service

Ключ -f следит за потоком в реальном времени, --since задаёт окно, -p err отбирает только ошибки, а последняя команда печатает PID главного процесса. Весь stdout и stderr скрипта попадает в журнал автоматически: стандартные потоки юнит по умолчанию направляет в journald, параметр StandardOutput= даже указывать не нужно. Поэтому в тестовом скрипте понадобился только flush, без библиотек логирования.

Команда systemctl cat diskwatch.service печатает оригинальный файл и все переопределения поверх. Сами переопределения делают не правкой оригинала, а через systemctl edit: команда открывает редактор и создаёт файл /etc/systemd/system/diskwatch.service.d/override.conf, куда вписывают только изменённые параметры:

[Service]
RestartSec=10s
Restart=always

После сохранения менеджер сам выполнит daemon-reload, останется лишь рестарт службы. Основной юнит можно менять как угодно, локальные настройки лежат отдельно и не потеряются.

Укрепление сервиса изоляцией и лимитами ресурсов

Работа от непривилегированного пользователя уже сильно безопаснее root, но systemd позволяет дожать изоляцию несколькими строчками в секции [Service]:

NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
ReadWritePaths=/opt/diskwatch
MemoryMax=200M
CPUQuota=20%

NoNewPrivileges запрещает процессу получать новые привилегии через setuid-бинарники. ProtectSystem=strict монтирует всю файловую систему только для чтения, а ReadWritePaths открывает исключения по списку. PrivateTmp выдаёт службе отдельный каталог /tmp вместо общего. MemoryMax ставит жёсткий предел памяти, сверх которого менеджер завершит самый прожорливый процесс группы, а CPUQuota ограничивает долю процессорного времени, в примере до 20 процентов ядра. Нашему скрипту таких лимитов хватает с запасом.

Перед вводом в строй юнит стоит прогнать через проверку синтаксиса: systemd-analyze verify /etc/systemd/system/diskwatch.service распечатает опечатки и несуществующие параметры, которые менеджер в лучшем случае молча проигнорирует. Оценку изоляции даёт systemd-analyze security diskwatch.service: она ставит службе балл от 0 до 10, где меньше значит безопаснее, и построчно перечисляет выключенные защиты. У базового юнита счёт будет высоким, а после блока изоляции ощутимо снизится.

Версии systemd от дистрибутива к дистрибутиву различаются: Debian 12 несёт 252, Ubuntu 24.04 версию 255, свежие Fedora дошли до 256. Базовые параметры из статьи работают везде, а экспоненциальные паузы RestartSteps появились только в 254.

Скрипт из примера превращается в полноценного жителя системы: стартует сам после ребута, переживает собственные падения, пишет в общий журнал и не лезет за отведённые границы. Дальше открываются таймеры, сокет-активация и шаблонные юниты, но фундамент именно такой: юнит-файл в /etc/systemd/system, Restart=on-failure, RestartSec с запасом и разумный лимит попыток. Ночные сообщения о молча упавших процессах после этого отправляются в прошлое.