Timers and operations
Create a service and timer
Run scheduled server work as a paired oneshot service and timer with separate logs and state.
8 minute lesson
A systemd timer activates another unit, normally a service with the same base name. This is systemd’s answer to cron, and the split into two files is the point: the service says what to run, the timer says when. You can run the service by hand any time, and swap the schedule without touching the command.
Compared to a crontab line, you also get the job’s output in the journal, real dependencies, resource limits, and catch-up behavior for machines that were off.
The service half
Put the command in a Type=oneshot service:
# /etc/systemd/system/backup.service
[Unit]
Description=Nightly PostgreSQL backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-postgres.sh
No [Install] section. The service is never enabled on its own; the timer owns the schedule.
The timer half
The schedule goes in a timer using OnCalendar= (or monotonic settings like OnUnitActiveSec= for “every N minutes after the last run”):
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service every night
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
[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
NEXT LEFT LAST PASSED UNIT ACTIVATES
Tue 2026-08-04 03:00:00 UTC 9h left - - backup.timer backup.service
list-timers is your proof: it shows the next elapse, and after the first run, when it last fired.
The classic mistake is enabling backup.service instead of backup.timer. The backup then runs once at every boot and never on schedule, and list-timers shows nothing — which is exactly how you spot it.
Check the schedule expression
Calendar expressions are easy to get subtly wrong. systemd-analyze calendar parses one and shows the next elapse:
systemd-analyze calendar "*-*-* 03:00:00"
Design backup.service and backup.timer as above without running a real backup — point ExecStart= at /bin/true first. Verify both files load, check list-timers, and confirm the schedule with systemd-analyze calendar.
Lesson completed