rsync and tar backup command builder

Pick a folder and a destination; get the backup script, a dry run, a nightly systemd timer and the restore command.

Last updated

In short: The simplest dependable Linux backup is rsync --archive from the folder to another disk or machine, run nightly by a systemd timer. Snapshots with --link-dest keep a dated copy per night, while unchanged files are hard links and take no extra space. Always preview with rsync --dry-run --itemize-changes first, and test a restore; for encrypted, deduplicated backups with retention, move on to restic or borg.

Your input never leaves your browser. Everything is calculated on your device; nothing you type is sent to a server.

What kind of backup?
Backup folder on another machine? (optional)

Leave empty when the backup folder is on this machine: a second disk, a USB disk or a mounted NAS share.

Options
Run it every night

Step 1: preview with a dry run

Shows what the next snapshot would copy compared with the latest one, without changing anything. Before the first snapshot everything is new, so everything is listed.

sudo rsync --dry-run --itemize-changes --archive --hard-links --one-file-system \
  --exclude='*.tmp' --exclude=.cache/ "/srv/data/" "/mnt/backup/data/latest/"
--dry-run
Goes through all the steps but changes nothing (short form -n).
--itemize-changes
Prints one line per file with a change code: >f+++++++++ is a new file, >f.st...... a changed file (size and time), *deleting a file that --delete would remove.

Step 2: the backup script

Save as /usr/local/bin/backup-data.sh

#!/usr/bin/env bash
# Dated snapshots of /srv/data in /mnt/backup/data, unchanged files hard-linked.
# Runs as root from a system timer.
set -euo pipefail

src="/srv/data/"
dest="/mnt/backup/data"
stamp=$(date +%Y-%m-%d_%H%M%S)

mkdir -p "$dest"
link=()
if [[ -d "$dest/latest" ]]; then
  link=(--link-dest="$dest/latest")
fi
rsync --archive --hard-links --one-file-system --exclude='*.tmp' --exclude=.cache/ \
  "${link[@]}" "$src" "$dest/$stamp/"
ln -sfn "$stamp" "$dest/latest"
set -euo pipefail
Stops the script at the first error (pipefail also catches errors on the left side of a pipe), so the timer reports a failure instead of carrying on.
stamp=$(date +%Y-%m-%d_%H%M%S)
Names each run after the date and time it started, for example 2026-09-26_030000.
--link-dest="$dest/latest"
Compares with the previous snapshot: unchanged files become hard links to it instead of new copies, so every snapshot looks complete but only changed files take new space. Skipped on the very first run, when there is no previous snapshot yet.
--archive
Copies folders recursively and keeps permissions, owners, groups, timestamps and symlinks (short form -a, the same as -rlptgoD).
--hard-links
Keeps hard-linked files as links instead of storing them twice (not part of --archive).
--one-file-system
Stays on the source's own disk: other disks, network shares and virtual filesystems mounted inside the source are skipped.
--exclude='*.tmp'
Skips every file or folder named *.tmp anywhere inside the source (* matches any characters).
--exclude=.cache/
Skips every folder named .cache anywhere inside the source.
"$src"
Ends in /, which means "the contents of /srv/data", so each snapshot folder holds the files directly.
ln -sfn "$stamp" "$dest/latest"
Points the latest link at the new snapshot, only after rsync succeeded. The next run compares against it.

Step 3: the service that runs the script

Save as /etc/systemd/system/backup-data.service

[Unit]
Description=Back up /srv/data to /mnt/backup/data
RequiresMountsFor=/mnt/backup/data

[Service]
Type=oneshot
ExecStart=/bin/bash /usr/local/bin/backup-data.sh
Nice=19
IOSchedulingClass=idle
RequiresMountsFor=/mnt/backup/data
Mounts the filesystem that holds the destination first, such as a USB disk or NAS share from /etc/fstab. If it cannot be mounted, the backup fails instead of filling up the system disk.
Type=oneshot
The service runs the script once and is done; systemd records whether it succeeded.
ExecStart=/bin/bash /usr/local/bin/backup-data.sh
Runs the script with bash. Calling bash explicitly also works when the executable bit is missing.
Nice=19 and IOSchedulingClass=idle
Give the backup the lowest CPU and disk priority, so the machine stays responsive while it runs.

The timer that starts it every night

Save as /etc/systemd/system/backup-data.timer

[Unit]
Description=Run backup-data.service every day at 03:00

[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true

[Install]
WantedBy=timers.target
OnCalendar=*-*-* 03:00:00
Every day at 03:00, in the machine's time zone.
Persistent=true
If the machine was off at that time, the backup runs soon after the next boot instead of being skipped.
WantedBy=timers.target
Makes enable start the timer automatically at every boot.

Step 4: install, test and enable

Run these one by one. In nano, paste with Ctrl+Shift+V, save with Ctrl+O and Enter, and exit with Ctrl+X.

sudo nano /usr/local/bin/backup-data.sh
sudo chmod 700 /usr/local/bin/backup-data.sh
sudo /usr/local/bin/backup-data.sh
sudo nano /etc/systemd/system/backup-data.service
sudo nano /etc/systemd/system/backup-data.timer
sudo systemctl daemon-reload
sudo systemctl enable --now backup-data.timer
systemctl list-timers backup-data.timer
journalctl -u backup-data.service
nano /usr/local/bin/backup-data.sh
Creates the script file; paste the script from step 2.
chmod 700 /usr/local/bin/backup-data.sh
Makes it executable and readable only by root.
sudo /usr/local/bin/backup-data.sh
Runs the first backup by hand, so you see errors straight away. The first run copies everything and can take a long time.
nano /etc/systemd/system/backup-data.service
Creates the two unit files; paste their contents from step 3.
sudo systemctl daemon-reload
Makes systemd read the new files.
sudo systemctl enable --now backup-data.timer
Starts the timer now and at every boot. Enable the .timer, not the .service.
systemctl list-timers backup-data.timer
Shows when the backup runs next and when it last ran.
journalctl -u backup-data.service
Shows the output of past runs; look here when a backup failed.

Restore from a snapshot

The first command lists the snapshots. The next two preview and then copy the latest one back to /srv/data; replace latest with a dated folder name to go further back in time. To restore a single file, copy it straight out of the snapshot folder with cp -a.

ls "/mnt/backup/data"
sudo rsync --dry-run --itemize-changes --archive --hard-links "/mnt/backup/data/latest/" \
  "/srv/data/"
sudo rsync --archive --hard-links "/mnt/backup/data/latest/" "/srv/data/"
source and destination swapped
A restore is the same rsync command in the other direction: from the backup to the original folder.

Want another schedule, such as weekly on Sunday? Build the line with the cron to systemd timer converter and replace the OnCalendar= line in the timer.

Review generated files before you use them. Provided as-is — see Terms.

How does the rsync and tar backup command builder work?

A backup is a second copy of your files in a place that does not fail together with the original: another disk, another machine, ideally another building. A copy in a folder next to the original, or on a second partition of the same disk, is not one. This builder writes a small bash script that makes the copy, and a systemd service plus timer that run it every night, so it keeps happening after you stop thinking about it.

rsync copies a folder to another folder and, on later runs, transfers only what changed. --archive keeps permissions, owners and timestamps; over SSH it reaches another machine directly. With dated snapshots every run writes a new folder such as 2026-09-26_030000, and --link-dest turns every unchanged file into a hard link to the previous snapshot, so you can go back to any night while only paying disk space for what changed (rsync(1) man page, --link-dest). A plain mirror is simpler but keeps no history. tar instead packs the folder into one archive file per run, optionally compressed with gzip or zstd, which suits small folders and destinations that cannot store Linux permissions.

Two habits make a backup trustworthy. First, preview: rsync --dry-run --itemize-changes shows what would be copied or deleted without touching anything. Second, test a restore: a backup you never restored is a guess. The restore commands are the same rsync call in the other direction, or tar --extract for archives.

The timer uses OnCalendar= for the time of day and Persistent=true, so a night the machine was off is caught up at the next boot (systemd.timer(5)). A system timer runs as root and can read every file; a user timer runs as you and needs loginctl enable-linger to run while you are logged out. Everything is generated in your browser; nothing you type is sent anywhere.

Worked examples

Nightly snapshots of /srv/data on a USB disk

Input Dated snapshots, source /srv/data, destination /mnt/backup/data, exclude *.tmp and .cache/, 03:00 gives link=(--link-dest="$dest/latest")

Each night gets its own folder such as /mnt/backup/data/2026-09-26_030000, and the latest link points at the newest one. From the second run on, --link-dest hard-links unchanged files to the previous snapshot, so a night with few changes takes almost no space. The service has RequiresMountsFor=/mnt/backup/data, so it fails instead of writing to the system disk when the USB disk is not mounted.

Mirror a folder to another machine over SSH on port 2222

Input rsync mirror, /srv/nextcloud/data to backup@192.168.1.20:/backups/nextcloud, port 2222, delete removed files, keep ACLs gives rsync --archive --hard-links --acls --xattrs --one-file-system --delete --numeric-ids -e 'ssh -p 2222'

--delete makes the copy an exact mirror, which also means an accidental deletion reaches the backup at the next run, so the tool shows a warning. --numeric-ids keeps owners as numbers, because the other machine has different user names. Root needs an SSH key without a passphrase and must have accepted the host key once, because the timer cannot answer prompts.

Compressed tar archives, 30 days kept

Input tar archive, zstd, /home/alex/Documents to /run/media/alex/USB Disk/backups, keep 30 days, user timer at 21:15 gives find "/run/media/alex/USB Disk/backups" -maxdepth 1 -type f -name 'Documents-*.tar.zst' -mtime +30 -delete

Each run writes a full archive such as Documents-2026-09-26_211500.tar.zst; the find line removes archives older than 30 days so the disk does not fill up. The path with a space is quoted everywhere. As a user timer, the units live in ~/.config/systemd/user and the service starts the script as %h/.local/bin/backup-documents.sh.

Frequently asked questions

What is the difference between rsync and tar for backups?

rsync copies a folder to another folder and on later runs only transfers what changed, so the backup is a normal folder you can browse and copy single files out of. tar packs a folder into one archive file, optionally compressed, and every run writes a complete new archive. Use rsync (preferably with dated --link-dest snapshots) for large folders you back up often; use tar for small folders, for one file to move elsewhere, or when the destination cannot store Linux permissions, because the archive keeps them inside.

Why does the trailing slash matter in rsync?

A trailing slash on the source means 'the contents of this folder'. rsync -a /srv/data/ /mnt/backup/data/ puts the files directly in /mnt/backup/data, while rsync -a /srv/data /mnt/backup/data/ creates /mnt/backup/data/data. The slash on the destination makes no difference. This builder always writes the source with a trailing slash, so the destination folder holds the files directly.

How do rsync --link-dest snapshots save space?

With --link-dest=DIR, rsync compares every file with the same file in DIR, the previous snapshot. If it is unchanged, rsync creates a hard link to it instead of a new copy: a second name for the same data on disk. Every dated snapshot folder looks like a complete backup, but only changed files use new space. Deleting an old snapshot folder frees only the files no other snapshot links to. Hard links only work within one filesystem, so all snapshots must be on the same disk.

Is rsync --delete safe for backups?

It only touches the destination, but it removes the safety net: a file you deleted by accident disappears from the backup at the next run too, and so does a file that ransomware replaced. Run rsync --dry-run --itemize-changes first and look for *deleting lines. If you want to go back in time, use dated snapshots with --link-dest instead of one mirror with --delete; each snapshot is a fresh folder, so --delete is not needed.

Why does my backup timer fail with 'Host key verification failed' or 'Permission denied'?

A systemd timer runs without a terminal, so ssh cannot ask you to accept the host key or type a password or passphrase. A system timer also runs as root, which has its own /root/.ssh folder and cannot use your user's keys. Fix it by creating a key for root without a passphrase (sudo ssh-keygen -t ed25519), adding its public key to the other machine, and connecting once by hand with sudo ssh backup@192.168.1.20 true to accept the host key. Then start the service with sudo systemctl start and check journalctl -u for the result.

Is an rsync copy a real backup, or should I use restic or borg?

An rsync copy on another disk protects against a dead disk, and with snapshots against accidental deletion, which is far better than nothing. It lacks what dedicated backup tools add: encryption (important for a copy at a friend's place or in the cloud), deduplication across files and machines, checksums that detect silent corruption, and retention rules such as 'keep 7 daily, 4 weekly and 12 monthly'. restic and borg do all of that; once your rsync backup runs, they are the next step, together with a copy outside your home (the 3-2-1 rule: 3 copies, on 2 different media, 1 offsite).