Новинка Виртуальный VDS сервер в Финляндии от 390р
Выберите продукт

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-сервер
Виртуальные серверы с быстрым запуском и гибкой конфигурацией от 390₽ / мес
Доступные локации
Россия Нидерланды Финляндия

Паттерны для 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‑ы: в долгосрочной перспективе обслуживание и отладка станут заметно проще.

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

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

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

Как безопасно выполнить bootstrap MariaDB Galera после полного останова

После одновременной остановки всех узлов Galera обычного запуска MariaDB недостаточно. Сначала найдите сервер с последней транзакц ...
CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

CoreDNS в Kubernetes: исправляем CrashLoopBackOff, SERVFAIL и DNS-петлю

Разберём, почему CoreDNS перезапускается или отвечает SERVFAIL, как обнаружить замкнутую пересылку DNS-запросов, проверить доступн ...
Apache Kafka KRaft на VDS: отдельные controller и broker

Apache Kafka KRaft на VDS: отдельные controller и broker

Соберём новый Kafka-кластер без ZooKeeper на шести Linux VDS: три узла controller образуют отказоустойчивый metadata quorum, а три ...