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 AM0 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 weekdays0 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/bashexport PATH=/usr/local/bin:/usr/bin:/binexport MY_VAR=my_valueexport 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 UpdateAfter=docker.serviceRequires=docker.service [Service]Type=oneshotWorkingDirectory=/opt/myappExecStart=/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:00Persistent=trueRandomizedDelaySec=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-reloadsudo 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:00or simplydaily - 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 withsystemctl --user.
Troubleshooting
- The task didn’t run: Check
systemctl list-timersorcrontab -lfirst 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.