How to Automate Linux Backups with rsync and systemd Timers

A backup you have to remember to run is a wish, not a backup. The good news is that the two pieces you need are already installed on most modern Linux systems: rsync, which has been copying files intelligently since the 1990s, and systemd timers, which have quietly replaced cron on nearly every major distribution. Together they give you incremental copies, real logs, and a schedule that survives reboots. No subscription, no daemon to babysit, no third-party tool to learn. Here is how to put them together properly.

Decide What and Where First

Before writing a single command, settle two things: the source and the destination. A backup that lives on the same disk is not a backup. If that drive dies, both copies go with it. Use a second internal disk, an external USB drive, or a NAS mount.

Then make the destination path predictable. USB drives get different device names depending on the order they are plugged in — /dev/sdb today, /dev/sdc tomorrow — so mount by UUID instead. Run blkid or lsblk -f to find the UUID, add an entry using the UUID=... form to /etc/fstab, and mount it somewhere stable like /mnt/backup. From that point on, the path in your scripts never changes.

Build the rsync Command First

Do not start with systemd. Start with a command that works when you type it by hand, because debugging a timer is much harder than debugging rsync.

The flags that matter

  • -a archive mode: recursive copy that preserves permissions, timestamps, symlinks and ownership.
  • -A and -X preserve ACLs and extended attributes. Useful if you rely on filesystem ACLs or SELinux labels.
  • -h prints sizes in human-readable form, which makes logs far easier to scan.
  • --delete removes files at the destination that no longer exist at the source, so the backup mirrors reality instead of accumulating junk.
  • --exclude-from=/etc/backup-excludes.txt keeps caches, browser profiles, build directories and VM images out of the copy.
  • --link-dest=/mnt/backup/2025-01-01 creates hardlinked snapshots, so an unchanged file costs almost no extra space.
  • -n is a dry run. It shows what would happen and touches nothing.

Trailing slashes change everything

A single slash decides the shape of your backup. rsync -a /home/user/ /mnt/backup/ copies the contents of the home directory into the destination. Drop that slash on the source and you get /mnt/backup/user/ instead. Small detail, endless confusion. Pick one convention and stay with it.

rsync -aAXh --delete --exclude-from=/etc/backup-excludes.txt /home/ /mnt/backup/home/

Wrap It in a Script

Put the command in a small shell script so systemd has something clean to run and something meaningful to fail on.

#!/bin/bash
set -euo pipefail

SRC=/home/
DEST=/mnt/backup/home/

if ! mountpoint -q /mnt/backup; then
  echo "$(date -Is) backup drive not mounted, aborting" >&2
  exit 1
fi

rsync -aAXh --delete --exclude-from=/etc/backup-excludes.txt "$SRC" "$DEST"
echo "$(date -Is) backup finished"

Save it as /usr/local/bin/backup-home.sh and make it executable with chmod +x. The set -euo pipefail line makes the script stop at the first error, so systemd records a failed unit instead of a silently half-finished copy. The mount check is the difference between a backup and 400 GB of data written into an empty directory on your root filesystem.

Create the systemd Service

Timers trigger services, so define the service first. Create /etc/systemd/system/backup-home.service:

[Unit]
Description=Incremental home backup with rsync

[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-home.sh
Nice=10
IOSchedulingClass=idle

Type=oneshot is correct for a script that runs and exits. The scheduling hints keep the copy from fighting with whatever you are doing at the time. If you would rather run this as your own user, place the units in ~/.config/systemd/user/ and enable lingering with loginctl enable-linger $USER, otherwise user timers stop when you log out.

Add the Timer

Now create /etc/systemd/system/backup-home.timer. The names must match, because a timer activates the service of the same name by default.

[Unit]
Description=Run home backup nightly

[Timer]
OnCalendar=*-*-* 02:30:00
Persistent=true
RandomizedDelaySec=15m

[Install]
WantedBy=timers.target

OnCalendar accepts systemd's calendar syntax: daily, Mon..Fri 22:00, *-*-* 02:30:00. Test any expression before trusting it with systemd-analyze calendar "Mon..Fri 22:00". Persistent=true runs a missed backup the next time the machine boots, which matters a great deal on laptops that are asleep at 2:30 in the morning. RandomizedDelaySec spreads the start time, useful when several machines write to the same NAS.

Then load and enable it:

  1. sudo systemctl daemon-reload
  2. sudo systemctl enable --now backup-home.timer
  3. systemctl list-timers --all to confirm the next run time

Test It Before You Trust It

Run the service by hand once: sudo systemctl start backup-home.service. Then check what happened with systemctl status backup-home.service and journalctl -u backup-home.service -n 50. If something looks wrong, fix the script and repeat — a failed manual run is a gift compared with one you only notice months later.

The real test is a restore. Copy one directory from the backup back into /tmp and open a file from it. Backups that have never been restored are theoretical. Do this at least twice a year, and write down the two commands it takes, because you will not remember them at 11 p.m. on a bad day.

Keeping It Boring

The best backup setup is the one you forget about. A few habits keep it that way. Watch the exit status rather than the disk: systemd marks failed units clearly, and you can add an OnFailure= unit later if you want an email. Keep your excludes file short and honest — caches and build artifacts waste space, but do not exclude anything you would genuinely miss.

Think about --delete before you rely on it. It keeps the mirror accurate, but if you delete a file by mistake, the next run deletes it from the backup too. Hardlinked snapshots with --link-dest, or a second destination you update weekly, give you a way back. And if the destination drive is external, remember it has to be connected and mounted at 2:30 a.m. — or use a service check like the one above and read the logs when it complains. A backup that fails loudly is worth ten that fail quietly.

Photo: Andrey Matveev / Pexels

You Might Also Like