OSEN-НИЙ SAAALEСкидка 50% на виртуальный хостинг и VDS
до 30.11.2025 Подробнее
Выберите продукт

systemd.path: запускаем сборки и задачи по событиям файловой системы

Разбираем, как с помощью systemd.path запускать сборки и фоновые задачи по событиям файловой системы. Это слой поверх inotify: минимум обвязки, нативные логи и sandbox systemd. Покажу шаблоны, дебаунс и отладку.
systemd.path: запускаем сборки и задачи по событиям файловой системы

Когда задача звучит как «пересобрать проект при изменении файлов» или «взять новые артефакты из каталога и обработать», большинство сразу думает о cron, скриптах с inotifywait или о полноценном CI. Но в Linux есть ещё один мощный инструмент — юниты systemd.path. Они используют ядровые события inotify, не тащат внешние зависимости и нативно интегрируются с systemd: логи, рестарт-политики, sandbox, шаблоны. В этой статье — системный взгляд, практические паттерны и отладка.

Как это работает

systemd.path — это «наблюдатель», который подписывается на события пути/каталога и активирует связанный .service. Модель проста:

  • Конфигурация .path задаёт, что именно наблюдать: файл, каталог или условие существования.
  • При событии systemd запускает указанный сервис (по умолчанию с тем же базовым именем, либо заданный через Unit=).
  • Сервис — ваша логика: сборка, конвертация, синхронизация и т.д.

Ключевые директивы секции [Path]:

  • PathModified= — срабатывает при изменении содержания файла (mtime) или директории (создание/удаление элементов).
  • PathChanged= — изменились метаданные/имя (перемещение, chmod, chown) или замена inode.
  • DirectoryNotEmpty= — каталог стал непустым (удобно для очередей).
  • PathExists= и PathExistsGlob= — появление файла(ов) по exact-пути или по маске.
  • MakeDirectory=, DirectoryMode= — создать каталог для слежения, если он отсутствует, с нужными правами.
  • Unit= — какое .service активировать по событию.

Важно: path units не передают в сервис «какой именно файл изменился». Они только триггерят запуск. Значит, логика сервиса должна быть идемпотентной и уметь сама определить, что делать.

На практике удобно держать такие watcher'ы на отдельном сервере: минимальная задержка реакции и изоляция окружения. Для этого подойдёт VDS с актуальным systemd — вы получите контроль над юнитами, логами и лимитами без ограничений общего хостинга.

Минимальный пример: пересборка при новом коммите

Самый стабильный триггер для Git-репозитория — файл ссылки на ветку, например .git/refs/heads/main. Он меняется при записи нового коммита в ветку, и это лучше, чем ориентироваться на .git/HEAD или общий каталог.

# /etc/systemd/system/mysite-build.path
[Unit]
Description=Watch repo branch and trigger build
Documentation=man:systemd.path(5)

[Path]
PathModified=/srv/mysite/.git/refs/heads/main
Unit=mysite-build.service

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/mysite-build.service
[Unit]
Description=Build mysite on repo update
ConditionPathExists=/srv/mysite

[Service]
Type=oneshot
WorkingDirectory=/srv/mysite
Environment=NODE_ENV=production
ExecStart=/usr/local/bin/build-site.sh
# Безопасность
User=deploy
Group=deploy
UMask=0077
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
RestrictSUIDSGID=yes
LockPersonality=yes
MemoryDenyWriteExecute=yes
# Активация
sudo systemctl daemon-reload
sudo systemctl enable --now mysite-build.path
sudo systemctl status mysite-build.path

Теперь любой git pull в /srv/mysite спровоцирует запуск mysite-build.service. Если коммиты прилетят пачкой, пока сервис ещё работает, systemd поставит следующий запуск в очередь; множественные события за время одного выполнения будут схлопнуты в один повторный запуск.

Очереди файлов: DirectoryNotEmpty

Классический кейс — положить новые файлы в «входную» папку и запустить обработчик. Пусть есть /srv/inbox; добавление любого файла должно стартовать сервис, который заберёт всё содержимое и очистит папку.

# /etc/systemd/system/inbox-processor.path
[Unit]
Description=Run processor when inbox has files

[Path]
DirectoryNotEmpty=/srv/inbox
Unit=inbox-processor.service
MakeDirectory=yes
DirectoryMode=0750

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/inbox-processor.service
[Unit]
Description=Process files from inbox atomically

[Service]
Type=oneshot
User=processor
Group=processor
WorkingDirectory=/srv
ExecStart=/usr/local/bin/process-inbox.sh
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=read-only
RestrictSUIDSGID=yes

Хитрость для гонок: сначала перемещайте все файлы из /srv/inbox во временный подкаталог /srv/inbox.work одной командой, а уже оттуда спокойно обрабатывайте. Так вы избегаете частичной обработки и конфликтов с загрузчиками.

Диаграмма: path-юнит активирует service при изменении файлов

PathChanged vs PathModified: как выбрать

  • PathModified: реагируйте на изменение содержимого. Для Git-веток, артефактов сборки, сборщиков статики — обычно это лучший выбор.
  • PathChanged: реагируйте на переименования или замену inode. Полезно, если деплой создаёт файл во временном месте и делает rename() в итоговый путь, либо если важны chmod/chown.
  • DirectoryNotEmpty: для очередей и «набросили файлов — запустили процессор».

Учтите поведение редакторов и инструментов: некоторые сохраняют во временный файл и делают атомарный rename() (это PathChanged), другие пишут прямо в файл (это PathModified). Если не уверены — включите оба, указав два параметра в секции [Path].

Шаблоны и мультиинстансы через @

Нужно наблюдать десятки проектов одинаково? Используйте шаблонные юниты. Экземпляр имени, вроде projectA, попадёт в спецификатор %i.

# /etc/systemd/system/repo-build@.path
[Unit]
Description=Watch repo branch for %i and trigger build

[Path]
PathModified=/srv/%i/.git/refs/heads/main
Unit=repo-build@%i.service

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/repo-build@.service
[Unit]
Description=Build %i on repo update
ConditionPathExists=/srv/%i

[Service]
Type=oneshot
WorkingDirectory=/srv/%i
ExecStart=/usr/local/bin/build-%i.sh
User=deploy
Group=deploy
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
# Включаем сразу несколько инстансов
sudo systemctl daemon-reload
sudo systemctl enable --now repo-build@projectA.path repo-build@projectB.path
sudo systemctl list-units --type=path

Так вы легко масштабируете «сборки по событию» на множество репозиториев или папок, сохраняя единообразие конфигов.

Дебаунс и «антишторм»

В systemd.path нет явного дебаунса по времени. Однако практическая устойчивость достигается так:

  • Один сервис — один запуск: делайте сервис Type=oneshot. systemd не запустит второй экземпляр, пока первый в работе; новые события слипнутся в один отложенный запуск.
  • Идемпотентность: пусть ваш скрипт сам сверяет «что уже сделано» и «что нужно сделать» (например, по хэшу артефактов или маркерным файлам).
  • Грабьте пачками: для очередей снимайте список всех доступных задач за один прогон, а не по одной.
  • Таймауты: добавляйте TimeoutStartSec=, чтобы зависшие сборки не держали очередь бесконечно.
  • Стратегия отложенного старта: если реально нужен тайм‑агрегатор, вызывайте из сервиса отдельный транзиентный юнит с задержкой: systemd-run --unit build-debounce --on-active=5s /usr/local/bin/build.sh. Path-юнит триггерит короткий «диспетчер», а тот планирует отложенный реальный запуск. Этот приём удобно дополнять флаг-файлом «занято».

Безопасность: минимальные привилегии

Сервисы, запускаемые по событиям, часто обрабатывают входящие данные. Сократите поверхность атаки:

  • User=, Group= — отдельный непривилегированный пользователь.
  • UMask=0077 — приватные артефакты на выходе.
  • NoNewPrivileges=yes, PrivateTmp=yes.
  • ProtectSystem=strict, ProtectHome=read-only.
  • ProtectKernelTunables=yes, ProtectKernelModules=yes, ProtectControlGroups=yes.
  • RestrictSUIDSGID=yes, LockPersonality=yes, MemoryDenyWriteExecute=yes.
  • ReadWritePaths= — ограничьте запись только нужными путями.

Не наблюдайте миро‑записываемые каталоги и не исполняйте из них напрямую. Гарантируйте, что «вход» и «выход» физически разделены и проверены правами.

Подробные приёмы изоляции разобраны в материале про жёсткий sandbox systemd для сервисов. См. раздел о безопасной конфигурации: чек-лист харденинга для systemd-сервисов.

Диагностика и отладка

  • systemctl status mysite-build.path — текущее состояние наблюдателя.
  • journalctl -u mysite-build.path -u mysite-build.service -f — смотрим логи в реальном времени.
  • systemd-analyze verify /etc/systemd/system/mysite-build.path — проверка синтаксиса.
  • systemctl show -p Triggers -p TriggeredBy mysite-build.path — связи между юнитами.
  • inotify-лимиты: при большом числе наблюдателей проверьте fs.inotify.max_user_watches, fs.inotify.max_user_instances, fs.inotify.max_queued_events и лимит дескрипторов процессов. Это критично для массовых path units.

Для ручной проверки сымитируйте событие: touch -m /srv/mysite/.git/refs/heads/main или mv файла в наблюдаемый каталог.

Сравнение с cron и systemd.timer

  • cron/systemd.timer — опрос по времени, не реагирует мгновенно, но стабилен и прост, когда события редкие или скан каталога дёшев.
  • systemd.path — реальное событие из ядра (inotify), минимальная задержка и нагрузка. Идеален для сборок и очередей, где «сигнал» — изменение файла.

Часто разумно комбинировать: .path запускает оперативную реакцию, а .timer раз в час запускает «сверку состояния» на случай пропущенных событий или ошибок. Больше о планировщиках и отличиях читайте в статье Cron, crontab и systemd.timer: сравнение.

Особенности файловых систем и окружений

  • Сетевые и FUSE‑FS: не все корректно пробрасывают inotify-события. Тестируйте: возможно, понадобится наблюдать локально смонтированный каталог или использовать PathExistsGlob= как запасной вариант.
  • Overlay в контейнерах: при слежении внутри контейнера проверяйте, приходят ли события от верхнего слоя. Иногда лучше вынести watcher на хост и триггерить сервис в контейнере через IPC или сокеты.
  • Ротация логов: при логротации часто происходит rename(). Для таких кейсов добавляйте PathChanged= на лог‑файл или директорию.
FastFox VDS
Облачный VDS-сервер в России
Аренда виртуальных серверов с моментальным развертыванием инфраструктуры от 195₽ / мес

Паттерны для CI/CD и сборок

  • Сборка статики по коммиту: наблюдайте .git/refs/heads/<branch>, сервис запускает ваш build и выкатывает артефакты в /var/www/site.
  • Пайплайн «готовность» через маркеры: внешняя система кладёт .ready файл; PathExistsGlob=/srv/builds/*.ready стартует сборщик, который подхватывает все .ready, обрабатывает и удаляет.
  • Транскодирование медиа: DirectoryNotEmpty=/srv/uploads — сервис забирает файлы в рабочую папку, конвертирует и кладёт результаты в /srv/public.
  • Атомарное обновление конфигов: PathChanged=/etc/myapp/config.yml — сервис валидирует и делает мягкий reload приложения.

Схема очереди: DirectoryNotEmpty и обработчик

Надёжность и идемпотентность

  • Повторяемость: пусть повторный запуск даёт тот же результат, что и единичный. Например, сравнивайте хэши входа/выхода и пропускайте работу, если нечего делать.
  • Разделение входа/выхода: не пишите артефакты туда, откуда их же читаете как «сигнал». Это снизит ложные срабатывания.
  • Маркер «в процессе»: при старте создания артефакта делайте .lock или отдельный служебный каталог, чтобы следующий прогон не стартовал тяжёлую работу повторно.

Жизненный цикл и управление

  • Активируют и отключают .path, а не .service: systemctl enable --now name.path, systemctl disable --now name.path.
  • Разовый ручной запуск логики — это systemctl start name.service.
  • Изменили юнит — systemctl daemon-reload, затем systemctl restart name.path.

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

  • Смотрите не на то: наблюдение каталога не означает, что вы увидите изменения во вложенных подпапках рекурсивно. Если нужна рекурсия, проектируйте сигнал осознанно (маркер, очередь, единая точка записи).
  • Глоб по PathModified: маски поддерживаются в PathExistsGlob, но не в PathModified/PathChanged. Для множества файлов используйте очередь или каталог как сигнал.
  • Права: юнит не имеет прав доступа к пути — событие не придёт. Проверьте владельца, группу и ProtectSystem/ReadWritePaths.
  • Слишком много наблюдателей: упираетесь в inotify-лимиты. Консолидируйте наблюдение на «сигнальный» файл или увеличивайте лимиты с пониманием рисков.

Пример с тестовой «сборкой» и логированием

Пусть у нас каталог /srv/demo/inbox, и мы хотим запускать обработчик, который пишет в журнал, сколько файлов пришло, и переносит их в /srv/demo/processed.

# /etc/systemd/system/demo-inbox.path
[Unit]
Description=Demo inbox trigger

[Path]
DirectoryNotEmpty=/srv/demo/inbox
Unit=demo-inbox.service
MakeDirectory=yes
DirectoryMode=0750

[Install]
WantedBy=multi-user.target
# /etc/systemd/system/demo-inbox.service
[Unit]
Description=Demo processor

[Service]
Type=oneshot
User=demo
Group=demo
WorkingDirectory=/srv/demo
ExecStart=/usr/local/bin/demo-process.sh
StandardOutput=journal
StandardError=inherit
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
# /usr/local/bin/demo-process.sh
#!/usr/bin/env bash
set -euo pipefail
INBOX="/srv/demo/inbox"
WORK="/srv/demo/work"
DEST="/srv/demo/processed"
mkdir -p "$WORK" "$DEST"
# Атомарно забираем всё входящее
shopt -s nullglob
count=0
for f in "$INBOX"/*; do
  base=$(basename "$f")
  mv "$f" "$WORK/$base"
  count=$((count+1))
done
logger -t demo "picked $count files"
for f in "$WORK"/*; do
  base=$(basename "$f")
  # Имитация обработки
  sleep 1
  mv "$f" "$DEST/$base"
  logger -t demo "processed $base"
done
logger -t demo "done"
# Запуск
sudo systemctl daemon-reload
sudo systemctl enable --now demo-inbox.path
# Тест
sudo -u demo sh -c 'echo test > /srv/demo/inbox/a.txt'
journalctl -t demo -f

Итоги

systemd.path — это легковесный, надёжный и поддерживаемый способом systemd механизм автоматизации по файловым событиям. Он снимает необходимость писать собственные демоны‑наблюдатели, даёт единую точку логирования и управления и отлично ложится на сценарии «builds по событию», очереди обработки и небольшие CI‑сборки на одном сервере. Комбинируя PathModified, PathChanged, DirectoryNotEmpty и аккуратно проектируя «сигнальные» пути, вы получаете предсказуемое поведение без опросов и лишней нагрузки. Добавьте идемпотентность, sandbox и мониторинг — и ваша автоматизация станет проще и надёжнее.

Если вы уже используете systemd.timer, попробуйте дополнить его systemd.path для мгновенной реакции на изменения. А если вы привыкли к внешним watcher‑скриптам, переведите их на unit‑ы: в долгосрочной перспективе обслуживание и отладка станут заметно проще.

Поделиться статьей

Вам будет интересно

Grafana Agent Flow на VDS: единый агент для metrics, logs и traces (Prometheus, Loki, Tempo, OTLP) OpenAI Статья написана AI (GPT 5)

Grafana Agent Flow на VDS: единый агент для metrics, logs и traces (Prometheus, Loki, Tempo, OTLP)

Grafana Agent в режиме Flow — лёгкий агент на одном VDS для метрик, логов и трейсов с отправкой в Prometheus/VictoriaMetrics, Loki ...
systemd-nspawn на VDS: лёгкие контейнеры, изоляция и сеть без Kubernetes OpenAI Статья написана AI (GPT 5)

systemd-nspawn на VDS: лёгкие контейнеры, изоляция и сеть без Kubernetes

Как запустить и подружить systemd-nspawn с вашим VDS: развертывание контейнеров, изоляция, bind mounts, сеть и cgroup-лимиты, упра ...
Node.js keepalive и http.Agent: практическая настройка с Nginx и upstream-пулами OpenAI Статья написана AI (GPT 5)

Node.js keepalive и http.Agent: практическая настройка с Nginx и upstream-пулами

Разбираем пул http.Agent в Node.js и практику keepalive: какие параметры важны (maxSockets, freeSocketTimeout, socketActiveTTL), к ...