Scheduled backups with cron
A backup you have to start by hand eventually gets forgotten — cron does it without you, running your rsync or restic script on a schedule, every night, with no one at the keyboard. This article covers putting that script on a schedule with crontab, keeping its logs, and not getting tripped up by timezones.
What you'll need
- An mHost Linux VPS — any of the supported distros works: Ubuntu 22.04/24.04, Debian 11/12, AlmaLinux/Rocky Linux 8/9.
- SSH access as
rootor a user withsudo. - A working backup script or command built on rsync or restic that you've already tested by hand. If you don't have one yet, set up the tool itself first, following "Backups with rsync" or "Backups with restic" — this article is only about scheduling and unattended runs, not about rsync/restic itself.
- A destination for the backups: a separate disk/mounted volume on this same server, or a remote host — whichever you chose in the articles above.
Step 1. Prepare the backup script
cron can run anything — a command, a script, a whole program — but a long command is easier to manage as its own executable file rather than written straight into the crontab. That makes it simpler to edit, test by hand, and reuse.
Below are two minimal examples. Pick whichever matches your tool (see the linked articles above for the full picture) and save it as /usr/local/bin/backup.sh.
An rsync-based script
sudo nano /usr/local/bin/backup.sh#!/bin/sh
set -e
SRC="/var/www"
DEST="/mnt/backup/www"
rsync -a --delete "$SRC" "$DEST"-a (--archive) recursively copies the directory while preserving permissions, modification times, symlinks, and ownership; --delete removes files from DEST that no longer exist in SRC, so the copy stays a mirror instead of only growing. DEST can also be a remote host over SSH (user@host:/path, with the -e ssh flag) — for that and the rest of rsync's flags, see "Backups with rsync".
A restic-based script
The restic repository is created once, by hand, ahead of time. First, set up the file with the repository password:
sudo mkdir -p /etc/restic
sudo nano /etc/restic/password
sudo chmod 600 /etc/restic/passwordPut one long password in the /etc/restic/password file (no trailing newline) and save it — this is the repository's encryption password, not an OS user password. Then initialize the repository:
sudo restic --repo /mnt/backup/restic-repo --password-file /etc/restic/password initThe same repository path and the same password file are also the first lines of the script itself — as the RESTIC_REPOSITORY and RESTIC_PASSWORD_FILE environment variables. That way restic won't ask for them interactively when it's launched from cron, where there's simply no one around to answer:
sudo nano /usr/local/bin/backup.sh#!/bin/sh
set -e
export RESTIC_REPOSITORY="/mnt/backup/restic-repo"
export RESTIC_PASSWORD_FILE="/etc/restic/password"
restic backup /var/www
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prunerestic backup creates a new snapshot; restic forget with the --keep-daily/--keep-weekly/--keep-monthly flags removes older snapshots according to the retention policy, and --prune immediately frees the space held by data that's no longer referenced. /mnt/backup/restic-repo doesn't have to be a local path — it can also be an SFTP or S3-compatible store; for choosing a backend, restoring from a copy, and fine-tuning retention, see "Backups with restic".
Make the script executable and run it by hand once — before you trust it to cron, make sure it actually works:
sudo chmod +x /usr/local/bin/backup.sh
sudo /usr/local/bin/backup.shStep 2. Open crontab
Every user has their own separate crontab. The crontab -e command opens the current user's crontab; to edit root's crontab (usually what you need for a system-wide backup script covering something like /var/www), run:
sudo crontab -eA couple of related commands:
crontab -l— show the current crontab without editing anything.crontab -r— delete the user's entire crontab at once, no confirmation.
Before adding the job, make sure cron itself is installed and running:
# Ubuntu / Debian
systemctl status cron
# AlmaLinux / Rocky Linux
systemctl status crondIf the service isn't found, install the package and enable it:
# Ubuntu / Debian
sudo apt install -y cron
sudo systemctl enable --now cron
# AlmaLinux / Rocky Linux
sudo dnf install -y cronie
sudo systemctl enable --now crondStep 3. Cron schedule syntax
Each crontab line describes a schedule with five fields, followed by the command itself. Field order: minute · hour · day of month · month · day of week.
- minute — 0–59
- hour — 0–23
- day of month — 1–31
- month — 1–12
- day of week — 0–7 (0 and 7 are Sunday)
*— "any value," the whole field.,— a list of values:1,15means the 1st and the 15th.-— a range:8-11in the hour field means 8 through 11 inclusive./— a step:*/2in the hour field means every other hour (0, 2, 4, …).
Examples:
0 3 * * *— Every night at 03:0030 2 * * 0— Every Sunday at 02:300 */6 * * *— Every 6 hours (00:00, 06:00, 12:00, 18:00)0 3 1 * *— On the 1st of every month at 03:00
Instead of five fields, you can use short "nicknames": @daily (equivalent to 0 0 * * *), @weekly (0 0 * * 0), @monthly, @hourly, @reboot (once, at system startup), and others. For a nightly backup, explicit fields are more convenient — they give you exact control over the hour: @daily fires at midnight, not, say, 03:00, when disk load is usually lower.
Step 4. Add the nightly job and redirect the logs
Open root's crontab and add the line:
sudo crontab -e0 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&10 3 * * *— every night at 03:00 (which timezone that's measured in is covered in step 5 below).>> /var/log/backup.log— append the script's standard output to the end of the file (>instead of>>would overwrite the log from scratch every time).2>&1— send the error stream (stderr) there too; order matters:>>first redirectsstdoutto the file, and only then does2>&1say "stream 2 goes wherever stream 1 currently points."
0 3 * * * /usr/bin/flock -n /var/run/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1The -n flag means "don't wait, just exit right away if the previous run hasn't finished yet."
The /var/log/backup.log file will grow forever if you don't rotate it — set up a dedicated logrotate rule for it if needed (a separate topic, not covered here).
Save the file and check that the job was added:
sudo crontab -lStep 5. Timezone
cron goes by the server's system date and time — not the timezone you had in mind while writing 0 3 * * *. Check the server's current timezone:
timedatectlChange it if needed (for example, to Moscow time):
sudo timedatectl set-timezone Europe/MoscowThe list of all available zones is timedatectl list-timezones.
Step 6. Verify the backup actually ran
Let the job fire at least once (you don't have to wait for nighttime — you can temporarily set the time a couple of minutes ahead, check the result, then set 0 3 * * * back) and check the script's log:
tail -n 50 /var/log/backup.logWhile you're at it, check that cron itself saw and ran the job — that's a separate record from your script's own output:
# Ubuntu / Debian
journalctl -u cron --since today
grep CRON /var/log/syslog
# AlmaLinux / Rocky Linux
journalctl -u crond --since today
sudo tail -n 50 /var/log/cronFinally, make sure fresh data actually showed up at the destination — a new file/directory dated today for rsync, or a new snapshot for restic (restic snapshots).
What's next
For the details of the tool itself — installation, choosing a destination, restoring from a copy, fine-tuning retention — see "Backups with rsync" and "Backups with restic".