systemd timer 설정, OnCalendar로 주기 실행하려면

Summary

Short answer: To run a periodic job, create a .timer unit that shares its base name with a .service unit, then set OnCalendar= in the [Timer] section. Add Persistent=true to cover runs missed while the system was off.

Compared with cron, a systemd timer reuses the same service infrastructure, so environment variable handling and log inspection through journalctl stay consistent with the rest of your services. This post covers pairing a timer with its service, writing OnCalendar= expressions, and handling missed runs with Persistent=.

How does a timer unit pair with a service?

Short answer: A .timer file targets the .service file that shares its base name by default.

For example, backup.timer targets backup.service without any extra configuration. The two unit files look like this:

# /etc/systemd/system/backup.service
[Unit]
Description=Backup job

[Service]
ExecStart=/usr/local/bin/backup.sh
# /etc/systemd/system/backup.timer
[Unit]
Description=Run backup.service on a schedule

[Timer]
OnCalendar=daily

[Install]
WantedBy=timers.target

After saving the unit files, enable the timer rather than the service.

systemctl enable --now backup.timer

Since the timer starts the service at the scheduled time, it is common practice not to separately enable backup.service itself.

How do you write an OnCalendar expression?

Short answer: OnCalendar= follows a weekday year-month-day hour:minute:second format, where * matches any value.

Common examples include:

  • Every midnight: OnCalendar=*-*-* 00:00:00 (equivalent to the shorthand OnCalendar=daily)
  • Every Monday at 9 AM: OnCalendar=Mon *-*-* 09:00:00
  • Every hour: OnCalendar=hourly

To verify an expression before deploying it, use systemd-analyze calendar to preview the next run time.

systemd-analyze calendar "Mon *-*-* 09:00:00"

This lets you confirm the expression matches the intended schedule without registering the unit.

How do you handle missed runs with Persistent?

Short answer: Adding Persistent=true to the [Timer] section runs a missed job immediately after the system boots.

If the system was powered off at the scheduled run time, that run is simply skipped. With Persistent=true, the timer records the last trigger time and checks at the next boot whether a run was missed, executing it right away if so.

# /etc/systemd/system/backup.timer
[Timer]
OnCalendar=daily
Persistent=true

This behaves similarly to anacron in the cron world. In environments where the machine is not always powered on, adding this option is a safer way to avoid silently skipping periodic jobs.

How do you check timer status and logs?

Short answer: Use systemctl list-timers to see the next run time, and journalctl to inspect the service’s execution logs.

Check registered timers and their next/previous run times with:

systemctl list-timers --all

Since the timer only triggers execution, the actual job logs live with the service it calls.

journalctl -u backup.service

When a timer doesn’t behave as expected, it helps to first confirm the next scheduled run with list-timers, then cross-check with journalctl to see whether the service actually ran.

What do people commonly ask?

What if the timer and service names differ?

Use Unit= in the [Timer] section to point at a specific service explicitly.

Do I have to switch from cron to timers?

Not necessarily, but timers are convenient when you want to reuse systemd’s service infrastructure and logging.

How do I check an expression before relying on it?

Use systemd-analyze calendar to preview the next run time.

What’s the takeaway?

A timer unit targets the service unit sharing its base name by default, and OnCalendar= sets the schedule. Adding Persistent=true ensures runs missed while the system was off still execute after the next boot.