Большинство материалов о systemd посвящены системным службам, которые стартуют при загрузке от имени root и управляют инфраструктурными процессами. Но systemd умеет гораздо больше. У каждого пользователя есть собственный экземпляр systemd, который запускается при входе в систему и живёт ровно столько, сколько существует активная сессия. Этот инструмент позволяет автоматизировать пользовательские задачи, запускать фоновые процессы и планировать задания по расписанию - всё это без единой строки в sudoers и без прикосновения к системным конфигурационным файлам.
Честно говоря, многие разработчики и администраторы годами работают с Linux, не подозревая об этой возможности. Пользовательские службы systemd закрывают целый класс задач, которые раньше решались через cron, screen и nohup с их ограниченными возможностями мониторинга.
Как устроен пользовательский экземпляр systemd
При входе пользователя в систему модуль PAM автоматически запускает отдельный процесс systemd --user. Этот процесс существует независимо от системного systemd (PID 1) и управляет исключительно пользовательскими юнитами. Ключевое различие - пользовательский экземпляр работает в контексте конкретного пользователя, с его правами и окружением.
Посмотреть статус собственного пользовательского экземпляра можно так:
systemctl --user status
Пользовательские юниты могут находиться в нескольких директориях, которые читаются в следующем порядке приоритета:
~/.config/systemd/user/ # файлы конкретного пользователя (приоритет)
/etc/systemd/user/ # системные файлы для всех пользователей
/usr/lib/systemd/user/ # файлы установленных пакетов
Для большинства задач используется первая директория. Она принадлежит пользователю, не требует прав root для изменения и хранится рядом с другими пользовательскими конфигурационными файлами. Именно туда и нужно класть собственные юниты.
Важный нюанс архитектуры: пользовательский экземпляр systemd работает вне сессий. Это значит, что служба, запущенная через systemctl --user, не привязана к конкретному терминалу или SSH-соединению. Если соединение оборвётся, служба продолжит работу до тех пор, пока у пользователя есть хотя бы одна активная сессия.
Создание пользовательской службы с нуля
Структура пользовательского service-файла в точности повторяет структуру системного. Те же секции [Unit], [Service], [Install], те же директивы. Разница только в расположении файла и в том, как им управляют.
Пример службы, которая запускает Python HTTP-сервер для раздачи файлов из домашней директории:
# ~/.config/systemd/user/fileserver.service
[Unit]
Description=Personal HTTP File Server
After=network.target
[Service]
ExecStart=/usr/bin/python3 -m http.server 8080
WorkingDirectory=%h/public
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=default.target
Спецификатор %h разворачивается в домашнюю директорию пользователя - удобный способ писать переносимые конфиги. После создания файла нужно перезагрузить конфигурацию пользовательского менеджера и запустить службу:
systemctl --user daemon-reload
systemctl --user enable fileserver.service
systemctl --user start fileserver.service
systemctl --user status fileserver.service
Журнал пользовательской службы читается через journalctl с флагом --user:
journalctl --user -u fileserver.service -f
Именно этот флаг отделяет пользовательские записи от системных. Без него journalctl будет искать службу в системном журнале и ничего не найдёт.
Таймеры как замена cron
Таймеры systemd устроены интереснее, чем кажется. В отличие от cron, где задание и расписание описываются одной строкой, в systemd это два отдельных файла: service-файл описывает, что делать, а timer-файл - когда делать. Такое разделение позволяет запускать одну и ту же службу как вручную, так и по таймеру, и независимо смотреть её статус.
Таймеры поддерживают два режима. Монотонные таймеры отсчитывают время от события (загрузка системы, старт самого таймера, последнее успешное выполнение). Календарные таймеры работают по расписанию в стиле cron, но с более гибким синтаксисом.
Пример пары файлов для ежедневного резервного копирования:
# ~/.config/systemd/user/backup.service
[Unit]
Description=Daily Backup to Remote Storage
[Service]
Type=oneshot
ExecStart=/home/user/scripts/backup.sh
StandardOutput=journal
StandardError=journal
# ~/.config/systemd/user/backup.timer
[Unit]
Description=Run backup daily at 3 AM
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
Unit=backup.service
[Install]
WantedBy=timers.target
Директива Persistent=true - одно из важных отличий от cron. Если машина была выключена в момент, когда таймер должен был сработать, при следующем включении задание запустится сразу же. cron пропустил бы его без следа.
Активировать таймер и проверить расписание:
systemctl --user enable backup.timer
systemctl --user start backup.timer
systemctl --user list-timers
Для разовых или экспериментальных заданий существуют транзиентные таймеры через systemd-run, которые не требуют создания файлов:
# Запустить скрипт через 30 минут
systemd-run --user --on-active=30min /home/user/scripts/notify.sh
# Запускать каждые 2 часа
systemd-run --user --on-active=2h --on-unit-active=2h /home/user/scripts/sync.sh
Переменные окружения и ограничения пользовательского экземпляра
Здесь скрывается самая частая ловушка при работе с пользовательскими службами. Пользовательский экземпляр systemd не наследует переменные окружения из ~/.bashrc или ~/.profile. Это не баг, а осознанное архитектурное решение: служба должна работать одинаково вне зависимости от того, как именно пользователь вошёл в систему.
Проверить текущее окружение пользовательского менеджера:
systemctl --user show-environment
Если PATH или другие переменные нужны службам, их передают явно. Один из способов - добавить в конец ~/.bash_profile:
# Экспортировать переменные в пользовательский экземпляр systemd
systemctl --user import-environment PATH SOME_VAR ANOTHER_VAR
Другой способ - файлы в директории ~/.config/environment.d/. Каждый файл с расширением .conf в этой директории читается пользовательским менеджером при старте:
# ~/.config/environment.d/myapp.conf
MY_APP_HOME=/home/user/myapp
MY_APP_PORT=3000
NODE_ENV=production
Ещё одна особенность - переменная XDG_RUNTIME_DIR. Она указывает на временную директорию пользователя /run/user/UID, которая создаётся при входе и удаляется при выходе. Именно там живёт сокет D-Bus пользовательской сессии. Если при попытке использовать systemctl --user появляется ошибка "Failed to connect to bus", причина почти всегда в том, что эта переменная не установлена. Быстрое решение для скриптов:
export XDG_RUNTIME_DIR="/run/user/$(id -u)"
export DBUS_SESSION_BUS_ADDRESS="unix:path=${XDG_RUNTIME_DIR}/bus"
Lingering и службы, которые живут дольше пользовательской сессии
По умолчанию пользовательский экземпляр systemd завершается, когда пользователь закрывает последнюю сессию. Для интерактивной работы это логично, но для серверных сценариев такое поведение неприемлемо. Синхронизация файлов, личный бот, фоновый агент - всё это должно работать постоянно, независимо от того, залогинен пользователь в данный момент или нет.
Для этого существует механизм lingering. Он заставляет systemd запустить пользовательский экземпляр при старте системы и не завершать его при выходе из сессии:
loginctl enable-linger $USER
loginctl show-user $USER | grep Linger
После включения lingering пользовательские службы с WantedBy=default.target и systemctl --user enable будут стартовать при загрузке системы. Это позволяет обычному пользователю без прав root держать постоянно работающий процесс - сценарий, который раньше требовал либо системной службы, либо нестандартных решений.
Отдельного внимания заслуживает Raspberry Pi и похожие embedded-системы, где нужно запустить пользовательское приложение при загрузке без системной службы. Lingering плюс пользовательский юнит решают эту задачу без прав суперпользователя после первоначальной настройки.
Отладка пользовательских юнитов и журналирование
Пользовательские службы поддерживают тот же инструментарий мониторинга, что и системные. Главное - не забывать флаг --user во всех командах.
# Список всех таймеров с расписанием
systemctl --user list-timers --all
# Проверить файл юнита на ошибки
systemd-analyze --user verify ~/.config/systemd/user/myservice.service
# Журнал с фильтрацией по службе
journalctl --user -u myservice.service --since "1 hour ago"
systemd-analyze --user verify - первое, что стоит запускать при написании нового юнита. Команда проверяет синтаксис и указывает на типичные ошибки конфигурации до того, как служба попытается запуститься и упасть в непонятном состоянии.
Пользовательские службы systemd меняют подход к автоматизации задач на Linux. Полноценное управление жизненным циклом процессов, журналирование через journald, зависимости между юнитами, перезапуск при сбоях, расписание с точностью до секунды - всё это доступно без root-доступа. Разработчик, который однажды перевёл свои скрипты запуска с nohup и cron на пользовательские юниты systemd, редко возвращается назад.