
Sync Two Directories in Near-Real-Time with inotifywait and rsync
Before you run this
This guide builds a small daemon that watches a source directory with inotifywait and, whenever a file there changes, runs rsync to mirror those changes into a destination directory. The result is one-way, near-real-time replication: destination follows source, never the other way around.
A few things to be clear about before you touch a live system:
- This is one-way and it uses
--delete. When a file is removed from the source,rsync --deleteremoves it from the destination too. If you point this at the wrong destination, it will happily delete files there to make it match the source. That deletion is not reversible — there is no trash bin. Treat the destination as disposable/derived data, not as your only copy. - This is not a backup. A backup keeps history so you can recover yesterday's version. This keeps the destination identical to right now, including mistakes. If you delete or corrupt a source file, the corruption propagates in seconds.
- Privileges. The watcher and
rsyncneed read access to the source and write access to the destination. Run it as a user that owns both, or as root if the paths require it. You only needsudo/root to install packages, to raise the kernel's inotify watch limit (asysctlsetting), and to install the systemd unit. The sync itself should run as the least-privileged user that can do the job. - Test first. Read the script. Then try it against two throwaway directories full of copied test files on a non-production machine or VM. Watch it delete a file on the destination when you delete one on the source — so the behaviour holds no surprises later — before you aim it at anything you care about.
What I'm assuming
I'm on Debian 12 / Ubuntu 22.04, bash, systemd. The tools are rsync and inotifywait (from the inotify-tools package). On RHEL/Alma the package is also inotify-tools, installed with dnf; the rest is identical. I'll show a local-to-local sync first, then note the one-line change for syncing to a remote host over SSH.
Install the tools
sudo apt update
sudo apt install inotify-tools rsync
Confirm both are present:
inotifywait --help | head -n 1
rsync --version | head -n 1
Understand the two moving parts
inotifywait -m runs continuously and prints a line every time a watched event fires. rsync -a --delete SRC/ DEST/ makes the destination match the source. We simply wire the first into the second.
One detail that matters enormously with rsync: the trailing slash on the source. rsync -a /data/src/ /data/dst/ copies the contents of src into dst. Without the trailing slash on the source, it copies the directory src inside dst. Get this wrong and your layout is off by one level. I always use trailing slashes on both.
The script
Save this as /usr/local/bin/dirsync.sh. Edit the two paths at the top — they are placeholders you must replace with your real directories.
#!/usr/bin/env bash
set -euo pipefail
# --- EDIT THESE TWO LINES ---
SRC="/path/to/source" # directory to watch
DEST="/path/to/destination" # directory to keep in sync
# ----------------------------
# rsync options: archive mode, delete extraneous files on DEST,
# and a human-readable summary of what changed.
RSYNC_OPTS=(-a --delete --human-readable)
# Do one full sync at startup so DEST matches SRC before we
# start reacting to events.
rsync "${RSYNC_OPTS[@]}" "$SRC/" "$DEST/"
# Watch SRC recursively. We react to close_write (a file finished
# being written), plus creates, deletes, and moves. Using
# close_write rather than 'modify' avoids syncing half-written files.
inotifywait -m -r -q \
-e close_write -e create -e delete -e move \
--format '%e %w%f' "$SRC" |
while read -r event path; do
# A tiny debounce: a burst of events (e.g. a file copy) collapses
# into one rsync run instead of dozens.
sleep 1
echo "$(date '+%F %T') triggered by: $event $path"
rsync "${RSYNC_OPTS[@]}" "$SRC/" "$DEST/"
done
Make it executable:
sudo chmod 0755 /usr/local/bin/dirsync.sh
Run it by hand first, in a terminal, and watch the output while you touch a file on the source:
/usr/local/bin/dirsync.sh
The sleep 1 is a deliberately simple debounce. It is not a precise rate limiter — under a constant stream of changes rsync will run repeatedly. That is fine and standard for moderate workloads. If you need true coalescing under heavy write load, look at inotifywait --format combined with a timed batch loop, but the version above is the boring, reliable one.
Raise the inotify watch limit if the tree is large
The kernel caps how many files a user can watch (fs.inotify.max_user_watches, commonly 8192 by default). Recursively watching a big tree can exceed that, and inotifywait will warn. Check and, if needed, raise it:
cat /proc/sys/fs/inotify/max_user_watches
echo 'fs.inotify.max_user_watches=524288' | sudo tee /etc/sysctl.d/90-inotify.conf
sudo sysctl --system
Pick a value appropriate to your file count; 524288 is a common, comfortable ceiling.
Run it as a systemd service
So it survives reboots and logouts, install a unit at /etc/systemd/system/dirsync.service. Replace youruser and yourgroup with the least-privileged account that can read the source and write the destination.
[Unit]
Description=Near-real-time directory sync (inotifywait + rsync)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=youruser
Group=yourgroup
ExecStart=/usr/local/bin/dirsync.sh
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Enable and start it:
sudo systemctl daemon-reload
sudo systemctl enable --now dirsync.service
Syncing to a remote host instead
To mirror to another machine, change the destination in the script to an SSH target and let rsync use SSH (its default transport):
DEST="user@remote.example.com:/path/to/destination"
This requires key-based SSH auth already working for that user (an interactive password prompt will break an unattended service). Set up and test the SSH key first, separately, before wiring it in. The trailing-slash rule still applies to the local source.
Verify it works
- Watch the service log as you make a change:
journalctl -u dirsync.service -f
Then, in another terminal, create a file in the source and confirm a sync line appears within a second or two:
echo hello > /path/to/source/testfile.txt
ls -l /path/to/destination/testfile.txt # should now exist
- Confirm the mirror is exact with a dry run that reports differences without changing anything.
-nis--dry-run; if it prints nothing to transfer, the directories match:
rsync -a --delete -n -v /path/to/source/ /path/to/destination/
- Confirm the delete path on your test directories: remove a file from the source and check it disappears from the destination.
Undo and roll back
- Stop the sync:
sudo systemctl disable --now dirsync.service
- Remove it entirely: delete
/etc/systemd/system/dirsync.service, runsudo systemctl daemon-reload, and delete/usr/local/bin/dirsync.sh. - Revert the watch limit: delete
/etc/sysctl.d/90-inotify.confand runsudo sysctl --system.
There is no undo for files this tool already deleted on the destination — that is exactly why you test against throwaway directories first, and why the destination should never be your only copy. For the exact meaning of any flag, see the rsync man page and the inotifywait man page shipped with inotify-tools.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
