Зачем вообще разбираться в unit-файлах
В продакшене «просто запустить бинарник» почти никогда не достаточно. Обычно нужно, чтобы сервис:
- стартовал в правильный момент (после сети, дисков, сокетов, базовых таргетов);
- давал предсказуемый статус готовности (readiness), а не «процесс запущен»;
- перезапускался при падениях, но не уходил в вечную restart-loop;
- умел сигнализировать о зависаниях (watchdog), а не только о падениях;
- оставлял диагностику в journald, чтобы разбор инцидентов был быстрым.
Всё это описывается в одном месте — в systemd unit-файле. Ниже — практический разбор ключевых директив: Type=, ExecStart, Restart=, WatchdogSec=, а также After/Wants и ограничения StartLimitIntervalSec.
База: из чего состоит systemd unit
Для сервисов используется файл вида /etc/systemd/system/myapp.service (или drop-in в /etc/systemd/system/myapp.service.d/*.conf). Структура стандартная:
[Unit]— зависимости, порядок запуска, лимиты на «шторм» перезапусков;[Service]— как запускать процесс, тип, перезапуск, watchdog;[Install]— подключение в автозагрузку через таргеты.
Убедиться, что systemd видит итоговую конфигурацию (с учётом drop-in), удобнее так:
systemctl cat myapp.service
systemctl show myapp.service
systemd-analyze verify /etc/systemd/system/myapp.service
Если вы переносите приложение на сервер и хотите минимум сюрпризов при рестартах и обновлениях, чаще всего удобнее работать на VDS: там полный контроль над systemd, journald и политиками запуска.
Type=: как systemd понимает «сервис запущен»
Параметр Type= — это договор между systemd и вашим сервисом о том, когда считать запуск успешным. Неверный выбор обычно проявляется так:
- systemd считает сервис поднятым слишком рано, и зависимости стартуют до готовности;
- юнит зависает в
activating, потому что systemd ждёт сигнал готовности, который никто не отправляет; - таймауты старта выглядят «рандомно», хотя причина — в модели readiness.
Type=simple (по умолчанию)
Type=simple — systemd запускает ExecStart и считает сервис стартовавшим сразу после запуска процесса, без ожидания готовности.
Подходит для:
- простых демонов без длительной инициализации;
- сервисов, которые не форкаются;
- кейсов, где readiness не критичен (или решён на уровне балансера или оркестратора).
Type=forking
Type=forking нужен для «классических» демонов, которые форкаются в фон и завершают родительский процесс. systemd ждёт завершения родителя и затем считает, что сервис стартовал.
Практический момент: почти всегда для forking нужен PIDFile=, иначе systemd может неверно определить главный PID и начнутся странности с рестартами и статусом.
Type=notify
Type=notify — лучший вариант, если приложение умеет отправлять systemd сигнал готовности через sd_notify (или совместимый механизм). Тогда systemd считает сервис поднятым только после сообщения READY=1.
Что это даёт на практике:
- зависимости стартуют после реальной готовности;
- watchdog встраивается естественно (тот же канал уведомлений);
- статусы и таймауты становятся предсказуемыми.
Если вы поставили
Type=notify, но приложение не отправляет notify, unit обычно будет висеть вactivatingдоTimeoutStartSec=. Перед включением notify проверьте документацию приложения или наличие флагов и библиотек для systemd integration.
Сервис «долго стартует»: что выбрать
Если сервис поднимается 30–90 секунд (миграции, прогрев кэшей, подключение к кластеру), Type=simple часто приводит к гонкам. Самый чистый путь — Type=notify. Если notify недоступен, лучше разделить жизненный цикл: отдельная подготовка (oneshot) и отдельный старт основного процесса.

ExecStart: как запускать процесс правильно
ExecStart — команда запуска. Звучит просто, но именно здесь чаще всего создают проблемы, которые потом проявляются в рестартах, PID’ах и «пропавших» логах.
Правило №1: один «главный» процесс
systemd должен понимать, какой процесс считать основным. Поэтому:
- избегайте запуска через shell-цепочки;
- не используйте
ExecStartкак мини-скрипт с пайпами; - подготовку выносите в
ExecStartPreили отдельный oneshot.
Плохо (shell-логика скрывает реальный процесс и усложняет остановку и рестарт):
ExecStart=/bin/sh -c 'myapp --flag | tee /var/log/myapp.log'
Лучше: писать в stdout/stderr и забирать логи через journald (или отдельный лог-агент). Если нужен файл, это обычно решается вне ExecStart.
Правило №2: не демонизируйте процесс внутри unit
В systemd-мире процесс обычно должен оставаться в форграунде. Флаги вида --daemon, --background, -d чаще всего не нужны (исключение — legacy-демоны с Type=forking).
ExecStart и переменные окружения
Если нужны параметры окружения (DSN, флаги, пути), не «вшивайте» их в команду. Используйте Environment= или EnvironmentFile= — так проще сопровождать и меньше риск случайно засветить значения в истории и диагностике.
Важно: переменные окружения часто можно увидеть локально при наличии прав на просмотр процессов. Для секретов лучше применять специализированные механизмы хранения секретов и ограничение доступа, но это отдельная тема.
Минимальный пример unit с ExecStart
[Unit]
Description=MyApp
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.yml
[Install]
WantedBy=multi-user.target
After/Wants: порядок и зависимости без самострела
Два часто путаемых параметра: After и Wants.
After= — только порядок
After= говорит systemd: «стартуй этот unit после указанного», но не делает его обязательным. Если указанный юнит не запускается, ваш сервис всё равно может стартовать (просто порядок применяется, когда оба участвуют).
Wants= — «хочу, но не критично»
Wants= добавляет мягкую зависимость: при старте вашего сервиса systemd попытается запустить и указанный unit, но его падение не остановит запуск вашего.
Когда нужен Requires=
Если без зависимости сервис бессмысленен (например, без смонтированного тома, без локального сокета, без обязательного сервиса-прокси), используйте Requires=. Но помните: жёсткие зависимости повышают риск каскадного отказа.
Практический шаблон для сети
Очень частый кейс: сервису нужна именно «поднятая сеть», а не просто network.target. Обычно используют:
[Unit]
After=network-online.target
Wants=network-online.target
Это не гарантия доступности внешних ресурсов (DNS, маршрут, конкретная БД), но заметно снижает гонки при буте.
Если у вас воркеры и очереди под управлением systemd, полезно свериться с типовыми паттернами из статьи про воркеры systemd вместо supervisor — там хорошо видно, как зависимости и рестарты влияют на стабильность.
Restart=: перезапуск без бесконечной петли
Restart= определяет, в каких случаях systemd перезапускает сервис. Это мощно, но легко сделать «вечный молоток», который только прячет проблему и превращает логи в шум.
Restart=on-failure
Restart=on-failure перезапустит сервис, если тот завершился с ошибкой: ненулевой код выхода, сигнал, timeout и т.п. Обычно это лучший стартовый выбор для веб-приложений и воркеров.
Рекомендуемые дополнения:
RestartSec=— пауза перед перезапуском, чтобы не устраивать шторм;TimeoutStartSec=— адекватный таймаут на старт;- ограничители StartLimit, чтобы не долбиться в стену бесконечно.
Restart=always: используйте осознанно
Restart=always перезапускает почти всегда, включая «нормальный» выход. Это бывает полезно для некоторых воркеров, но опасно, если приложение завершилось намеренно (например, при фатальной конфигурации): вы получите restart-loop и замусоренные логи.
Restart=no — это тоже нормально
Для oneshot-задач, миграций и импортов, которые должны выполниться один раз, Restart=no часто правильнее. Иногда лучше упасть и дать понятный алерт, чем «лечить» проблему бесконечными перезапусками.
StartLimitIntervalSec и StartLimitBurst: защита от restart-loop
Директивы StartLimitIntervalSec и StartLimitBurst (в современных systemd — в секции [Unit]) ограничивают количество стартов за интервал времени. Типовая логика: «не более N стартов за M секунд».
Если лимит превышен, unit уходит в failed и перестаёт перезапускаться до вмешательства администратора (или до сброса состояния).
[Unit]
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Restart=on-failure
RestartSec=5s
Как читать: за последние 300 секунд допускается до 5 попыток старта. Если приложение падает сразу после запуска и это повторяется — systemd остановится после 5 попыток.
Сброс «лимита стартов»
После исправления причины иногда нужно сбросить счётчики:
systemctl reset-failed myapp.service
systemctl start myapp.service
WatchdogSec=: обнаруживаем зависания, а не только падения
Restart=on-failure помогает, когда процесс упал. Но хуже, когда он не падает, а завис (deadlock, зависший I/O, заблокированная очередь). Для этого в systemd есть watchdog: WatchdogSec=.
Как работает watchdog в systemd
- вы задаёте
WatchdogSec=30s; - systemd ожидает, что сервис будет регулярно «пинговать» менеджер через
sd_notifyсWATCHDOG=1; - если пинга нет — systemd считает сервис неотзывчивым и применяет политику завершения и перезапуска.
На практике watchdog почти всегда завязан на Type=notify, потому что протокол один: systemd notification socket.
Минимальный пример с WatchdogSec
[Unit]
Description=MyApp with watchdog
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=300
StartLimitBurst=5
[Service]
Type=notify
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=5s
WatchdogSec=30s
TimeoutStartSec=60s
[Install]
WantedBy=multi-user.target
Важно: если приложение не отправляет watchdog-пинги, оно будет регулярно убиваться как «зависшее». Сначала подтвердите поддержку notify и watchdog самим приложением.
Как понять, что watchdog реально работает
Смотрите состояние и логи:
systemctl status myapp.service
journalctl -u myapp.service -b
Если сервис умеет читать параметры systemd (часто через окружение), полезно проверить, какое значение watchdog-интервала реально передаётся процессу: конкретика зависит от языка и фреймворка.

Связка Type=notify + readiness + зависимости: типовой продовый паттерн
В реальном мире чаще всего нужно: «сервис считается готовым, когда открыл порт, прогрел кэш и прошёл self-check». Тогда:
Type=notifyдаёт честный readiness;After=/Wants=задают порядок запуска по инфраструктуре;Restart=on-failureстрахует падения;WatchdogSec=ловит зависания;StartLimitIntervalSecзащищает от бесконечных циклов.
В стеке из нескольких сервисов это особенно заметно: «правильная» готовность одного компонента убирает большую часть флапающих стартов остальных.
Диагностика: что делать, если unit ведёт себя странно
1) Сервис вечно в activating
Частая причина — Type=notify без реального notify. Проверьте:
systemctl status myapp.service
journalctl -u myapp.service -b --no-pager
Для проверки гипотезы можно временно переключить на Type=simple (а затем вернуть корректный readiness).
2) Сервис перезапускается слишком часто
Смотрите причину завершения и коды:
systemctl show myapp.service -p ExecMainStatus -p ExecMainCode -p Result
journalctl -u myapp.service -b -n 200 --no-pager
Убедитесь, что RestartSec= не нулевой, а StartLimitIntervalSec/StartLimitBurst выставлены. Если хотите дополнительно поднять общий уровень «прочности» сервисов, пригодится материал про systemd sandboxing и hardening.
3) «Зависимости есть, но порядок не тот»
Напоминание: After= — это порядок, Wants= — мягкое притягивание. Если нужно «не стартовать без X», используйте Requires= (часто вместе с After=).
Чек-лист перед выкладкой unit в прод
- Выбран правильный
Type=: если есть notify — используйтеType=notify, иначеsimple(илиforkingдля legacy). ExecStartзапускает один главный процесс без shell-обвязок и самодемонизации.Restart=on-failureплюсRestartSec=настроены осмысленно.- Анти-шторм:
StartLimitIntervalSecиStartLimitBurstвыставлены. - Watchdog включён только если приложение реально его поддерживает, и интервал не слишком агрессивный.
- Зависимости выражены корректно:
After/Wantsдля порядка и мягких связей,Requires— только когда действительно нужно. - Проверка:
systemd-analyze verifyи тестовый старт и стоп с просмотром journald.
Итог
Хороший systemd unit не просто «запускает бинарник», а описывает ожидаемое поведение сервиса при старте, сбоях и зависаниях. Правильно подобранные Type=, аккуратный ExecStart, разумный Restart=on-failure, watchdog через WatchdogSec= и ограничения StartLimitIntervalSec превращают запуск в предсказуемый механизм, а не в набор случайностей.
Дальше по сложности обычно идут ExecReload, RuntimeDirectory, status-строки через systemd-notify --status, шаблоны unit’ов и drop-in overrides под разные окружения.


