VPS Snaps

How to schedule backups with cron

To run a backup script every night at 02:15, open your crontab with crontab -e and add 15 2 * * * /usr/local/bin/backup-www.sh >> /var/log/backup-www.log 2>&1. Writing the schedule is the easy part. Cron backups usually fail silently, because of PATH, an unescaped %, output that goes nowhere or two runs overlapping, and this guide fixes each one.

9 min readUpdated Checked against official documentation

Crontab syntax: the five time fields

Each line is five time fields followed by the command. cron checks every minute and runs the lines that match.

crontab
15 2 * * * /usr/local/bin/backup-www.sh
FieldAllowed valuesIn the example
Minute0–5915
Hour0–232
Day of month1–31* (every day)
Month1–12, or jan–dec*
Day of week0–7 (0 and 7 are Sunday), or sun–sat*
  • * matches every value.
  • 1,15 is a list, 1-5 a range, and */15 every 15th value (0, 15, 30, 45).
  • Day and month names work, but Debian and Ubuntu cron does not allow them in ranges or lists: write 1-5, not mon-fri.
  • @daily, @weekly, @monthly and @hourly replace all five fields. @reboot runs once when cron starts.

When both day of month and day of week are set, the job runs when either matches. 30 4 1 * 5 runs on the 1st of every month and on every Friday, not only on Fridays that fall on the 1st.

Common backup schedules

ScheduleWhen it runs
15 2 * * *Every day at 02:15
0 */6 * * *Every 6 hours: 00:00, 06:00, 12:00, 18:00
*/15 * * * *Every 15 minutes
30 3 * * 0Sundays at 03:30
0 4 1 * *The 1st of each month at 04:00
0 1 * * 1-5Monday to Friday at 01:00
@dailyEvery day at 00:00, the same as 0 0 * * *

Choose an odd minute such as 02:17 rather than midnight. @daily and many other jobs start at 00:00, and a backup competes with all of them for disk and network.

Where to put the job

LocationRuns asNotes
crontab -eThe user who owns the crontabNo user field. List it with crontab -l; edit root's with sudo crontab -e.
/etc/cron.d/backup-wwwThe user named on each lineOwned by root, not writable by group or others. Name: letters, digits, - and _ only.
/etc/cron.daily/rootAn executable script, run once a day by run-parts. No dots in the name.

For server backups, a file in /etc/cron.d is easy to manage and to read later. It has one extra field, the user, before the command, and you can set the environment at the top:

/etc/cron.d/backup-www
SHELL=/bin/sh
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# m  h  dom mon dow  user  command
17 2  *   *   *    root  flock -n /run/lock/backup-www.lock /usr/local/bin/backup-www.sh >> /var/log/backup-www.log 2>&1

A file in /etc/cron.d with a dot in its name, such as backup.cron, is silently ignored. Scripts in /etc/cron.daily follow the same rule; run-parts --test /etc/cron.daily lists the ones that will actually run. In our test it skipped backup-www.sh and a script without the execute bit.

Every line must end with a newline, including the last; otherwise cron considers the file (at least partly) broken. cron rereads changed crontabs and /etc/cron.d files every minute, so there is nothing to restart.

crontab -r deletes your whole crontab without asking, and r sits next to e on the keyboard. Keep a copy with crontab -l > ~/crontab.bak, or use crontab -i -r, which asks first.

The environment: PATH, SHELL and the % sign

cron does not start a login shell. On Debian and Ubuntu each job runs under sh, gets HOME and LOGNAME from /etc/passwd, and PATH=/usr/bin:/bin. Nothing from your .bashrc or .profile is loaded, so a script that calls a program in /usr/sbin or /usr/local/bin works in your terminal and fails under cron.

Reproduce cron's environment to test a script before you schedule it:

Terminal
sudo env -i HOME=/root LOGNAME=root SHELL=/bin/sh PATH=/usr/bin:/bin /bin/sh -c '/usr/local/bin/backup-www.sh'

In our test, a script calling logrotate (which lives in /usr/sbin) ran fine from a normal shell, and in this environment printed logrotate: not found and exited with status 127. Fix it by setting PATH at the top of the crontab, as in the /etc/cron.d example, or by using full paths in the script.

  • Variables in a crontab are not expanded. PATH=$HOME/bin:$PATH sets PATH to that literal text.
  • % in a command becomes a newline, and everything after it is sent to the command as input. Escape it as \%, as in 0 3 * * * tar -czf /backups/etc-$(date +\%F).tar.gz -C / etc, or keep commands in a script, where % needs no escaping.
  • Comments must be on their own line. Text after a command, even after #, is passed to the shell as part of the command.
  • Jobs run under sh, not bash. Start a script with #!/bin/bash if it needs bash features such as set -o pipefail.

Log every run

Without a redirect, cron emails a job's output to the crontab's owner. If the server has no mail transfer agent, cron logs No MTA installed, discarding output and your error messages are gone. Send output to a file instead:

crontab
17 2 * * * /usr/local/bin/backup-www.sh >> /var/log/backup-www.log 2>&1

>> appends standard output to the log, and 2>&1 then sends errors to the same place. The order matters: we ran a script that writes one line to each stream, and with 2>&1 >> file the error line missed the log and went to the original output, which under cron means mail.

Timestamp each run inside the script. This one also checks its own archive, and only prunes old archives after a good run:

/usr/local/bin/backup-www.sh
#!/bin/sh
set -eu

BACKUP_DIR=/backups/www
STAMP=$(date +%F)
ARCHIVE="$BACKUP_DIR/www-$STAMP.tar.gz"

mkdir -p "$BACKUP_DIR"
echo "$(date -Is) start"
tar -czf "$ARCHIVE" -C /var www
tar -tzf "$ARCHIVE" > /dev/null
find "$BACKUP_DIR" -name 'www-*.tar.gz' -mtime +14 -delete
echo "$(date -Is) done: $ARCHIVE"

set -eu stops the script at the first failing command or unset variable, so a failed tar never reaches the cleanup line. Each run adds a start and a done line stamped like 2026-10-03T16:31:15+00:00. The tar steps are explained in the tar backup guide and the cleanup in the find guide.

cron itself logs each job it starts to syslog. On Ubuntu, read it with journalctl -u cron --since today or grep CRON /var/log/syslog.

Stop two runs from overlapping

If a backup takes longer than the gap between runs, or hangs, cron starts the next one anyway, and two copies writing the same archive can ruin both. flock takes a lock before running the command; with -n it gives up at once if another run holds it:

Terminal
flock -n /run/lock/backup-www.lock /usr/local/bin/backup-www.sh

The lock is released when the command exits, even if it crashes, so there is no stale lock to clean up. In our test, a second flock -n on a held lock exited immediately with status 1. Variations:

  • -w 600 waits up to 600 seconds for the lock instead of giving up.
  • -E 0 makes a skipped run exit with status 0, if you would rather not count it as a failure.
  • timeout 4h in front of the script kills a run that hangs, so it cannot hold the lock forever. timeout exits with status 124 when it has to.
Terminal
flock -n /run/lock/backup-www.lock timeout 4h /usr/local/bin/backup-www.sh

To take the lock inside a bash script instead, open the lock file on a spare file descriptor:

Top of a bash script
exec 9>/run/lock/backup-www.lock
flock -n 9 || { echo "already running"; exit 1; }

systemd timers catch up on runs missed while the server was off and never start a second copy of a job that is still running: see how to schedule backups with systemd timers.

Test the job, then test the backup

  1. Run the script by hand as the user cron will use, for example sudo -u deploy /home/deploy/bin/backup.sh, then check echo $? prints 0.
  2. Run it again in cron's environment with the env -i command above.
  3. Schedule it a few minutes ahead, watch tail -f /var/log/backup-www.log, then set the real time.
  4. Confirm cron started it: journalctl -u cron --since today.
  5. Restore the archive somewhere and open the files. A backup you have not restored is a hope; this guide shows how to test one properly.

Logs only help if someone reads them. To hear about a run that failed or never started, have the job report success to a monitoring service that alerts you when the reports stop:

crontab
17 2 * * * /usr/local/bin/backup-www.sh >> /var/log/backup-www.log 2>&1 && curl -fsS -m 10 --retry 3 https://monitor.example.com/ping/backup-www > /dev/null

&& sends the ping only when the script exits with status 0. -f makes curl fail on HTTP errors, -sS hides the progress bar but still shows errors, -m 10 gives up after 10 seconds, and --retry 3 retries temporary failures.

Timezones and daylight saving time

cron schedules jobs in the system timezone, read from /etc/localtime. Check it with timedatectl; on a server set to UTC, the line to look for is Time zone: Etc/UTC (UTC, +0000).

  • Debian and Ubuntu cron has no per-crontab timezone. TZ= in a crontab changes the timezone your commands see, not when they run.
  • cronie, the cron used on RHEL and Fedora, supports CRON_TZ= to run a crontab's jobs in another zone.
  • When clocks change for daylight saving time, Debian and Ubuntu cron runs jobs from a skipped hour soon after the change, and does not run jobs in a repeated hour twice. Jobs with * in the minute or hour field simply follow the new time.

Run servers on UTC. It has no daylight saving time, so a 02:17 job runs exactly once every day, and logs from all your servers line up.

A schedule needs a matching rule for what to delete: see how long to keep backups. For what the jobs should cover, see what to back up on a Linux server.

Frequently asked questions

Why does my cron job work in the terminal but not in cron?
Almost always the environment. On Debian and Ubuntu, cron's PATH is /usr/bin:/bin and your shell profile is not loaded. Set PATH in the crontab or use full paths, and test with the env -i command in this guide.
How do I run a cron job every 5 minutes?
*/5 * * * * /path/to/script. Steps work in any field: 0 */2 * * * runs every two hours, on the hour.
Does cron run jobs it missed while the server was off?
No. cron only runs jobs whose time comes while it is running. anacron, a separate package, runs missed daily, weekly and monthly jobs, which suits machines that are not on all the time.
Where are cron logs on Ubuntu?
cron logs the jobs it starts to syslog: journalctl -u cron or grep CRON /var/log/syslog. A job's own output is kept only if you redirect it to a file.
Do I need to restart cron after editing a crontab?
No. cron checks every minute for changed crontabs, including files in /etc/cron.d, and reloads them.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: