systemd unit файлы: пишем надёжный сервис для продакшена
Вы написали сервис, он работает в tmux на проде, и вы молитесь, чтобы никто не закрыл сессию. Знакомо? Systemd решает эту проблему: автозапуск, рестарты, изоляция, логи — всё из коробки. Нужен только правильный unit-файл.
Структура unit-файла: [Unit], [Service], [Install]
Unit-файл — INI-подобный конфиг в /etc/systemd/system/. Три секции:
[Unit]
Description=My Application Server
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/bin/server --config /etc/myapp/config.yaml
ExecReload=/bin/kill -HUP $MAINPID
[Install]
WantedBy=multi-user.target
[Unit] — метаданные и зависимости. After определяет порядок запуска, Requires — жёсткую зависимость.
[Service] — как запускать, от какого пользователя, что делать при падении.
[Install] — куда подключить сервис. multi-user.target = запуск при обычной загрузке.
Type=simple vs notify vs forking
Выбор Type определяет, когда systemd считает сервис запущенным:
- Type=simple (по умолчанию) — сервис считается готовым сразу после fork. Подходит для большинства современных приложений.
- Type=notify — сервис сам сообщает о готовности через
sd_notify("READY=1"). Лучший выбор при долгой инициализации. - Type=forking — для legacy-демонов, которые форкаются. Нужен
PIDFile=. Избегайте для нового кода.
# Для Go/Node/Python — simple или notify
[Service]
Type=notify
NotifyAccess=main
ExecStart=/opt/myapp/bin/server
# Для legacy-демонов
[Service]
Type=forking
PIDFile=/run/myapp.pid
ExecStart=/opt/myapp/bin/daemon -d
Restart policies и watchdog
Политики перезапуска — главная причина использовать systemd вместо nohup:
[Service]
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=300
StartLimitBurst=5
WatchdogSec=30
Варианты Restart=:
no— не перезапускать (по умолчанию)on-failure— только при ненулевом exit code или сигналеalways— перезапускать всегда, даже при exit 0on-abnormal— при сигнале, таймауте или watchdog
StartLimitBurst=5 + StartLimitIntervalSec=300 — если сервис упал 5 раз за 5 минут, прекратить попытки. Защита от бесконечного цикла рестартов.
Для watchdog приложение должно периодически вызывать sd_notify("WATCHDOG=1"). В Go — пакет coreos/go-systemd.
Hardening: ProtectSystem, PrivateTmp, NoNewPrivileges
Systemd даёт изоляцию без контейнеров:
[Service]
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
ReadWritePaths=/var/lib/myapp
NoNewPrivileges=true
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
SystemCallFilter=@system-service
MemoryDenyWriteExecute=true
ProtectSystem=strict— файловая система read-only, кроме указанных путейNoNewPrivileges=true— процесс не может повысить привилегииPrivateTmp=true— изолированный /tmpSystemCallFilter=@system-service— белый список syscalls
Проверить уровень hardening:
systemd-analyze security myapp.service
# Overall exposure level: 2.1 SAFE
Логирование через journald
Systemd захватывает stdout/stderr в journald автоматически:
[Service]
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
# Follow в реальном времени
journalctl -u myapp.service -f
# Логи за последний час
journalctl -u myapp.service --since "1 hour ago"
# Только ошибки
journalctl -u myapp.service -p err
Реальный пример: Go-сервис с graceful shutdown
Полный unit-файл для продакшена:
[Unit]
Description=Order Processing Service
After=network-online.target postgresql.service
Wants=network-online.target
Requires=postgresql.service
[Service]
Type=notify
User=orders
Group=orders
WorkingDirectory=/opt/orders
ExecStart=/opt/orders/bin/orders-server
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5
StartLimitBurst=5
WatchdogSec=30
TimeoutStopSec=30
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
NoNewPrivileges=true
ReadWritePaths=/var/lib/orders
SystemCallFilter=@system-service
StandardOutput=journal
SyslogIdentifier=orders
MemoryMax=512M
[Install]
WantedBy=multi-user.target
TimeoutStopSec=30 даёт 30 секунд на graceful shutdown. В Go это signal.NotifyContext + http.Server.Shutdown().
Деплой:
cp orders.service /etc/systemd/system/
systemctl daemon-reload
systemctl enable --now orders.service
systemctl status orders.service
Один unit-файл заменяет supervisor, pm2, docker-compose для простых деплоев. Сервис переживёт ребут, перезапустится при падении и будет изолирован не хуже контейнера.