Lesson 06 of 8 · Modules
Scheduling & automation
Run scripts on a schedule reliably: cron syntax and its environment traps, systemd timers with logging and catch-up, and the checklist that turns 'works in my terminal' into 'works every night'.
Two schedulers
| cron | systemd timers | |
|---|---|---|
| Availability | Everywhere, familiar | Every systemd distribution |
| Logs | Mail or wherever you redirect | Automatically in the journal |
| Missed runs (machine off) | Skipped | Can catch up (Persistent=true) |
| Run on demand | Copy the command | systemctl start job.service |
| Resource limits, dependencies | ❌ | ✅ (all unit options) |
Use cron for quick per-user jobs; prefer systemd timers for anything that matters on servers.
cron is an alarm clock: at the set time, it rings and runs your script, and if you were asleep (the computer was off), the alarm just doesn't happen. A systemd timer is a reminder app: it rings too, but it also writes down every time it rang, what happened, and if it missed one, it reminds you when you wake up.
cron syntax
┌───────── minute (0–59)
│ ┌─────── hour (0–23)
│ │ ┌───── day of month (1–31)
│ │ │ ┌─── month (1–12)
│ │ │ │ ┌─ day of week (0–7, Sunday = 0 or 7)
│ │ │ │ │
30 2 * * 1-5 /opt/bin/backup.sh >> /var/log/backup.log 2>&1
The cron environment trap
cron runs your command with a minimal environment: a short PATH, no .bashrc, a different working directory. The fix is to make scripts self-sufficient:
#!/usr/bin/env bash
set -euo pipefail
export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
cd "$(dirname "$0")" # don't depend on the working directory
and capture output: >> /var/log/job.log 2>&1, or 2>&1 | logger -t backup to send it to the system log.
systemd timers: the better scheduler
Two small units. The service says what to run:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly backup
[Service]
Type=oneshot
ExecStart=/opt/bin/backup.sh
User=backup
Nice=10
IOSchedulingClass=idle
The timer says when:
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup nightly
[Timer]
OnCalendar=*-*-* 02:30:00
RandomizedDelaySec=15m
Persistent=true
[Install]
WantedBy=timers.target
$ sudo systemctl daemon-reload
$ sudo systemctl enable --now backup.timer
$ systemctl list-timers backup.timer
NEXT LEFT LAST PASSED UNIT ACTIVATES
Sun 2026-09-28 02:41:07 UTC 14h left - - backup.timer backup.service
$ sudo systemctl start backup.service # run it now, as a test
$ journalctl -u backup.service -n 20
OnCalendar=accepts expressions likedaily,Mon..Fri 02:30,*:0/15(every 15 minutes). Check them withsystemd-analyze calendar.RandomizedDelaySec=spreads a fleet's jobs so they don't all hit the backup server at once.Nice=andIOSchedulingClass=idlekeep the job from hurting daytime-like workloads.
A job that pages you when it fails
With systemd you can run a unit on failure:
[Unit]
OnFailure=notify-failure@%n.service
where notify-failure@.service sends an alert (a webhook, an email, a monitoring event). A backup that fails silently for three months is a classic incident.
Try it: move a cron job to a timer
- Write
/opt/bin/disk-report.shthat appendsdateanddf -h /output to/var/tmp/disk-report.log(strict mode, full paths). - Schedule it with cron every 2 minutes; confirm it runs. Now remove
PATHfrom the environment on purpose and make it fail (env -i /opt/bin/disk-report.shsimulates cron). - Replace the cron entry with
disk-report.service+disk-report.timerusingOnCalendar=*:0/2. - Check
systemctl list-timers, run it on demand withsystemctl start, and read its output injournalctl -u disk-report.
Going deeper: scheduling across a platform
- In Kubernetes, scheduled work becomes a CronJob (see Kubernetes Administration, lesson 03); set
concurrencyPolicy: Forbid(like flock) andstartingDeadlineSeconds. - Put time zones in writing: cron uses the system time zone; systemd timers accept an explicit zone in
OnCalendar. - Monitor jobs with a heartbeat: the job pings a monitoring endpoint on success, and an alert fires if the ping doesn't arrive. That catches jobs that never start at all.
Recap
- cron: 5 fields (minute, hour, day, month, weekday); minimal environment, so set
PATH, use full paths, redirect output. - systemd timers: a
.service(what) + a.timer(when), journal logging,Persistent=true,RandomizedDelaySec=, on-demand runs. - Alert on failure, and monitor with heartbeats.
This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.