Cron is the classic Linux job scheduler: a daemon that wakes up every minute and runs any commands whose schedule matches the current time. It is the simplest way to run backups, cleanups and reports at fixed times. In this tutorial you will learn the crontab syntax, schedule jobs for your own user and for the whole system on Ubuntu 24.04, capture their output, stop long jobs from overlapping and find out why a job did not run.

Prerequisites

To follow this tutorial you need:

  • A server running Ubuntu 24.04 LTS, for example a CubePath VPS. Everything also applies to Debian 12.
  • A non-root user with sudo privileges.
  • Basic familiarity with a text editor such as nano.

Step 1 - Checking that cron is running

Ubuntu installs and enables the cron package by default. Confirm the service is active:

systemctl status cron --no-pager
● cron.service - Regular background program processing daemon
     Loaded: loaded (/usr/lib/systemd/system/cron.service; enabled; preset: enabled)
     Active: active (running) since Thu 2026-09-25 08:01:12 UTC; 2h ago

If the service is missing, for example on a minimal image, install and start it:

sudo apt update
sudo apt install -y cron
sudo systemctl enable --now cron

Cron runs jobs in the server's time zone. Check it before choosing times:

timedatectl | grep "Time zone"
                Time zone: Etc/UTC (UTC, +0000)

Step 2 - Understanding the crontab syntax

Each line in a crontab has five time fields followed by the command to run:

MINUTE HOUR DAY_OF_MONTH MONTH DAY_OF_WEEK COMMAND
FieldAllowed values
Minute0-59
Hour0-23
Day of month1-31
Month1-12 or jan-dec
Day of week0-7 (0 and 7 are Sunday) or sun-sat

Each field accepts these operators:

OperatorMeaningExample
*Every value* in the hour field means every hour
,List of values0,30 means minute 0 and minute 30
-Range1-5 in day of week means Monday to Friday
/Step*/10 in the minute field means every 10 minutes

Here are the schedules you will use most often:

ScheduleExpression
Every 5 minutes*/5 * * * *
Every hour, on the hour0 * * * *
Every day at 03:3030 3 * * *
Weekdays at 18:000 18 * * 1-5
Every 15 minutes from 09:00 to 17:59 on weekdays*/15 9-17 * * 1-5
At 06:00, 12:00 and 18:000 6,12,18 * * *
Every Monday at 09:000 9 * * 1
First day of each month at midnight0 0 1 * *

Cron also accepts shortcuts in place of the five fields: @hourly, @daily (or @midnight), @weekly, @monthly, @yearly (or @annually) and @reboot, which runs once when the cron daemon starts after boot.

Step 3 - Creating your first user cron job

Every user has a personal crontab, and its jobs run as that user. Start with a job that runs every minute so you can see cron working without waiting.

Open your crontab. The first time, crontab asks which editor to use; choose nano if you are unsure:

crontab -e

Add this line at the end of the file:

* * * * * date >> "$HOME/cron-test.log"

Save and close the editor. crontab checks the syntax on save and prints crontab: installing new crontab if the file is valid. List your jobs to confirm:

crontab -l

After a minute or two, check the log:

cat ~/cron-test.log
Thu Sep 25 10:21:01 UTC 2026
Thu Sep 25 10:22:01 UTC 2026

Now replace the test with something useful. Open the crontab again with crontab -e, delete the test line and add a nightly archive of a ~/documents directory at 02:30:

30 2 * * * tar -czf "$HOME/backups/documents-$(date +\%F).tar.gz" -C "$HOME" documents

Notice the backslash in date +\%F. In a crontab, an unescaped % is turned into a newline and everything after it is passed to the command as standard input, so the job would break. Always write \% inside crontab lines.

Create the destination directory so the job does not fail on its first run:

mkdir -p ~/backups
rm ~/cron-test.log

Step 4 - Adding system-wide jobs in /etc/cron.d

Jobs that belong to the server rather than to a person, such as maintenance for an application, are easier to manage as files in /etc/cron.d. You can deploy them with configuration management and remove them by deleting one file.

Files in /etc/cron.d use the same syntax with one extra field: the user the job runs as, placed between the schedule and the command.

Create a job that removes files older than 14 days from an application's temporary upload directory every day at 04:15:

sudo nano /etc/cron.d/app-cleanup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

# Remove uploads older than 14 days
15 4 * * * www-data find /var/www/app/tmp-uploads -type f -mtime +14 -delete

Three rules apply to these files:

  • The file name may only contain letters, digits, underscores and hyphens. A file called app-cleanup.cron or app.cleanup is silently ignored.
  • The file must be owned by root and must not be writable by group or others.
  • The file must end with a newline.

Set the correct permissions:

sudo chown root:root /etc/cron.d/app-cleanup
sudo chmod 644 /etc/cron.d/app-cleanup

Cron notices new and changed files automatically; there is no need to restart it.

For jobs that do not need an exact time, you can also drop an executable script into /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly or /etc/cron.monthly. The same naming rule applies there, so backup works but backup.sh is skipped. Preview what would run with:

run-parts --test /etc/cron.daily
/etc/cron.daily/apt-compat
/etc/cron.daily/dpkg
/etc/cron.daily/logrotate
/etc/cron.daily/man-db

Step 5 - Setting the environment and capturing output

Cron runs jobs with a very small environment: a short PATH, /bin/sh as the shell and none of the variables from your .bashrc. This is the most common reason a command works in your terminal and fails in cron.

Set what your jobs need at the top of the crontab. These lines apply to every job below them:

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

By default cron tries to email any output to the job owner. Most servers have no mail transfer agent, so that output is lost. MAILTO="" disables mail explicitly, and you redirect output to a log file instead:

30 2 * * * /usr/local/bin/backup-db >> "$HOME/logs/backup-db.log" 2>&1

>> appends standard output to the file and 2>&1 sends errors to the same place. Without 2>&1, error messages, which are the ones you need, would be discarded. Create the log directory first with mkdir -p ~/logs.

If your jobs write large logs, add them to logrotate so they do not grow forever.

Step 6 - Preventing overlapping runs with flock

If a job scheduled every five minutes sometimes takes seven, two copies end up running at the same time. For backups or syncs that can corrupt data. The flock utility, part of util-linux and installed by default, holds a lock file while the command runs:

*/5 * * * * flock -n /tmp/sync-media.lock /usr/local/bin/sync-media >> "$HOME/logs/sync-media.log" 2>&1

With -n, a new run exits immediately if the previous one still holds the lock, instead of queuing up. You can test the behavior from a shell. Start a long command holding the lock in the background:

flock -n /tmp/demo.lock sleep 60 &

Then try to take the same lock:

flock -n /tmp/demo.lock echo "got the lock" || echo "already running, skipped"
already running, skipped

Step 7 - Checking the logs when a job does not run

On Ubuntu 24.04, cron logs every job it starts to the systemd journal. Show recent entries:

journalctl -u cron --since "1 hour ago" --no-pager
Sep 25 10:21:01 web01 CRON[5312]: pam_unix(cron:session): session opened for user sammy(uid=1000) by sammy(uid=0)
Sep 25 10:21:01 web01 CRON[5313]: (sammy) CMD (date >> "$HOME/cron-test.log")
Sep 25 10:21:01 web01 CRON[5312]: pam_unix(cron:session): session closed for user sammy

A CMD line proves cron started the job. If you see it but the job did not do its work, the problem is in the command itself: check your redirected log file. If there is no CMD line at all, cron never matched the schedule or never loaded the file.

To reproduce the cron environment when debugging, run the command with an almost empty environment:

env -i HOME="$HOME" SHELL=/bin/sh PATH=/usr/bin:/bin /bin/sh -c 'your_command'

Replace your_command with the exact command from the crontab line. If it fails here, it will fail in cron too.

Troubleshooting

  • No CMD line in the journal for a job in /etc/cron.d: check the file name for dots, the owner (root) and the permissions (644). Look for WRONG FILE OWNER or BAD FILE MODE messages with journalctl -u cron.
  • The job runs but a command is "not found": set PATH at the top of the crontab or use absolute paths (find them with command -v NAME).
  • The job runs at the wrong hour: cron uses the system time zone. Check it with timedatectl and change it with sudo timedatectl set-timezone Region/City, then restart cron.
  • A command with date +%F is cut off: escape % as \% in crontab lines.
  • (CRON) info (No MTA installed, discarding output): the job printed something and cron had nowhere to send it. Redirect the output to a file as shown in Step 5.

Conclusion

You can now read and write cron schedules, create personal and system-wide jobs, give them a predictable environment, keep their output, and stop overlapping runs with flock. When a job misbehaves, the journal tells you whether cron started it and your log file tells you what happened next.

For jobs that need dependencies, resource limits or catch-up after downtime, look at systemd timers, which add those features on top of the same idea. You may also want to review existing jobs with sudo ls /etc/cron.d and sudo crontab -l -u USER for each account on the server.