Серверное оборудование для бизнеса — надёжные решения для любых задач Подробнее →
Статья

systemd за 30 минут: от юнитов до таймеров

systemd давно стал стандартом инициализации в большинстве Linux-дистрибутивов, но многие сисадмины до сих пор знают его на уровне systemctl restart nginx и не глубже. Между тем systemd умеет гораздо больше: управлять таймерами, монтированием, сетевыми зависимостями и даже изолировать процессы без контейнеров. Разберём всё по порядку.

Типы юнитов

systemd оперирует юнитами (unit) — файлами конфигурации с расширением, определяющим тип:

  • .service — управление демонами и процессами
  • .timer — замена cron, запуск по расписанию
  • .mount — точки монтирования (генерируется из /etc/fstab)
  • .socket — socket activation, запуск сервиса по первому соединению
  • .target — группы юнитов, аналог runlevel
  • .path — мониторинг файловой системы, запуск при изменении файла

Системные юниты хранятся в /lib/systemd/system/, пользовательские переопределения — в /etc/systemd/system/.

Создаём свой сервис

Предположим, у нас есть скрипт /opt/myapp/server.py. Создаём юнит:

cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Application Server
After=network.target postgresql.service
Requires=postgresql.service

[Service]
Type=simple
User=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 /opt/myapp/server.py
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=multi-user.target
EOF

Активируем и запускаем:

systemctl daemon-reload
systemctl enable --now myapp.service
systemctl status myapp.service

Зависимости: After, Requires, Wants

Три ключевых директивы для порядка запуска:

  • After= — этот юнит стартует ПОСЛЕ указанных. Только порядок, не зависимость.
  • Requires= — жёсткая зависимость. Если зависимость не запустилась — этот юнит тоже не запустится.
  • Wants= — мягкая зависимость. Попробует запустить, но не упадёт если не получилось.

Типичная ошибка: написать After=postgresql.service без Requires= и удивляться, почему приложение стартует раньше базы данных при определённых условиях.

Journalctl: просмотр логов

Забудьте о tail -f /var/log/syslog — journald хранит структурированные логи с метаданными:

# Логи конкретного сервиса
journalctl -u myapp.service

# Следить в реальном времени
journalctl -u myapp.service -f

# Логи с момента последнего запуска
journalctl -u myapp.service -b

# Логи за последний час
journalctl -u myapp.service --since "1 hour ago"

# Только ошибки
journalctl -u myapp.service -p err

# Вывод в JSON
journalctl -u myapp.service -o json-pretty | head -50

systemd Timers: замена cron

systemd timers — это полноценная замена cron с рядом преимуществ: логи в journald, зависимости от других юнитов, возможность запуска после загрузки системы.

Для каждого таймера нужны два файла. Пример — ежедневный бэкап:

# /etc/systemd/system/backup.service
[Unit]
Description=Daily Backup

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
User=backup
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup daily at 3am

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target

Директива Persistent=true запускает таймер, если он пропустил запуск (например, машина была выключена). Очень полезно для серверов с периодическими перезагрузками.

systemctl enable --now backup.timer
systemctl list-timers --all

Hardening: изоляция без контейнеров

systemd позволяет существенно ограничить права сервисов прямо в unit-файле:

[Service]
# Файловая система только для чтения (кроме /tmp, /var)
ProtectSystem=strict

# Приватный /tmp, недоступный другим процессам
PrivateTmp=true

# Запрет получения новых привилегий через suid/sudo
NoNewPrivileges=true

# Запрет записи в домашний каталог
ProtectHome=true

# Доступные для записи пути (исключение из ProtectSystem=strict)
ReadWritePaths=/var/lib/myapp /var/log/myapp

# Ограничение системных вызовов
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM

# Запуск от непривилегированного пользователя
User=myapp
Group=myapp

# Ограничение capabilities
CapabilityBoundingSet=
AmbientCapabilities=

Проверить уровень изоляции сервиса можно командой:

systemd-analyze security myapp.service

Она выдаст оценку от 0 (максимальная защита) до 10 (никакой защиты) с конкретными рекомендациями.

Частые ошибки

1. Забыть daemon-reload после изменения unit-файла. systemd читает файлы в кэш — без reload изменения не применятся.

2. Type=forking без PIDFile. Если процесс форкается (как старые демоны), нужно указать PIDFile= или использовать Type=notify если приложение поддерживает sd_notify.

3. ExecStart с shell-синтаксисом. systemd не запускает команды через shell. Конструкция ExecStart=/bin/bash -c "cmd1 && cmd2" работает, но ExecStart=cmd1 && cmd2 — нет.

4. Права на файл юнита. Файлы в /etc/systemd/system/ должны принадлежать root и не иметь прав на запись для других.

systemd — мощный инструмент, который при правильном использовании заменяет cron, init-скрипты, supervisor и отчасти даже контейнеры для изоляции. Стоит потратить время на изучение.

4 Ответа

  1. Таймеры реально удобнее cron — особенно то, что можно посмотреть когда последний раз запускался и когда следующий запуск через list-timers. В cron это было настоящей болью.

  1. journald у меня на паре серверов сожрал под 20 гигабайт, пока не выставил SystemMaxUse в journald.conf. Советую сразу ограничивать, особенно если сервисы болтливые.

  1. Раздел про hardening очень ценный. systemd-analyze security — отличная штука, показывает что именно надо добавить. Мы прогнали все наши сервисы и половина оказалась вообще без ограничений.

  1. А для Python-сервисов лучше использовать Type=exec или Type=simple? И стоит ли добавлять gunicorn в тот же юнит или делать отдельный?