← Notes

inotify-rsync: push files to other routers the moment they change

· openwrt, ucode, rsync, prometheus

Some files on my hubs are produced in one place: one hub renders a few static files and downloads a few big ones, and the other hubs serve copies. The copies should follow quickly. A cron job means either lag or a lot of pointless runs, so I wanted rsync to fire when something actually changes.

inotify-rsync does exactly that and nothing more: watch some directories, run rsync to a destination when they change.

Configuration

One UCI section per watch:

# /etc/config/inotify-rsync
config watch 'site'
	option enabled '1'
	list src '/data/site'
	option dest 'root@peer.example.org:/data/'
	option rsync_args "-Ppra --delete --delete-delay -e 'dbclient -i /root/.ssh/id_ed25519 -y'"
	list filter 'exclude *.tmp'
	option textfile_dir '/run/prometheus/textfile'

Each watch is its own procd instance, respawned if it dies. /etc/init.d/inotify-rsync rsync [watch] forces a sync by hand, which is handy right after deployment. On OpenWrt the ssh client is usually Dropbear’s dbclient, so rsync_args is split respecting quotes — the -e '…' above stays one argument.

From inotifyd to ucode

The first version was a shell script around BusyBox’s inotifyd applet. It worked, but inotifyd runs as a child process, and when procd killed the script on restart the child was left behind — orphaned watchers piling up.

So in August 2026 it was rewritten in ucode, and I wrote a small C module for it, ucode-mod-inotify, that exposes the Linux inotify API to ucode directly. The script now:

Metrics

With textfile_dir set, every watch writes a small .prom file for the node exporter’s textfile collector:

The counters are read back on start, so they survive restarts, and the file is replaced atomically. Two alerts are enough to watch it: errors increasing, and a last run that is suspiciously old.