# Создание собственной службы Systemd

Индекс LLMS: [llms.txt](/llms.txt)

---

Приложение нужно запускать при старте сервера, перезапускать при падении и вести логи. Shell-скрипты в /etc/rc.local не дают ни перезапуска, ни зависимостей. Systemd решает всё это одной декларацией.

## Зачем писать свой unit-файл

Вместо скриптов и супервизоров вроде supervisord systemd даёт единый интерфейс управления службами. Он знает о сокетах, зависимостях, ресурсных лимитах и имеет встроенный journald для логов.

## Структура unit-файла

Unit-файл — ini-подобный файл в `/etc/systemd/system/` или `/run/systemd/system/` для runtime. Имя формата `имя.service`.

```ini
[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
ExecStart=/opt/myapp/bin/start.sh
Restart=on-failure
User=myapp

[Install]
WantedBy=multi-user.target
```

## Обязательные секции и директивы

| Секция | Ключ | Назначение |
|--------|------|------------|
| `[Unit]` | `Description` | Человеческое описание |
| `[Unit]` | `After` | Порядок запуска относительно других юнитов |
| `[Service]` | `Type` | Способ демонизации |
| `[Service]` | `ExecStart` | Команда запуска |
| `[Install]` | `WantedBy` | Цель, при которой включается |

### Type

- `simple` — процесс сам остаётся в foreground, systemd ждёт его
- `forking` — процесс форкается, родитель завершается (классический демон)
- `oneshot` — однократный запуск, не держит процесс
- `exec` — аналог simple, но с ожиданием завершения ExecStartPre

Для современных приложений почти всегда `simple`.

## Пример: простой сервис приложения

```ini
[Unit]
Description=Backend API Service
Documentation=https://internal.example.com/docs
After=network-online.target postgresql.service
Wants=network-online.target

[Service]
Type=simple
User=appuser
Group=appgroup
WorkingDirectory=/opt/api
ExecStart=/opt/api/start.sh
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=5s
TimeoutStartSec=30s
TimeoutStopSec=60s
Environment=NODE_ENV=production
EnvironmentFile=/etc/default/api
StandardOutput=journal
StandardError=journal
SyslogIdentifier=api-backend

# Hardening
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/opt/api /var/log/api
ProtectKernelTunables=true
ProtectControlGroups=true

[Install]
WantedBy=multi-user.target
```

> [!NOTE]
> Флаг `--user` создаёт пользовательский сервис, который живёт вместе с сессией пользователя, а не системой.

### Python: venv и gunicorn

В `ExecStart` указывайте интерпретатор из venv, а не системный `python` — зависимости сервиса не смешаются с пакетами хоста:

```ini
[Service]
Type=simple
User=appuser
Group=appuser
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/venv/bin/python -m myapp
EnvironmentFile=/etc/myapp/env
Restart=on-failure
RestartSec=5
```

Для WSGI-приложения:

```ini
ExecStart=/opt/myapp/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 myapp.wsgi:application
```

Секреты держите в `EnvironmentFile` (`KEY=VALUE`), права `600`, владелец `root:appuser`. Если юнит не стартует, `journalctl -u myapp` обычно показывает traceback, отсутствующий `WorkingDirectory` или пропавший env-файл.

## Полезные директивы для продакшена

### Перезапуск

```ini
Restart=on-failure          # при ненулевом коде завершения
Restart=on-abnormal         # при signal или timeout
Restart=always              # всегда, включая clean stop
RestartSec=5
```

### Зависимости

```ini
After=network.target         # после поднятия сети
After=postgresql.service     # после конкретной службы
Wants=network-online.target  # попытка запустить, не критично
Requires=postgresql.service  # жёсткая зависимость
```

### Ограничения

```ini
LimitNOFILE=65536
LimitNPROC=4096
MemoryMax=512M
CPUQuota=50%
```

### Логирование

```ini
StandardOutput=journal
StandardError=journal
SyslogIdentifier=myapp
```

Читать логи:

```bash
journalctl -u myapp -f
journalctl -u myapp --since "1 hour ago"
journalctl -u myapp -p err
```

## Активация и управление

```bash
# Перечитать unit-файлы
sudo systemctl daemon-reload

# Запустить
sudo systemctl start myapp

# Проверить статус
sudo systemctl status myapp

# Включить автозапуск
sudo systemctl enable myapp

# Перезагрузить конфигурацию без рестарта
sudo systemctl reload myapp

# Перезапустить
sudo systemctl restart myapp

# Остановить
sudo systemctl stop myapp

# Убрать из автозапуска
sudo systemctl disable myapp
```

Проверка синтаксиса без применения:

```bash
systemd-analyze verify /etc/systemd/system/myapp.service
```

## Типичные ошибки

**Missing ExecStart**. Unit падает при запуске, в логах `Unit entered failed state`.

**Не указан Type**. По умолчанию может быть `simple`, но если процесс демонизируется сам, нужен `forking`.

**Неправильный путь к скрипту**. Проверяйте, что файл существует и имеет бит исполнения.systemd не найдёт ошибку в пути — просто не запустит.

**Runtime unit без перезагрузки**. Изменили файл, забыли `daemon-reload`. Systemd работает со старой версией.

**WorkingDirectory не существует**. Если указана директория, она должна быть.

**Restart=always без Exit**. Если процесс корректно завершается, `always` поднимет его снова. Это не всегда ожидаемо при `systemctl stop`.

> [!WARNING]
> Не редактируйте unit-файлы из пакетов напрямую в `/usr/lib/systemd/system/`. Они перезаписываются при обновлении. Пользуйтесь drop-in файлами в `/etc/systemd/system/<name>.service.d/`.

Drop-in пример:

```bash
mkdir -p /etc/systemd/system/myapp.service.d
```

```ini
# /etc/systemd/system/myapp.service.d/override.conf
[Service]
Environment=DEBUG=1
RestartSec=10s
```

```bash
sudo systemctl daemon-reload
sudo systemctl restart myapp
```
