Как структурировать systemd unit-файл
1.Начните с понятного блока [Unit]
[Unit] Description=My Custom Web App After=network.target Wants=network-online.target
Description- это то, что вы увидите приsystemctl status, так что пусть будет понятно, что за сервис.Afterзадаёт порядок запуска: этот сервис стартует после того, как поднимется сеть.Wants- мягкая зависимость. Полезна, например, если у вас есть связанные сервисы вроде логгирования или баз данных.
Совет: избегайте Requires=, если не уверены, что падение зависимого сервиса действительно должно валить и ваш.2. Осмысленно заполняйте блок [Service]
Это, по сути, ядро вашего unit-файла. Вот минимальный, но вполне рабочий пример:
[Service] ExecStart=/usr/local/bin/myapp WorkingDirectory=/var/www/myapp Restart=on-failure RestartSec=5s User=webuser Group=webgroup Environment=NODE_ENV=production
Разберёмся, что тут к чему:
ExecStart- команда запуска вашего сервиса. Только абсолютный путь.WorkingDirectory- задаёт директорию, относительно которой будут интерпретироваться относительные пути и в которую будут писаться логи.RestartиRestartSec- чтобы сервис не умирал от случайного сбоя. Без этого в проде никуда.User/Group- сбрасываем привилегии (не запускайте от root, если можно избежать).Environment- задавать переменные окружения тут чище и нагляднее, чем через.envи shell-обёртки.
Если приложению нужно что-то почистить при остановке:
ExecStop=/usr/local/bin/myapp-stop
А если сервис умеет принимать сигнал на перезагрузку конфигурации (например, как у Nginx):
ExecReload=/bin/kill -HUP $MAINPID
3.Выберите подходящий Type=
Type=simple
Типов вообще пять, но в реальности чаще всего используются вот эти:
simple- значение по умолчанию. Подходит, если ваше приложение не уходит в фон самостоятельно.forking- для старых демонов, которые сами себя форкают в фоне.notify- если приложение умеет общаться с systemd черезsd_notify().oneshot- для коротких задач, например инициализаторов или скриптов-хуков.idle- редкий зверь. Запускается, когда все активные задачи systemd завершились.
Не заморачивайтесь: если ваш сервис не форкается сам, Type=simple - ваш выбор.
4.Умело используйте StandardOutput и StandardError
StandardOutput=journal StandardError=inherit
По умолчанию вывод уходит в journald, что удобно для просмотра через journalctl. Обычно именно это и нужно.
Хотите писать логи в файл? Можно так:
StandardOutput=append:/var/log/myapp.log
Но честно? Лучше оставьте как есть и используйте журнал - если только у вас нет веской причины делать иначе.
5.Защитите систему (используйте sandbox-настройки)
Если ваш сервис торчит наружу (например, API или веб-приложение) - стоит ограничить ему доступ куда не надо:
ProtectSystem=full ProtectHome=yes NoNewPrivileges=true PrivateTmp=true
Эти параметры не дают вашему приложению лазить туда, где ему делать нечего.
Хотите устроить жёсткий режим? Добавьте:
CapabilityBoundingSet=
И после этого явно укажите только те Linux-способности (capabilities), которые реально нужны вашему приложению.
6.Не забудьте про блок [Install]
[Install] WantedBy=multi-user.target
Именно этот блок отвечает за то, будет ли сервис запускаться при загрузке.
WantedBy=multi-user.target- означает запуск при обычной загрузке системы (наиболее частый случай).WantedBy=default.target- для десктопных сессий.Alias=myapp.service- необязательный алиас, чтобы можно было запускать под другим, более дружелюбным именем.
Без этого блока systemctl enable просто ничего не сделает.
Пример чистого и годного к употреблению unit-файла:
[Unit] Description=My Production Web App After=network.target Wants=network-online.target [Service] ExecStart=/usr/local/bin/myapp WorkingDirectory=/srv/myapp Restart=on-failure RestartSec=5s User=web Group=web Environment=NODE_ENV=production StandardOutput=journal StandardError=journal ProtectSystem=full ProtectHome=yes PrivateTmp=true NoNewPrivileges=true [Install] WantedBy=multi-user.target