Keeping files synchronized across multiple servers is one of those tasks that sounds simple until you need it to happen automatically, in real time, without babysitting a cron job. Rsync handles the heavy lifting of efficient file transfer, and lsyncd watches for filesystem changes and triggers rsync the moment something changes. Together, they give you a lightweight, real-time file mirroring setup that works over SSH with minimal overhead.
This guide walks through setting up rsync for both local and remote file synchronization, then layering lsyncd on top for automatic, event-driven mirroring between two Linux servers. Everything is tested on Rocky Linux 10 and Ubuntu 24.04, with rsync’s core options covered alongside the full lsyncd configuration and systemd integration. If you need something more robust for backups specifically, consider pairing this with rsync backup schedules using systemd timers.
Tested March 2026 | Rocky Linux 10.1 (rsync 3.4.1, lsyncd 2.3.1) and Ubuntu 24.04.4 LTS (rsync 3.2.7, lsyncd 2.2.3). SELinux enforcing on Rocky.
Prerequisites
- Two Linux servers (source and target) with network connectivity between them
- Root or sudo access on both servers
- Tested on: Rocky Linux 10.1, Ubuntu 24.04.4 LTS
- SSH key-based authentication between the servers (covered below)
The examples use 10.0.1.50 as the source server and 10.0.1.51 as the target. Replace these with your actual server IPs.
Set Up SSH Key Authentication
Before any remote sync works, you need passwordless SSH from the source to the target. On the source server, generate an Ed25519 key pair:
ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -N ""
Copy the public key to the target server:
ssh-copy-id [email protected]
Verify the connection works without a password prompt:
ssh [email protected] hostname
The expected output is the target server’s hostname with no password prompt. If you need a deeper walkthrough on SSH keys, see using SSH and SSH keys on Debian or Linux Mint.
Install Rsync
Rsync comes pre-installed on most Linux distributions. Verify it’s available:
rsync --version | head -1
On Rocky Linux 10.1:
rsync version 3.4.1 protocol version 32
On Ubuntu 24.04:
rsync version 3.2.7 protocol version 31
If it’s missing, install it:
sudo dnf install -y rsync # Rocky / AlmaLinux / RHEL
sudo apt install -y rsync # Ubuntu / Debian
Rsync Fundamentals
Rsync uses a delta-transfer algorithm that only sends the differences between source and destination files, making it far more efficient than scp or plain cp for repeated transfers. The flags you’ll use most often:
| Flag | Purpose |
|---|---|
-a | Archive mode (preserves permissions, ownership, timestamps, symlinks) |
-v | Verbose output |
-h | Human-readable file sizes |
-z | Compress data during transfer |
--delete | Remove files from destination that no longer exist in source (mirror mode) |
--dry-run | Show what would happen without making changes |
--exclude | Skip files matching a pattern |
--progress | Show per-file transfer progress |
--bwlimit | Limit bandwidth in KB/s |
The Trailing Slash Gotcha
This catches people off guard constantly. A trailing slash on the source path changes rsync’s behavior:
rsync -avh /opt/source/ /opt/backup/
With the trailing slash, rsync copies the contents of /opt/source/ directly into /opt/backup/:
/opt/backup/file1.txt
/opt/backup/file2.txt
Without the trailing slash:
rsync -avh /opt/source /opt/backup/
Rsync copies the directory itself into the destination:
/opt/backup/source/file1.txt
/opt/backup/source/file2.txt
Most of the time you want the trailing slash. When in doubt, use --dry-run first.
Local Sync Example
Create some test data and sync it to a backup directory:
mkdir -p /opt/source /opt/backup
for i in $(seq 1 5); do dd if=/dev/urandom of=/opt/source/file-$i.dat bs=1M count=1 2>/dev/null; done
rsync -avh /opt/source/ /opt/backup/
The output shows each file transferred:
sending incremental file list
file-1.dat
file-2.dat
file-3.dat
file-4.dat
file-5.dat
sent 5.24M bytes received 111 bytes 10.49M bytes/sec
total size is 5.24M speedup is 1.00
Mirror Mode with –delete
The --delete flag makes the destination an exact mirror of the source. If you remove a file from the source, it gets removed from the destination too:
rm /opt/source/file-3.dat
rsync -avh --delete /opt/source/ /opt/backup/
Rsync detects the deletion and removes it from the backup:
sending incremental file list
deleting file-3.dat
sent 129 bytes received 26 bytes 310.00 bytes/sec
total size is 4.19M speedup is 27,060.03
Use --delete with caution. A --dry-run first can save you from accidental data loss.
Rsync Over SSH
This is where rsync becomes genuinely useful. Syncing files between servers over an encrypted SSH connection, with only the changed bytes going over the wire.
Push Files to a Remote Server
Send files from the source server to the target:
rsync -avhz -e ssh /opt/source/ [email protected]:/opt/remote-backup/
The -z flag compresses data during transfer (useful over WAN links) and -e ssh explicitly specifies SSH as the transport:
sending incremental file list
created directory /opt/remote-backup
./
file-1.dat
file-2.dat
file-4.dat
file-5.dat
sent 4.20M bytes received 136 bytes 8.39M bytes/sec
total size is 4.19M speedup is 1.00
Pull Files from a Remote Server
Pull works in the opposite direction. Fetch files from the remote server to a local directory:
rsync -avhz -e ssh [email protected]:/opt/remote-backup/ /opt/pulled-data/
Transfer Progress and Bandwidth Limiting
For large transfers, --progress shows per-file progress. Pair it with --bwlimit to avoid saturating a production link:
rsync -avhz --progress --bwlimit=5000 -e ssh /opt/source/ [email protected]:/opt/remote-backup/
The --bwlimit=5000 caps the transfer at 5000 KB/s (roughly 5 MB/s). Adjust based on your available bandwidth.
sending incremental file list
bigfile.dat
32.77K 0% 0.00kB/s 0:00:00
10.49M 100% 117.28MB/s 0:00:00 (xfr#1, to-chk=0/1)
sent 10.49M bytes received 35 bytes 20.98M bytes/sec
total size is 10.49M speedup is 1.00
Exclude Patterns
Skip files you don’t want synced, like log files or temporary caches:
rsync -avhz --exclude='*.log' --exclude='.cache' -e ssh /opt/source/ [email protected]:/opt/remote-backup/
Custom SSH Port
If SSH runs on a non-standard port, pass it through the -e flag:
rsync -avhz -e "ssh -p 2222" /opt/source/ [email protected]:/opt/remote-backup/
Install Lsyncd
Lsyncd (Live Syncing Daemon) monitors directories using Linux’s inotify interface and triggers rsync whenever files change. Instead of running rsync on a schedule (like systemd timers), lsyncd reacts to actual filesystem events, syncing changes within seconds.
Ubuntu / Debian
Lsyncd is in the default repositories:
sudo apt update
sudo apt install -y lsyncd
Verify the installed version:
lsyncd --version
Ubuntu 24.04 ships version 2.2.3:
Version: 2.2.3
Rocky Linux / AlmaLinux / RHEL
Lsyncd is not currently available in EPEL 10 or the base Rocky Linux 10 repositories, so you’ll need to build it from the official GitHub repository. Install the build dependencies first:
sudo dnf install -y epel-release
sudo dnf install -y cmake gcc gcc-c++ compat-lua compat-lua-devel make git
Clone the repository and check out the latest release:
cd /tmp
git clone https://github.com/lsyncd/lsyncd.git
cd lsyncd
git checkout v2.3.1
Configure the build with the correct Lua paths (Rocky 10 uses compat-lua which installs to /usr/include/lua-5.1):
cmake . -DCMAKE_INSTALL_PREFIX=/usr \
-DLUA_INCLUDE_DIR=/usr/include/lua-5.1 \
-DLUA_LIBRARIES=/usr/lib64/liblua-5.1.so \
-DLUA_COMPILER=/usr/bin/luac-5.1 \
-DLUA_EXECUTABLE=/usr/bin/lua-5.1
Compile and install:
make -j$(nproc)
sudo make install
Confirm the installation:
lsyncd --version
You should see version 2.3.1:
Version: 2.3.1
Configure Lsyncd for Local Directory Sync
Start with a simple local sync to understand lsyncd’s configuration before moving to remote sync. Create the required directories:
sudo mkdir -p /etc/lsyncd /var/log/lsyncd /opt/source /opt/local-mirror
Create a few test files in the source directory:
sudo touch /opt/source/file{1..5}.txt
Open the lsyncd configuration file:
sudo vi /etc/lsyncd/lsyncd.conf.lua
Add the following Lua configuration:
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
statusInterval = 10,
nodaemon = false,
}
sync {
default.rsync,
source = "/opt/source/",
target = "/opt/local-mirror/",
delay = 5,
rsync = {
archive = true,
compress = true,
},
}
The settings block controls logging and daemon behavior. The sync block defines what to sync: default.rsync uses rsync for local transfers, delay = 5 batches changes within a 5-second window before triggering a sync (reduces overhead for rapid file changes).
Create a Systemd Service
Ubuntu’s package installs an init script, but a proper systemd unit works better on both distros. Create the service file:
sudo vi /etc/systemd/system/lsyncd.service
Add the following:
[Unit]
Description=Live Syncing Daemon
After=network.target
[Service]
ExecStart=/usr/bin/lsyncd -nodaemon /etc/lsyncd/lsyncd.conf.lua
Restart=on-failure
[Install]
WantedBy=multi-user.target
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now lsyncd
Check the status:
sudo systemctl status lsyncd
You should see lsyncd active and the initial sync completed:
● lsyncd.service - Live Syncing Daemon
Loaded: loaded (/etc/systemd/system/lsyncd.service; enabled)
Active: active (running)
Main PID: 6790 (lsyncd)
Mar 30 18:35:08 source-srv lsyncd[6790]: 18:35:08 Normal: --- Startup ---
Mar 30 18:35:08 source-srv lsyncd[6790]: 18:35:08 Normal: recursive full rsync: /opt/source/ -> /opt/local-mirror/
Mar 30 18:35:08 source-srv lsyncd[6790]: 18:35:08 Normal: Startup of /opt/source/ -> /opt/local-mirror/ finished.
Test the live sync by creating a new file:
sudo touch /opt/source/livetest.txt
sleep 8
ls /opt/local-mirror/
The new file appears in the mirror within seconds:
file1.txt file2.txt file3.txt file4.txt file5.txt livetest.txt
The lsyncd log confirms the sync:
sudo tail -3 /var/log/lsyncd/lsyncd.log
Output showing the detected change and rsync call:
Mon Mar 30 18:35:35 2026 Normal: Calling rsync with filter-list of new/modified files/dirs
/livetest.txt
/
Mon Mar 30 18:35:35 2026 Normal: Finished a list after exitcode: 0
Configure Lsyncd for Remote Sync Over SSH
This is the real payoff. Lsyncd can push changes to a remote server in real time over SSH, which is exactly what you want for multi-server deployments, web content mirroring, or keeping a warm standby in sync.
On the target server, create the destination directory:
sudo mkdir -p /opt/remote-mirror
sudo chown user:user /opt/remote-mirror
On the source server, update the lsyncd configuration. The key change is switching from default.rsync to default.rsyncssh and adding the host and targetdir parameters:
sudo vi /etc/lsyncd/lsyncd.conf.lua
Replace the contents with:
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
statusInterval = 10,
nodaemon = false,
}
sync {
default.rsyncssh,
source = "/opt/source/",
host = "[email protected]",
targetdir = "/opt/remote-mirror/",
delay = 5,
rsync = {
archive = true,
compress = true,
},
ssh = {
port = 22,
},
}
The host field takes user@hostname format. If lsyncd runs as root (via systemd), make sure root’s SSH key is authorized on the target server. Restart the service:
sudo systemctl restart lsyncd
sudo systemctl status lsyncd
The startup log confirms the remote sync target:
● lsyncd.service - Live Syncing Daemon
Active: active (running)
Mar 30 18:37:18 source-srv lsyncd[7350]: 18:37:18 Normal: --- Startup ---
Mar 30 18:37:18 source-srv lsyncd[7350]: 18:37:18 Normal: recursive full rsync: /opt/source/ -> [email protected]:/opt/remote-mirror/
Mar 30 18:37:18 source-srv lsyncd[7350]: 18:37:18 Normal: Startup of "/opt/source/" finished: 0
Test it by creating a file on the source and checking the target:
sudo touch /opt/source/remote-test.txt
sleep 8
ssh [email protected] "ls /opt/remote-mirror/"
The file appears on the remote server:
file1.txt file2.txt file3.txt file4.txt file5.txt livetest.txt remote-test.txt
Syncing Multiple Directories
Add multiple sync blocks to mirror different directory trees to the same or different targets:
settings {
logfile = "/var/log/lsyncd/lsyncd.log",
statusFile = "/var/log/lsyncd/lsyncd.status",
statusInterval = 10,
nodaemon = false,
}
sync {
default.rsyncssh,
source = "/var/www/html/",
host = "[email protected]",
targetdir = "/var/www/html/",
delay = 5,
rsync = {
archive = true,
compress = true,
},
ssh = {
port = 22,
},
}
sync {
default.rsyncssh,
source = "/opt/app/config/",
host = "[email protected]",
targetdir = "/opt/app/config/",
delay = 5,
rsync = {
archive = true,
compress = true,
},
ssh = {
port = 22,
},
}
Exclude Patterns in Lsyncd
Skip temporary files, caches, or version control directories from syncing:
sync {
default.rsyncssh,
source = "/opt/source/",
host = "[email protected]",
targetdir = "/opt/remote-mirror/",
delay = 5,
exclude = {
"*.tmp",
"*.log",
".git",
".cache",
},
rsync = {
archive = true,
compress = true,
},
ssh = {
port = 22,
},
}
Monitor and Troubleshoot Lsyncd
Lsyncd writes both a log file and a status file. The status file is particularly useful because it shows the current state of all sync targets and how many changes are pending.
cat /var/log/lsyncd/lsyncd.status
A healthy status file looks like this:
Lsyncd status report at Mon Mar 30 18:35:18 2026
Sync1 source=/opt/source/
There are 0 delays
Filtering:
nothing.
Inotify watching 1 directories
1: /opt/source/
“0 delays” means everything is synced. If you see pending delays, lsyncd is waiting for the delay timer before triggering the next rsync batch.
Watch the log in real time while making changes to see sync events as they happen:
sudo tail -f /var/log/lsyncd/lsyncd.log
Error: “Host key verification failed”
This is the most common issue when setting up remote sync. Lsyncd runs as root via systemd, so root’s ~/.ssh/known_hosts must contain the target server’s host key. Fix it by running once as root:
sudo ssh-keyscan -H 10.0.1.51 | sudo tee -a /root/.ssh/known_hosts
Or SSH to the target once as root to accept the key interactively:
sudo ssh [email protected] hostname
Error: “Permission denied” on Target Directory
If lsyncd’s startup rsync fails with “mkdir failed: Permission denied”, the target directory either doesn’t exist or the SSH user doesn’t have write access. Create it on the target and set ownership:
sudo mkdir -p /opt/remote-mirror
sudo chown user:user /opt/remote-mirror
Production Hardening
Increase Inotify Watch Limits
Each directory that lsyncd monitors consumes an inotify watch. The default limit is typically around 8192 to 65536 depending on the distribution. For large directory trees, you’ll hit this limit and lsyncd will silently stop watching new subdirectories. Rocky Linux 10 defaults to 13,389 and Ubuntu 24.04 to 15,052.
Check the current limit:
cat /proc/sys/fs/inotify/max_user_watches
Increase it permanently:
echo "fs.inotify.max_user_watches = 524288" | sudo tee /etc/sysctl.d/99-lsyncd.conf
sudo sysctl -p /etc/sysctl.d/99-lsyncd.conf
Log Rotation
Lsyncd’s log file grows indefinitely. Set up logrotate to prevent disk space issues:
sudo vi /etc/logrotate.d/lsyncd
Add the following rotation policy:
/var/log/lsyncd/*.log {
weekly
rotate 4
compress
missingok
notifempty
postrotate
systemctl restart lsyncd > /dev/null 2>&1 || true
endscript
}
Tuning the Delay Parameter
The delay setting in the sync block controls how long lsyncd waits to batch changes before triggering rsync. A lower value (1 to 3 seconds) means faster sync but more rsync processes spawned. A higher value (15 to 30 seconds) batches more changes per sync call, reducing overhead on high-churn directories.
For web content mirroring where near-instant sync matters, delay = 1 works well. For general file sync where a few seconds of lag is acceptable, delay = 10 is a good default. If you’re syncing a directory where hundreds of files change simultaneously (build artifacts, log rotation), bump it to delay = 30 to let lsyncd batch everything into a single rsync call.
Bandwidth Control
Prevent lsyncd from saturating your network by adding bandwidth limits to the rsync configuration:
rsync = {
archive = true,
compress = true,
bwlimit = 10000,
},
The bwlimit value is in KB/s. Set it based on your available bandwidth and how much you’re willing to allocate to sync traffic. For a comparison of other file synchronization approaches, see Syncthing on Rocky Linux which takes a peer-to-peer approach instead of the push model that rsync and lsyncd use.
| Item | Rocky Linux 10 | Ubuntu 24.04 |
|---|---|---|
| rsync version | 3.4.1 | 3.2.7 |
| lsyncd version | 2.3.1 (built from source) | 2.2.3 (from repos) |
| lsyncd install method | cmake + make | apt install lsyncd |
| SELinux/AppArmor | SELinux enforcing, no issues | AppArmor, no issues |
| Default inotify watches | 13,389 | 15,052 |
| Firewall | firewalld (port 22 open by default) | ufw (disabled by default) |