People often ask me how to schedule tasks on Linux. I spent quite a while figuring it out myself and ran into a fair number of pitfalls, so I’ve gathered the methods I currently use in practice.

There are two main options: Cron and Systemd Timer. I now use the latter for most new projects, mainly because Systemd Timer is newer. Cron hasn’t been phased out, though; it still meets everyday needs.

Scheduling Scenarios

A scheduled task simply tells the system to do something automatically at a set time. Common examples include:

  • Backing up my blog database every Sunday
  • Periodically cleaning up Docker images and logs
  • Checking whether a service is still running and restarting it or sending an alert if it has stopped
  • Updating deployed open-source services on a schedule to keep them current

Large companies, of course, don’t maintain scheduled tasks directly from the command line like this. I use these methods on a personal server, where running commands directly is simple and meets my needs.

Here’s how to use these two scheduling options:

The Cron Daemon

As a developer, you may already be somewhat familiar with Cron expressions. Cron schedules use the following format:

minute hour day month weekday command

It’s simple to use: just add a command or Bash script after the Cron expression. Run crontab -e to edit your crontab:

# Back up every day at 2 AM
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
 
# Check a service every 5 minutes
*/5 * * * * /usr/local/bin/check.sh
 
# Send a report at 9 AM on weekdays
0 9 * * 1-5 /usr/local/bin/report.sh

Management is straightforward, too: use crontab -e to edit and crontab -l to view. I usually redirect all output to log files so I can review task execution and troubleshoot problems.

Note that file paths must be absolute, or the task won’t run!

Also, scheduled tasks can’t read your usual environment variables. I recommend defining the required variables at the beginning of the script. For example, add these to /usr/local/bin/check.sh:

#!/bin/bash
export PATH=/usr/local/bin:/usr/bin:/bin
export MY_VAR=my_value
export LANG=zh_CN.UTF-8

That’s all there is to Cron. Next, let’s look at Systemd Timer.

Systemd Timer

I’ve switched to Systemd Timer for most new machines. It requires two files, which is a little more work, but I find it more convenient to use.

Here’s an example from my own setup: automatically update a Docker Compose project every day at 3 AM.

First, create the service file /etc/systemd/system/docker-update.service:

[Unit]
Description=Daily Docker Compose Update
After=docker.service
Requires=docker.service
 
[Service]
Type=oneshot
WorkingDirectory=/opt/myapp
ExecStart=/bin/bash -c 'docker compose pull && docker compose up -d'

I ran into a pitfall here. At first, I wrote ExecStart=docker compose pull && docker compose up -d, but it didn’t run at all. Systemd doesn’t use a shell by default, so && is treated as an argument. Wrapping the command with /bin/bash -c fixed it. For anything more complex, I usually put the logic in a script and call that instead.

Next, create the timer file /etc/systemd/system/docker-update.timer:

[Unit]
Description=Run Docker Update Daily at 3 AM
 
[Timer]
OnCalendar=*-*-* 03:00:00
Persistent=true
RandomizedDelaySec=300
 
[Install]
WantedBy=timers.target

I really like the Persistent=true setting. If the machine is down when the task is scheduled, it catches up by running the task the next time the machine starts.

RandomizedDelaySec helps prevent a bunch of machines from pulling images at the same time and overwhelming the registry.

Enable the timer:

sudo systemctl daemon-reload
sudo systemctl enable --now docker-update.timer

Then use systemctl list-timers to see when it will run next, and journalctl -u docker-update.service to view the logs. I find this much cleaner than Cron.

I’ve also noted a few common time formats. For anything more complex, you can ask an AI assistant to generate one.

  • Every day at midnight: *-*-* 02:00:00 or simply daily
  • Every Monday morning: Mon *-*-* 09:00:00
  • Five minutes after boot: OnBootSec=5min

Choosing a Task Scheduler

Here’s how I generally decide:

Scenario My preference
A small script that fits on one line Cron
Need to inspect logs, depend on other services, or handle machines that may be shut down Systemd Timer
Need resource limits or permission isolation Systemd Timer
Running in a non-systemd environment Cron

I now use Timer for almost everything in production. You can query its logs directly with journalctl, see why a task failed, and rely on its catch-up mechanism. I keep Cron for some temporary, simple tasks and scripts on older machines.

A Few Other Options I Use Occasionally

  • at: For a task you only want to run once, such as “run this command at 3 PM today.”
  • anacron: For laptops or machines that are frequently shut down; it can handle tasks that were missed.
  • User-level Timer: When you don’t need root privileges, put the files under ~/.config/systemd/user/ and manage them with systemctl --user.

Troubleshooting

  • The task didn’t run: Check systemctl list-timers or crontab -l first to confirm it’s enabled.
  • It ran but failed: For a Timer, check the logs with journalctl -u service-name. For Cron, check the redirected log file.
  • It ran at the wrong time: Check the system time zone with timedatectl. Both use local time by default.

Final Thoughts

My current rule of thumb is to use Systemd Timer for new tasks and leave older, simple scripts on Cron. Both get the job done, but Timer is more convenient when it comes to logs and reliability. If you’re also handling tasks like automatic updates or backups, taking the time to configure Timer properly can save you a lot of troubleshooting later.