Systemd-юниты, которые я копирую из проекта в проект

2025-11-19 · заметка

Каждый раз, поднимая новый сервис, я начинаю с одного и того же юнита. Он не идеален, но закрывает базовые ситуации: падение процесса, утечка памяти, слишком долгие попытки подняться, отсутствие записей в журнале.

Шаблон

[Unit]
Description=My service
Documentation=file:///opt/myservice/README.md
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=notify
User=svc
Group=svc
WorkingDirectory=/opt/myservice
EnvironmentFile=-/etc/myservice/env
ExecStart=/opt/myservice/bin/service
ExecReload=/bin/kill -HUP $MAINPID

Restart=on-failure
RestartSec=3s
StartLimitIntervalSec=60
StartLimitBurst=5
TimeoutStartSec=30
TimeoutStopSec=20

# --- лимиты ---
MemoryMax=1G
MemoryHigh=768M
CPUQuota=150%
TasksMax=512
LimitNOFILE=8192

# --- изоляция ---
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/var/lib/myservice /var/log/myservice
ProtectKernelTunables=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes

# --- журнал ---
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myservice

[Install]
WantedBy=multi-user.target

Что здесь важно

StartLimitBurst — без него сервис в цикле падения бесконечно дёргает процессор и засоряет журнал. Пять попыток за минуту, дальше systemd останавливается и оставляет юнит в failed — это заметно.

MemoryHigh и MemoryMax — мягкий и жёсткий потолок. Soft limit заставляет ядро активнее вытеснять страницы процесса, hard limit убивает. Утечка становится видна сразу, а не через неделю, когда свободной памяти на хосте не осталось.

ProtectSystem=strict плюс ReadWritePaths — сервис физически не может писать куда попало. Если его взломают, ущерб ограничен двумя каталогами.

Type=notify требует от приложения сообщить о готовности. Если приложение этого не умеет, ставьте simple, но тогда After= не гарантирует, что база уже принимает соединения.

Проверка

systemd-analyze verify /etc/systemd/system/myservice.service
systemctl daemon-reload && systemctl restart myservice
systemctl status myservice --no-pager -l
journalctl -u myservice -f

Отдельно стоит проверить, что сервис действительно поднимется после перезагрузки хоста, а не только после ручного restart: systemctl is-enabled myservice должен отвечать enabled.