Бот, сборщик данных или торговый робот обычно начинают жизнь одинаково: скрипт запущен руками в сессии tmux или через nohup. Работает это до первого события из короткого списка: сервер перезагрузился после обновления ядра, скрипт упал ночью из-за сетевой ошибки, кончилось место под файл nohup.out. Во всех трёх случаях вы узнаёте о проблеме от пользователей или по дыре в данных.
systemd решает все три задачи одним текстовым файлом на двадцать строк. Он запускает программу при старте сервера, поднимает её после сбоя и собирает вывод в журнал с отметками времени. Ниже полный пример для Ubuntu Server 26.04 LTS (systemd 259), от пользователя до разбора ошибок. Те же директивы работают в Debian, РЕД ОС и других системах с systemd.
Этим механизмом пользуются и готовые программы. Установщик Ollama, например, создаёт системного пользователя ollama и кладёт файл /etc/systemd/system/ollama.service. Docker после установки тоже работает как служба docker.service. Свой скрипт заслуживает того же обращения.
Подготовка: пользователь, каталоги, файл с переменными
Возьмём условный сборщик котировок: программа на Python в каталоге /opt/collector, виртуальное окружение в /opt/collector/venv, запуск файлом main.py. Токены и адреса она читает из переменных окружения, а накопленные данные пишет в свой рабочий каталог.
Службе нужен собственный пользователь без права входа в систему. Если в программе найдётся уязвимость, взломщик получит права этого пользователя, а не root и не ваши.
sudo useradd --system --home-dir /opt/collector --shell /usr/sbin/nologin collector
sudo chown -R collector:collector /opt/collector
Секреты выносим в отдельный файл. systemd читает его сам, с правами root, до того как переключится на пользователя службы, поэтому файл можно закрыть от всех остальных:
sudo mkdir -p /etc/collector
sudo nano /etc/collector/collector.env
sudo chmod 600 /etc/collector/collector.env
Формат файла простой: по одной паре ИМЯ=значение в строке, без слова export и без пробелов вокруг знака равенства.
API_TOKEN=YOUR_TOKEN
DB_DSN=postgresql://collector@localhost/quotes
Если базовые вещи вроде пользователя с sudo и файрвола на сервере ещё не сделаны, начните с базовой настройки сервера: служба с сетевым доступом на ненастроенной машине добавляет рисков.
Unit-файл: что означает каждая строка
Описания своих служб кладут в /etc/systemd/system/. Каталог /usr/lib/systemd/system/ принадлежит пакетам, и править файлы там не нужно: обновление пакета вернёт всё назад.
sudo nano /etc/systemd/system/collector.service
[Unit]
Description=Quotes collector
Wants=network-online.target
After=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=simple
User=collector
Group=collector
WorkingDirectory=/opt/collector
EnvironmentFile=/etc/collector/collector.env
Environment=PYTHONUNBUFFERED=1
ExecStart=/opt/collector/venv/bin/python /opt/collector/main.py
Restart=on-failure
RestartSec=10
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
StateDirectory=collector
[Install]
WantedBy=multi-user.target
Секция [Unit] описывает место службы среди остальных. Пара Wants и After с network-online.target означает «запускать после того, как сеть поднята». Без неё сборщик на старте сервера может обратиться к API раньше, чем появится маршрут, и упасть. Две строки с StartLimit задают предел: не больше пяти запусков за 300 секунд.
В секции [Service] главное - ExecStart. Путь должен быть абсолютным, systemd не ищет программы по PATH так, как это делает оболочка. Указан интерпретатор из виртуального окружения, поэтому активировать окружение не требуется. Строка Environment=PYTHONUNBUFFERED=1 отключает буферизацию вывода Python. Без неё сообщения print появляются в журнале пачками с задержкой в минуты, а при аварийном завершении последние строки теряются вовсе.
Короткий пример: изменить одну настройку чужой службы, не трогая её файл, можно командой sudo systemctl edit имя.service. Она создаёт файл-дополнение в /etc/systemd/system/имя.service.d/, который переживает обновление пакета. Так, например, для Ollama задают переменную OLLAMA_HOST. Источник: документация systemd (systemd.unit), руководство по установке Ollama.
Type=simple подходит для программы, которая работает на переднем плане и не уходит в фон. Таких большинство. Если ваша программа сама «демонизируется» через fork, либо отключите это поведение её ключом, либо используйте Type=forking с PIDFile.
StateDirectory=collector создаёт каталог /var/lib/collector, принадлежащий пользователю службы. Он нужен из-за следующих строк. ProtectSystem=strict делает всю файловую систему для этой службы доступной только на чтение, ProtectHome=true скрывает домашние каталоги, PrivateTmp=true даёт собственный /tmp. Писать программа сможет только в свой StateDirectory. Если данные лежат в другом месте, добавьте строку ReadWritePaths=/путь.
Секция [Install] отвечает за автозапуск: WantedBy=multi-user.target означает «включать при обычной загрузке сервера».
Запуск, проверка и чтение журнала
До запуска файл можно проверить на опечатки. Команда ничего не выводит, если ошибок нет:
sudo systemd-analyze verify /etc/systemd/system/collector.service
Проверка ловит неверные значения директив. Если написать, например, Restart=sometimes, она ответит строкой Failed to parse service restart specifier, ignoring: sometimes. Обратите внимание на слово ignoring: systemd не откажется запускать службу с такой ошибкой, он молча пропустит строку, и перезапуска после сбоя просто не будет.
Теперь запуск:
sudo systemctl daemon-reload
sudo systemctl enable --now collector.service
systemctl status collector.service
daemon-reload заставляет systemd перечитать файлы описаний. Его нужно повторять после каждой правки unit-файла, иначе в памяти останется старая версия. enable --now включает автозапуск и сразу стартует службу. В выводе status ищите строку Active: active (running) и ниже неё последние строки вывода программы.
Журнал службы читается через journalctl:
journalctl -u collector.service -f
journalctl -u collector.service --since "1 hour ago" -p err
Первая команда показывает вывод в реальном времени, вторая - только ошибки за последний час. Остальные полезные сочетания ключей собраны в шпаргалке по journalctl и systemctl.
Перезапуск стоит проверить руками, не дожидаясь настоящего сбоя. Узнайте номер процесса из status и завершите его сигналом, имитирующим аварию:
sudo kill -9 $(systemctl show -p MainPID --value collector.service)
sleep 12
systemctl status collector.service
Через десять секунд, заданных в RestartSec, служба должна снова быть в состоянии active (running) с новым номером процесса.
Перезапуск после сбоя: как он работает и где подводит
Restart=on-failure поднимает службу, если процесс завершился с ненулевым кодом, был убит сигналом или превысил тайм-аут. Штатный выход с кодом 0 перезапуска не вызывает. Для программы, которая должна работать всегда, а код возврата у неё ненадёжен, ставят Restart=always.
Таблица 1. Варианты директивы Restart
| Значение | Перезапуск при коде 0 | При ненулевом коде | При завершении сигналом |
|---|---|---|---|
no | нет | нет | нет |
on-failure | нет | да | да |
always | да | да | да |
Автоперезапуск лечит случайные сбои и маскирует постоянные. Если программа падает сразу после старта из-за ошибки в коде или неверного токена, она будет падать снова и снова. Здесь срабатывает предел из секции [Unit]: после пятого падения за пять минут systemd прекращает попытки, и служба переходит в состояние failed с причиной start-limit-hit. Это защита от бесконечного цикла, который забивает журнал и грузит процессор. После исправления причины счётчик сбрасывают вручную:
sudo systemctl reset-failed collector.service
sudo systemctl start collector.service
Отсюда следует вывод для эксплуатации: автоперезапуск не избавляет от наблюдения. Служба, которая за ночь перезапустилась сорок раз, формально «работает», но данные собирает с дырами. Число перезапусков показывает команда systemctl show -p NRestarts collector.service, и на это значение стоит повесить оповещение.
Вторая особенность касается самой программы. При остановке systemd посылает процессу сигнал SIGTERM и ждёт 90 секунд, после чего убивает его сигналом SIGKILL. Если ваш сборщик держит открытую транзакцию или недописанный файл, обработайте SIGTERM в коде и завершайте работу аккуратно. Иначе каждый systemctl restart будет оставлять за собой испорченные данные.
Ошибки, которые встречаются у всех
Когда служба не стартует, причина почти всегда видна в строке status с кодом вида status=203/EXEC. Коды стоит знать в лицо.
Таблица 2. Частые коды ошибок запуска службы
| Код | Что случилось | Что проверить |
|---|---|---|
203/EXEC | systemd не смог запустить программу | путь в ExecStart, бит исполнения, первую строку скрипта |
217/USER | пользователь не найден | имя в User=, создан ли пользователь |
200/CHDIR | не удалось перейти в каталог | путь в WorkingDirectory, права на него |
226/NAMESPACE | не удалось собрать изолированное окружение | пути в ReadWritePaths, существуют ли они |
Ещё две ситуации кодом не обозначены. Программа стартует и тут же падает с Permission denied или Read-only file system при записи: значит, она пишет вне StateDirectory, и сработал ProtectSystem=strict. Либо перенесите данные в /var/lib/collector, либо добавьте путь в ReadWritePaths. И вторая: в терминале скрипт работает, а службой нет. Почти всегда дело в окружении. У службы нет ваших переменных из ~/.bashrc, нет вашего PATH и другой текущий каталог. Всё, от чего зависит программа, должно быть прописано в unit-файле или в файле переменных.
Заметка из эксплуатации: журнал journald по умолчанию ограничен долей диска, а не сроком хранения. Сколько он занимает сейчас, показывает journalctl --disk-usage. Жёсткий предел задаётся параметром SystemMaxUse в /etc/systemd/journald.conf. Источник: документация systemd (journald.conf).
Насколько служба изолирована, оценивает встроенная проверка systemd-analyze security collector.service. Она выводит список из нескольких десятков параметров с пометками и общий балл. Гнаться за идеальным баллом для своего скрипта не нужно. Четыре директивы из примера закрывают главное: служба не получит новых привилегий, не изменит систему и не прочитает чужие домашние каталоги.
Есть случаи, когда служба systemd - не лучший выбор. Задаче, которая запускается раз в сутки и завершается, нужен таймер, а не постоянно работающий процесс. Программе в контейнере перезапуск обеспечивает Docker своей политикой restart, и оборачивать docker run в unit-файл обычно лишнее. А если на сервере десяток взаимосвязанных служб, порядок их запуска лучше описывать зависимостями Requires и After между ними, чем надеяться на паузы в RestartSec.
Проверка команд:
- Запущено в тестовой среде (systemd 255): unit-файл из статьи проверен командой
systemd-analyze verifyбез замечаний; строка об ошибкеFailed to parse service restart specifier, ignoring: sometimes- настоящий вывод этой команды. - Сверено с документацией (запуск невозможен без systemd как PID 1):
systemctl enable --now,status,killи перезапуск,reset-failed,show -p NRestarts,systemd-analyze security.