Transparent inotify injection for NFS filesystems
FakeNotify solves the fundamental problem that NFS mounts don't emit inotify events, breaking applications like Jellyfin, Plex, Sonarr, Radarr, and qBittorrent that rely on filesystem change notifications.
┌─────────────────────────────────────────────────┐
│ Application (Jellyfin, Sonarr, etc.) │
│ │
│ inotify_init() ──→ [HOOK] ──→ returns pipe fd │
│ inotify_add_watch() ──→ [HOOK] ──→ returns wd │
│ read(fd) ←── receives inotify_event structs ────┤
└─────────────────────────────────────────────────┘
↑
LD_PRELOAD=libfakenotify.so
↑
Unix Socket IPC
↓
┌─────────────────────────────────────────────────┐
│ fakenotifyd (daemon) │
│ │
│ • Polls NFS mounts for changes │
│ • Tracks watch descriptors ↔ paths │
│ • Writes synthetic inotify_event to pipes │
│ • CLI for runtime configuration │
└─────────────────────────────────────────────────┘
Two components:
libfakenotify.so- LD_PRELOAD library that interceptsinotify_init,inotify_add_watch,inotify_rm_watch, returns pipe fds instead, and connects to the daemonfakenotifyd- Background daemon that polls NFS paths, detects changes, and writes syntheticinotify_eventstructs to connected applications
- Transparent - No application modifications needed, just
LD_PRELOAD - Docker-friendly - Works with containerized apps via volume-mounted socket
- Configurable polling - Adjust intervals per-path based on your needs
- Runtime CLI - Add/remove watched paths without restart
curl -sSL https://raw.githubusercontent.com/zachhandley/FakeNotify/main/install-release.sh | sudo bashThen configure and start:
sudo nano /etc/fakenotify/config.toml # Add your NFS paths
sudo systemctl enable --now fakenotifygit clone https://github.com/zachhandley/FakeNotify.git
cd FakeNotify
cargo build --release
sudo ./install.sh# Start with default config
fakenotifyd start
# Or specify config file
fakenotifyd start --config /etc/fakenotify/config.toml# Add an NFS path to monitor
fakenotifyd add /mnt/media --poll-interval 5s
# Remove a path
fakenotifyd remove /mnt/media
# List watched paths
fakenotifyd list
# Check status
fakenotifyd status# Single application
LD_PRELOAD=/usr/lib/libfakenotify.so jellyfin
# Docker container
docker run -e LD_PRELOAD=/fakenotify/libfakenotify.so \
-v /usr/lib/libfakenotify.so:/fakenotify/libfakenotify.so:ro \
-v /run/fakenotify.sock:/run/fakenotify.sock \
jellyfin/jellyfinThe cleanest deployment is a sidecar container that publishes the preload library and socket into a shared named volume. No host install is required on consumer hosts.
# docker-compose.yml
services:
fakenotify:
image: ghcr.io/zachhandley/fakenotify:latest
volumes:
- fakenotify-share:/fakenotify
- ./fakenotify.toml:/etc/fakenotify/config.toml:ro
- /mnt/media:/media:ro
command: ["start", "--config", "/etc/fakenotify/config.toml"]
jellyfin:
image: jellyfin/jellyfin
environment:
- LD_PRELOAD=/fakenotify/lib/libfakenotify_preload.so
- FAKENOTIFY_SOCKET=/fakenotify/run/fakenotify.sock
volumes:
- fakenotify-share:/fakenotify:ro
- /mnt/media:/media
depends_on:
- fakenotifyThe entrypoint inside the sidecar publishes
/fakenotify/lib/libfakenotify_preload.so and listens on
/fakenotify/run/fakenotify.sock. Sibling containers mount the same
volume read-only and point LD_PRELOAD + FAKENOTIFY_SOCKET at the
volume paths.
For LSIO containers (Sonarr, Radarr, etc.), use the DockerMod alongside
the sidecar. The mod auto-detects the shared volume — no manual
LD_PRELOAD env needed.
services:
fakenotify:
image: ghcr.io/zachhandley/fakenotify:latest
volumes:
- fakenotify-share:/fakenotify
- ./fakenotify.toml:/etc/fakenotify/config.toml:ro
- /mnt/media:/media:ro
command: ["start", "--config", "/etc/fakenotify/config.toml"]
sonarr:
image: linuxserver/sonarr
environment:
- DOCKER_MODS=ghcr.io/zachhandley/fakenotify-mod:latest
volumes:
- fakenotify-share:/fakenotify:ro
- /mnt/media:/media
depends_on:
- fakenotifyIf you prefer to run fakenotifyd as a systemd unit on the host (e.g.
for non-docker workloads), the original pattern still works — install
via install-release.sh and bind-mount /run/fakenotify plus
/usr/local/lib/libfakenotify_preload.so into the consumer container.
See install-release.sh for the install steps.
/etc/fakenotify/config.toml:
[daemon]
socket = "/run/fakenotify.sock"
log_level = "info"
[[watch]]
path = "/mnt/media"
poll_interval = "5s"
recursive = true
[[watch]]
path = "/mnt/downloads"
poll_interval = "2s"
recursive = trueLinux's inotify monitors filesystem changes at the kernel VFS layer. When files change on an NFS server (or from another NFS client), the local kernel never sees the operation - it happens remotely. Therefore, inotify watches on NFS mounts are silent.
This affects:
- Jellyfin/Plex/Emby - Library not updating when new media added
- Sonarr/Radarr - Downloads not detected (when using folder watching)
- qBittorrent - Watched folders not triggering
- Any app using
inotify,fswatch,watchdog, etc.
Uses the redhook crate to intercept:
inotify_init()/inotify_init1()- Returns a pipe fd insteadinotify_add_watch()- Registers path with daemon, returns synthetic wdinotify_rm_watch()- Unregisters path with daemon
The pipe fd is indistinguishable from a real inotify fd to the application - it works with poll(), epoll(), select(), and blocking read().
Uses the notify crate with PollWatcher backend:
- Periodic
stat()calls to detect mtime/ctime changes - Directory listing comparison for create/delete detection
- Debouncing via
notify-debouncer-fullto coalesce rapid changes
Writes standard inotify_event structs to pipes:
struct inotify_event {
int wd; // Watch descriptor
uint32_t mask; // Event mask (IN_CREATE, IN_MODIFY, etc.)
uint32_t cookie; // Cookie for rename pairing
uint32_t len; // Length of name field
char name[]; // Filename (variable length)
};- Only affects dynamically linked binaries - Static binaries bypass LD_PRELOAD
- Polling latency - Changes detected on poll interval, not instantly
- NFS attribute caching - May need
actimeo=0mount option for immediate visibility - No rename cookie pairing -
IN_MOVED_FROM/IN_MOVED_TOwon't have matching cookies across polls
- Linux (uses Linux-specific inotify API)
- Rust 1.75+ (for building)
- NFS mounts accessible to the daemon
MIT