How to Set Up Automated Backups with rsync and cron/systemd on Fedora

Automate Fedora backups by creating a systemd timer unit that triggers an rsync script on a daily schedule.

You forgot to run the backup again

You copied your project folder to the external drive, forgot to unmount, and the drive ejected halfway through. Now the files are corrupted. Or you scheduled a backup script in your crontab, but the system clock drifted and the job never fired. You only notice three weeks later when you need a file that was deleted in a cleanup script. Manual backups drift. Cron is legacy. Fedora uses systemd timers for a reason.

A botched backup configuration can silently overwrite your data or fill your disk with nested directory copies. Run the script manually with a dry run first. Verify the output before you trust the automation.

What's actually happening

rsync moves files efficiently. It calculates checksums and only transfers differences. It can mirror a directory tree while preserving permissions, timestamps, and symlinks. The tool is powerful, but it requires precise paths and flags. A small mistake in the command line can destroy your backup target.

systemd manages processes and scheduling. A service unit describes what to run. A timer unit describes when to run it. The timer triggers the service. This decouples scheduling from execution. It gives you logging, dependency management, and the ability to catch up on missed runs.

Cron runs a command at a time. It does not care if the disk is mounted. It does not care if the network is up. It just runs. If the disk is not there, the command fails, and the output goes to a mail queue you never check. Systemd timers are smarter. They trigger a service unit. The service unit can declare dependencies. It waits for the disk. It waits for the network. It logs to the journal. It catches up if the machine was asleep.

Config files in /etc/ are user-modified. Files in /usr/lib/ ship with packages. Put your custom script in /usr/local/bin/. This keeps your additions separate from package manager updates. The package manager will not overwrite /usr/local/bin/ during upgrades.

The backup script

Create the backup script that performs the actual file transfer. The script lives in /usr/local/bin/ so it is in the system path and executable by systemd.

#!/usr/bin/bash
# Exit immediately if any command fails
set -euo pipefail

# --delete removes files from destination that no longer exist in source
# -a preserves permissions, timestamps, and symlinks
# -v prints verbose output for the journal
# Trailing slash on source matters: /src/ copies contents, /src copies the directory itself
rsync -av --delete /home/user/documents/ /mnt/backup/documents/

# Log completion with a timestamp for easy grep
echo "Backup completed at $(date)"

The trailing slash on the source path changes behavior completely. /src/ copies the contents of the source directory. /src copies the directory itself. If your backup target already has a documents folder, /src creates /backup/documents/documents. Use the slash to mirror contents.

Test the script manually before automating it. A silent failure in a timer is worse than a loud error in your terminal.

The service unit

Define the service unit so systemd knows how to execute the script. The service unit declares dependencies and execution context.

[Unit]
Description=Backup local documents to external drive
# Requires the mount point to be available before running
Requires=mnt-backup.mount
After=mnt-backup.mount

[Service]
Type=oneshot
# Run as the current user to avoid permission headaches
User=%i
# Capture stdout and stderr in the journal
StandardOutput=journal
StandardError=journal
ExecStart=/usr/local/bin/backup.sh

Type=oneshot tells systemd the service runs a command and exits. The service is considered active only while the command runs. This is correct for backups. Requires and After ensure the backup drive is mounted before rsync starts. If the drive is not mounted, systemd waits or fails gracefully instead of running rsync against an empty directory.

Always declare dependencies. If the backup drive is not mounted, rsync will fail silently or overwrite your root filesystem.

The timer unit

Create the timer unit to schedule the service execution. The timer unit controls the schedule and catch-up behavior.

[Unit]
Description=Run backup timer daily

[Timer]
# Run at 2 AM every day
OnCalendar=*-*-* 02:00:00
# Run the service immediately if the system was off when the timer fired
Persistent=true
# Add random delay to avoid thundering herd on shared storage
RandomizedDelaySec=300
# Allow up to 1 minute drift to batch wakeups
AccuracySec=1min

[Install]
WantedBy=timers.target

Persistent=true is the safety net. If the computer is asleep or off at 2 AM, the timer records the missed trigger. When the system boots, systemd checks the journal for missed timers and runs the service immediately. RandomizedDelaySec adds a random delay up to the specified seconds. This prevents multiple machines from hammering a shared NAS at the exact same second.

Enable the timer, not the service. The timer is the entry point.

Enable and verify

Reload the daemon and enable the timer to start the schedule. Systemd caches unit files. You must reload the daemon after creating or editing unit files.

# Reload systemd to recognize new unit files
systemctl daemon-reload
# Enable timer to start on boot and start it now
systemctl enable --now backup.timer
# Verify the timer is active and shows next run time
systemctl status backup.timer

systemctl status shows recent log lines and state in one view. Always check status before restarting or debugging. The output includes the next trigger time. If it says n/a, your calendar syntax is wrong.

Check the next trigger time. If it says n/a, your calendar syntax is wrong.

Verify it worked

Confirm the backup ran and check the logs for errors. Do not assume the timer fired correctly. Inspect the journal.

# Trigger the service manually to test immediately
systemctl start backup.service
# Check the journal for the service output
journalctl -u backup.service --since "5 minutes ago"
# List recent timer triggers
systemctl list-timers --all | grep backup

journalctl -u backup.service filters logs to the specific unit. The --since flag limits output to recent events. systemctl list-timers shows all active timers and their next trigger times. The --all flag includes inactive timers.

Run journalctl first. Read the actual error before guessing.

Common pitfalls

The trailing slash trap is the most common backup disaster. rsync -av /src /dest creates /dest/src. rsync -av /src/ /dest copies contents to /dest. If you run the first command twice, you get /dest/src/src. Your backup grows exponentially. Always use the trailing slash on the source when mirroring contents.

SELinux denials can block rsync even if file permissions look correct. Fedora enforces SELinux by default. If rsync fails with permission denied, check journalctl -t setroubleshoot. The one-line summary tells you the fix. Usually restorecon -Rv /path or chcon. Never disable SELinux to fix a backup script. Fix the context.

Mount points can disappear. If the external drive is USB, the device name might change from sdb1 to sdc1. Use UUIDs in /etc/fstab for reliable mounts. Systemd mount units handle this automatically. The service unit dependency Requires=mnt-backup.mount works as long as the mount point path is stable.

OnCalendar syntax is flexible but strict. daily is a shortcut for *-*-* 00:00:00. weekly is Mon *-*-* 00:00:00. If you need a custom schedule, use the full syntax. Run systemd-analyze calendar "*-*-* 02:00:00" to validate the expression and see the next trigger time.

Check the journal before blaming SELinux. The error message usually tells you exactly which file is blocked.

When to use this vs alternatives

Use rsync with systemd timers when you need a simple, efficient mirror of files and want native Fedora integration. Use borg or restic when you require deduplication, encryption, and versioned snapshots to protect against ransomware. Use timeshift when you want system-level snapshots to roll back configuration changes on a desktop. Use cron only when you are maintaining legacy scripts that cannot run under systemd user sessions. Stay on manual rsync if the backup runs less than once a month and automation adds unnecessary complexity.

Choose the tool that matches your recovery goal, not the one that looks fanciest.

Where to go next