--- title: "How to Automate Backups with rsync on Ubuntu 24.04" description: "Set up automatic rsync backups on Ubuntu 24.04: push daily hard-linked snapshots to a remote server over SSH, schedule them with a systemd timer and restore." category: "Backup Recovery" language: en source: https://cubepath.com/docs/backup-recovery/automatic-backup-with-rsync updated: 2026-09-25 --- # How to Automate Backups with rsync on Ubuntu 24.04 rsync copies files efficiently by transferring only what has changed, and it works over SSH out of the box. With the `--link-dest` option it can also create dated snapshots in which unchanged files are hard links to the previous snapshot, so each day looks like a full backup but only uses space for new or changed files. In this tutorial you will push daily snapshots of an Ubuntu 24.04 server to a separate backup server, schedule them with a systemd timer, and restore files from them. ## Prerequisites To follow this guide you need: - A **source server** running Ubuntu 24.04 LTS (for example a CubePath VPS) with a non-root user with `sudo` privileges. This is the server you want to back up. - A **backup server** running Ubuntu 24.04 or Debian 12, ideally in a different data center, with enough disk space for your data plus history, and SSH access with a `sudo` user. - The backup server's IP address, referred to as `your_backup_server_ip`. rsync is installed by default on Ubuntu 24.04. Check it on both servers: ```bash rsync --version | head -n 1 ``` ```output rsync version 3.2.7 protocol version 31 ``` If the command is missing, install it with `sudo apt install rsync`. ## Step 1 - Understanding the rsync basics Before automating anything, it helps to see the two rsync behaviors that cause most mistakes. Create a test directory: ```bash mkdir -p ~/rsync-test/src && touch ~/rsync-test/src/file1 ~/rsync-test/src/file2 ``` A trailing slash on the source copies the **contents** of the directory. Without it, rsync copies the **directory itself**: ```bash rsync -av ~/rsync-test/src/ ~/rsync-test/contents/ rsync -av ~/rsync-test/src ~/rsync-test/dir/ ls ~/rsync-test/contents ~/rsync-test/dir ``` ```output /home/your_user/rsync-test/contents: file1 file2 /home/your_user/rsync-test/dir: src ``` The second behavior is `--dry-run` (`-n`), which shows what would happen without changing anything. Use it every time you try a new command, especially with `--delete`: ```bash rsync -avn --delete ~/rsync-test/src/ ~/rsync-test/contents/ ``` The options used throughout this guide are: - `-a` (archive): recursive, and preserves permissions, timestamps, symlinks, owner and group. - `-H`, `-A`, `-X`: preserve hard links, ACLs and extended attributes. - `-R` (relative): keep the full source path, so `/var/www` is stored as `var/www` instead of `www`. - `--numeric-ids`: store user and group IDs as numbers instead of mapping them by name. Remove the test directory when you are done: ```bash rm -r ~/rsync-test ``` ## Step 2 - Preparing the backup server On the **backup server**, create a dedicated, unprivileged user that will receive the backups. Do not name it `backup`, because Ubuntu already has a system account with that name: ```bash sudo adduser --disabled-password --gecos "" rbackup ``` Create the directory that will hold the snapshots and give it to the new user: ```bash sudo mkdir -p /srv/backups sudo chown rbackup:rbackup /srv/backups sudo chmod 700 /srv/backups ``` Because `rbackup` is not root, it cannot set file ownership directly. rsync solves this with `--fake-super`: the receiving side stores the original owner, group, permissions, ACLs and extended attributes in `user.rsync.*` extended attributes, and puts them back when you restore. ext4 and XFS support user extended attributes by default. ## Step 3 - Creating an SSH key for unattended backups The backup job runs as root on the **source server** (it needs to read files like `/etc/shadow`), so the key belongs to root. Generate a dedicated key without a passphrase so the job can run unattended: ```bash sudo ssh-keygen -t ed25519 -f /root/.ssh/backup_ed25519 -N "" -C "rsync-backup@$(hostname -s)" ``` Print the public key: ```bash sudo cat /root/.ssh/backup_ed25519.pub ``` On the **backup server**, create the `.ssh` directory for `rbackup` and open its `authorized_keys` file: ```bash sudo -u rbackup mkdir -m 700 -p /home/rbackup/.ssh sudo -u rbackup nano /home/rbackup/.ssh/authorized_keys ``` Paste the public key on one line, prefixed with the `restrict` option. `restrict` disables port forwarding, agent forwarding and terminal allocation for this key, which rsync does not need: ```text restrict ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... rsync-backup@web01 ``` Set the permissions SSH expects: ```bash sudo chmod 600 /home/rbackup/.ssh/authorized_keys ``` Back on the **source server**, connect once to accept the host key and confirm the login works: ```bash sudo ssh -i /root/.ssh/backup_ed25519 rbackup@your_backup_server_ip 'echo connected' ``` Type `yes` when asked to confirm the fingerprint. The output should be: ```output connected ``` > Note: This key can still run any command as the unprivileged `rbackup` user. For a stricter setup, rsync ships the `rrsync` helper, which limits a key to rsync inside one directory. It changes how destination paths are written, so set it up once the basic job works. ## Step 4 - Writing the backup script The script creates a new snapshot directory named after the current date and time, uses the previous snapshot as `--link-dest`, and only marks the snapshot as complete once rsync succeeds. That way an interrupted run never becomes the base for the next one. Create an exclude list first, for data that should not be backed up: ```bash sudo mkdir -p /etc/rsync-backup sudo nano /etc/rsync-backup/excludes ``` ```text /home/*/.cache/ /root/.cache/ /var/www/*/cache/ *.swp ``` Now create the script: ```bash sudo nano /usr/local/sbin/rsync-backup ``` ```bash #!/usr/bin/env bash set -euo pipefail REMOTE="rbackup@your_backup_server_ip" SSH_KEY="/root/.ssh/backup_ed25519" REMOTE_DIR="/srv/backups/$(hostname -s)" SOURCES=(/etc /home /root /var/www) EXCLUDES="/etc/rsync-backup/excludes" KEEP_DAYS=14 STAMP="$(date +%F_%H%M)" SSH_CMD="ssh -i ${SSH_KEY} -o BatchMode=yes" # Make sure the host directory exists on the backup server. ${SSH_CMD} "${REMOTE}" "mkdir -p '${REMOTE_DIR}'" # Copy into a temporary directory, hard-linking unchanged files to the last snapshot. rsync -aHAXR --numeric-ids \ --rsync-path="rsync --fake-super" \ --exclude-from="${EXCLUDES}" \ --link-dest="${REMOTE_DIR}/latest" \ -e "${SSH_CMD}" \ "${SOURCES[@]}" "${REMOTE}:${REMOTE_DIR}/${STAMP}.incomplete/" # Mark the snapshot complete, point "latest" at it and remove old snapshots. ${SSH_CMD} "${REMOTE}" "cd '${REMOTE_DIR}' \ && mv '${STAMP}.incomplete' '${STAMP}' \ && touch '${STAMP}' \ && ln -sfn '${STAMP}' latest \ && find . -mindepth 1 -maxdepth 1 -type d -name '20??-??-??_????' -mtime +${KEEP_DAYS} -exec rm -rf -- {} +" echo "Backup ${STAMP} completed" ``` Replace `your_backup_server_ip` and adjust `SOURCES` to the directories you care about. If the server runs a database, dump it to a directory first and add that directory to `SOURCES`; copying live database files does not give a usable backup. Make the script executable and readable only by root: ```bash sudo chmod 700 /usr/local/sbin/rsync-backup ``` Run it by hand the first time: ```bash sudo /usr/local/sbin/rsync-backup ``` On the very first run there is no previous snapshot, so rsync prints a warning that the `--link-dest` directory does not exist. That is expected. The script ends with: ```output Backup 2026-09-25_1012 completed ``` ## Step 5 - Verifying the snapshots Run the script a second time, then list the snapshots on the **backup server**: ```bash sudo ls -l /srv/backups/web01 ``` ```output drwxr-xr-x 5 rbackup rbackup 4096 Sep 25 10:12 2026-09-25_1012 drwxr-xr-x 5 rbackup rbackup 4096 Sep 25 10:15 2026-09-25_1015 lrwxrwxrwx 1 rbackup rbackup 15 Sep 25 10:15 latest -> 2026-09-25_1015 ``` Replace `web01` with the short hostname of your source server. To confirm hard linking works, ask `du` for the size of both snapshots in one call. `du` counts each hard-linked file only once, so the second snapshot should be tiny: ```bash sudo du -sh /srv/backups/web01/2026-09-25_* ``` ```output 1.8G /srv/backups/web01/2026-09-25_1012 2.4M /srv/backups/web01/2026-09-25_1015 ``` You can also check that the original ownership was saved as extended attributes: ```bash sudo getfattr -d /srv/backups/web01/latest/etc/shadow ``` ```output # file: srv/backups/web01/latest/etc/shadow user.rsync.%stat="100640 0,0 0:42" ``` The value records the original mode (`640`) and owner (`root:shadow`, IDs `0:42`). If `getfattr` is missing, install it with `sudo apt install attr`. ## Step 6 - Scheduling the backup with a systemd timer A systemd timer is more reliable than cron for this job: the output goes to the journal, `Persistent=true` runs a missed backup after the server was powered off, and you can check the last result with `systemctl`. On the **source server**, create the service unit: ```bash sudo nano /etc/systemd/system/rsync-backup.service ``` ```ini [Unit] Description=rsync snapshot backup to the backup server Wants=network-online.target After=network-online.target [Service] Type=oneshot ExecStart=/usr/local/sbin/rsync-backup Nice=10 IOSchedulingClass=idle ``` Create the timer unit: ```bash sudo nano /etc/systemd/system/rsync-backup.timer ``` ```ini [Unit] Description=Daily rsync snapshot backup [Timer] OnCalendar=*-*-* 02:30:00 RandomizedDelaySec=15m Persistent=true [Install] WantedBy=timers.target ``` Reload systemd and enable the timer: ```bash sudo systemctl daemon-reload sudo systemctl enable --now rsync-backup.timer ``` Confirm the next run is scheduled: ```bash systemctl list-timers rsync-backup.timer ``` ```output NEXT LEFT LAST PASSED UNIT ACTIVATES Fri 2026-09-26 02:38:12 UTC 16h left - - rsync-backup.timer rsync-backup.service ``` Trigger one run through systemd to be sure it works in that environment, then read the log: ```bash sudo systemctl start rsync-backup.service journalctl -u rsync-backup.service -n 20 --no-pager ``` The last lines should include `Backup ... completed` and `Finished rsync-backup.service`. If the job fails, `systemctl status rsync-backup.service` shows `failed` with the exit code. ## Step 7 - Restoring files Restores are pulls from the backup server, run as root on the source server, with the same `--fake-super` option so ownership and permissions are put back. First restore into a temporary directory and compare, so you do not overwrite anything by accident. This restores `/etc/nginx` from the latest snapshot: ```bash sudo rsync -aHAX --numeric-ids \ --rsync-path="rsync --fake-super" \ -e "ssh -i /root/.ssh/backup_ed25519" \ rbackup@your_backup_server_ip:/srv/backups/web01/latest/etc/nginx/ /tmp/restore-nginx/ ``` Check that the files match the live copy and that ownership is correct: ```bash sudo diff -r /etc/nginx /tmp/restore-nginx && echo "identical" sudo ls -l /tmp/restore-nginx/nginx.conf ``` ```output identical -rw-r--r-- 1 root root 1447 Apr 8 2024 /tmp/restore-nginx/nginx.conf ``` To recover an older version, replace `latest` with a snapshot name such as `2026-09-20_0231`. To restore in place, point the destination at the real directory and run it with `--dry-run` first. ## Troubleshooting - **`Permission denied (publickey)`**: check that `/home/rbackup/.ssh` is `700`, `authorized_keys` is `600`, both are owned by `rbackup`, and the key is on a single line. - **`Host key verification failed` in the journal**: root never accepted the backup server's host key. Run the `ssh` test from Step 3 with `sudo`. - **`rsync: [receiver] ... Operation not supported (95)` on extended attributes**: the backup filesystem does not support user extended attributes. Use ext4 or XFS for `/srv/backups`. - **Every snapshot uses full disk space**: `--link-dest` did not find the previous snapshot. Check that the `latest` symlink exists on the backup server and that the source list and options have not changed between runs. - **Exit code 24 (`some files vanished before they could be transferred`)**: files were deleted during the backup, which is normal for temporary files. Exclude those paths if it happens every night. ## Conclusion You now have daily, browsable snapshots of your server on a second machine, with 14 days of history that costs little more than one full copy, plus a tested restore procedure. rsync does not encrypt data at rest or deduplicate within files, so for backups to untrusted storage consider [BorgBackup](/docs/backup-recovery/backup-with-borgbackup-deduplication) or [restic](/docs/backup-recovery/backup-with-restic). To see how this copy fits a complete plan, read [the 3-2-1 backup rule](/docs/backup-recovery/backup-strategies-3-2-1-and-best-practices).