Watches folders on the storage device for filesystem changes and automatically triggers library scans on services running on a separate VM (Immich, Plex, etc.).
NAS (Storage server) VM (Ubuntu)
┌─────────────────────┐ ┌─────────────────────┐
│ inotify-watcher │ HTTP POST │ inotify-trigger │
│ │ ──────────► │ │
│ watches folders │ │ calls Immich / │
│ debounces events │ │ Plex APIs │
└─────────────────────┘ └─────────────────────┘
- inotify-watcher — runs on the storage device, watches configured directories using Linux inotify, debounces bursts of events (e.g. 1000-file import → one webhook call), then POSTs to the trigger.
- inotify-trigger — runs on the VM, receives webhooks and calls the appropriate service APIs.
cd inotify-watcher
cp config.yaml.example config.yaml # edit paths, webhook URL, shared secret
docker compose up -d --buildIf you hit inotify watch limit errors:
echo "fs.inotify.max_user_watches=524288" | sudo tee /etc/sysctl.d/99-inotify.conf
sudo sysctl -p /etc/sysctl.d/99-inotify.confcd inotify-trigger
cp config.yaml.example config.yaml # edit API keys, shared secret
docker compose up -d --buildBoth services share a shared_secret that must match — the watcher sends it as a Bearer token, the trigger validates it.
inotify-watcher/config.yaml — define watch groups:
shared_secret: "your-secret"
groups:
- name: pictures
paths:
- /volume1/photo
debounce_seconds: 30
webhook_url: "http://<vm-ip>:64123/trigger/pictures"inotify-trigger/config.yaml — define what each group triggers:
shared_secret: "your-secret"
handlers:
pictures:
- type: immich_scan
url: "http://immich:2283"
api_key: "your-immich-api-key"
movies:
- type: plex_refresh
url: "http://plex:32400"
token: "your-plex-token"
section_id: "1"| Type | Description |
|---|---|
immich_scan |
Triggers an Immich external library scan |
plex_refresh |
Refreshes a Plex library section |
curl http://<vm-ip>:64123/health