Когда задача звучит как «пересобрать проект при изменении файлов» или «взять новые артефакты из каталога и обработать», большинство сразу думает о 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 одной командой, а уже оттуда спокойно обрабатывайте. Так вы избегаете частичной обработки и конфликтов с загрузчиками.

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=на лог‑файл или директорию.
Паттерны для 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 приложения.

Надёжность и идемпотентность
- Повторяемость: пусть повторный запуск даёт тот же результат, что и единичный. Например, сравнивайте хэши входа/выхода и пропускайте работу, если нечего делать.
- Разделение входа/выхода: не пишите артефакты туда, откуда их же читаете как «сигнал». Это снизит ложные срабатывания.
- Маркер «в процессе»: при старте создания артефакта делайте
.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‑ы: в долгосрочной перспективе обслуживание и отладка станут заметно проще.


