Bash Scripting for Engineers›06 · Scheduling & automation

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'.

Practitioner
Key wordscroncrontabsystemd timersOnCalendarenvironmentPATHloggingmissed runs

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 like daily, Mon..Fri 02:30, *:0/15 (every 15 minutes). Check them with systemd-analyze calendar.
  • RandomizedDelaySec= spreads a fleet's jobs so they don't all hit the backup server at once.
  • Nice= and IOSchedulingClass=idle keep 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

  1. Write /opt/bin/disk-report.sh that appends date and df -h / output to /var/tmp/disk-report.log (strict mode, full paths).
  2. Schedule it with cron every 2 minutes; confirm it runs. Now remove PATH from the environment on purpose and make it fail (env -i /opt/bin/disk-report.sh simulates cron).
  3. Replace the cron entry with disk-report.service + disk-report.timer using OnCalendar=*:0/2.
  4. Check systemctl list-timers, run it on demand with systemctl start, and read its output in journalctl -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) and startingDeadlineSeconds.
  • 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.