A server earns its keep by doing things while you sleep: backups, log rotation, certificate renewal, a report at nine. Linux has two schedulers for that. Cron is forty years old and takes one line. Systemd timers take two small files and give you logs, catch-up after downtime and exact timing. This guide covers both on Ubuntu 24.04, with every behaviour checked on a fresh server from our image, including the three mistakes that make a cron job fail without a word.
You need a user with sudo; the Ubuntu setup guide creates one.
1. Cron in five minutes
Cron is installed and running on our Ubuntu and Debian images. Each user has a table of jobs; edit yours with:
crontab -e
Each line is five time fields and a command:
# min hour day month weekday command
30 3 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
*/5 * * * * /usr/local/bin/check.sh
0 9 * * 1-5 /usr/local/bin/report.sh
@reboot /usr/local/bin/start-tunnel.sh
That is: 03:30 every night, every 5 minutes, 09:00 on weekdays, and once after each boot. */5 means every fifth value, 1-5 a range, 1,15 a list. The server's time zone applies, which is UTC on our images; set yours once with the hostname and time zone guide and cron follows.
Check what ran with journalctl -u cron. Each run logs the user and the command, which is where the three traps show up.
Trap 1: the output vanishes. Cron mails a job's output to the user. Our image has no mail server, so the journal said (CRON) info (No MTA installed, discarding output) and the output was gone. Redirect it yourself, as the first example does with >> /var/log/backup.log 2>&1.
Trap 2: the percent sign. In a crontab, % ends the command and turns the rest into standard input. We scheduled date +%F > /tmp/today. The journal shows what cron really ran: CMD (date +). No file was written and no error reached us. Write \% instead, or put the command in a script and schedule the script.
Trap 3: a different environment. Cron runs jobs with /bin/sh, not bash, in your home directory, with a short environment. On Ubuntu 24.04 the PATH was better than its reputation, the full system path including /usr/local/bin and /snap/bin, for root and for a normal user. What is still missing is anything under your home: ~/.local/bin, nvm's node, pipx, cargo. Use full paths in the job, or start the line with . $HOME/.profile;.
Jobs that belong to the system rather than a user go in /etc/cron.d/ as a file with one extra field, the user:
30 3 * * * root /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
Ubuntu also runs everything in /etc/cron.hourly, cron.daily, cron.weekly and cron.monthly at 17 minutes past each hour, 06:25, 06:47 on Sundays and 06:52 on the 1st. Drop an executable script there and it runs with the rest.
2. Systemd timers
A timer is two units: a service that does the work, and a timer that says when. Create the script first, then the units.
sudo nano /usr/local/bin/backup.sh
sudo chmod +x /usr/local/bin/backup.sh
sudo nano /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup.sh
sudo nano /etc/systemd/system/backup.timer
[Unit]
Description=Run the backup every night at 03:30
[Timer]
OnCalendar=*-*-* 03:30
Persistent=true
AccuracySec=1m
[Install]
WantedBy=timers.target
Enable the timer, not the service:
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
systemctl list-timers backup.timer
list-timers shows the next run, how long until it, and the last run. Logs need no setup: journalctl -u backup.service has every start, finish and line of output, with timestamps. To run the job right now, sudo systemctl start backup.service, which is also how you test the script before trusting the schedule.
Calendar syntax. OnCalendar reads weekday year-month-day hour:minute:second, with * for any. Test an expression before you use it:
systemd-analyze calendar "Mon..Fri *-*-* 03:00" --iterations 2
It prints the normalized form and the next dates. Some expressions we checked:
| OnCalendar | Meaning |
|---|---|
*-*-* 03:30 | Every day at 03:30 |
Mon..Fri *-*-* 09:00 | Weekdays at 09:00 |
*:0/15 | Every 15 minutes |
weekly | Mondays at 00:00 |
*-*-01 04:00 | The 1st of each month at 04:00 |
*-*-* 03:00 Europe/Kyiv | 03:00 Kyiv time; ran at 01:00 UTC for us |
The three settings worth knowing. Persistent=true records the last run on disk and catches up a run missed while the server was off or the timer stopped. We stopped our every-two-minutes timer, waited past a scheduled run, and started it again: the missed run fired within the same second as the start. Without Persistent, it simply waits for the next slot.
AccuracySec is the surprise. Systemd groups timers within a one-minute window by default to save wake-ups, so our first run came at 07:42:18, not 07:42:00. With AccuracySec=1s the next runs landed on 07:46:00 and 07:48:00 to the second. Set it when the minute matters; leave the default for a nightly backup.
RandomizedDelaySec=10m does the opposite on purpose: it spreads the start over a window so ten servers do not hit the same backup storage at the same second.
For a one-off job, no files at all:
sudo systemd-run --on-active=30m --unit=cleanup-once /usr/local/bin/cleanup.sh
It creates a transient timer that fires once and disappears; systemctl list-timers shows it until then.
3. Which one to use
| Cron | Systemd timer | |
|---|---|---|
| Set up | 1 line | 2 files |
| Logs | Only if you redirect output | journalctl -u name.service |
| Missed run while server was off | Lost | Runs at boot with Persistent=true |
| Job still running at the next tick | A second copy starts | Skipped until the first finishes |
| Exact time | To the minute | To the second with AccuracySec=1s |
| Status | None | list-timers, systemctl status |
Use cron for a quick line you will recognise in a year. Use a timer for anything whose failure you need to see, or that must not overlap, or that must run after a reboot it missed: backups, certificate renewal, database cleanups. If a cron job can overlap, wrap it: flock -n /tmp/backup.lock /usr/local/bin/backup.sh, and the second copy exits at once.
Both schedulers run a job and stop. For a program that should run around the clock and come back after a crash, you want a service, not a schedule; see how to create a systemd service and, for Windows, how to run a script 24/7 on Windows Server. A good first timer is the one from our restic backup guide; automatic security updates are a timer Ubuntu ships already, apt-daily-upgrade.timer, one of the 17 that run on a fresh server.
Comments
No comments yet. Be the first.