Every server has boring, repetitive work: database backups at 2 AM, clearing old log files, pulling the latest code, checking that a service is still alive. On Ubuntu, you do not babysit these tasks — you hand them to cron, the time-based scheduler that has run Linux automation for decades. Once you understand its five-field syntax, you can schedule anything: shell scripts, PHP artisan commands, Node jobs, Python maintenance scripts, or a single rsync line.

This guide is for anyone running Ubuntu 22.04 or 24.04 (server or desktop). By the end you will have a working cron job of your own, know the difference between user and system crontabs, and know exactly where to look when a job silently does not run.

Prerequisites

  • Ubuntu 22.04 or 24.04 with sudo access.
  • A script or command you want to run on a schedule (we will use a database backup as the example).
  • Basic comfort with the terminal and a text editor like nano.

Step 1: Make sure cron is installed and running

On Ubuntu, cron comes from the cron package and runs as a systemd service. Verify it is installed and active:

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

systemctl enable --now cron starts the service immediately and makes sure it starts on every boot. systemctl status cron should show active (running) — if the scheduler itself is not running, nothing you schedule will ever fire.

Step 2: Open your personal crontab

Every user on the system can have their own crontab (schedule file). Open yours for editing:

crontab -e

The first time you run this, Ubuntu asks which editor to use — choose nano if you are unsure. Each line you add is one scheduled job that runs as your user, with your file permissions. This is the safest place for personal scripts: a job added here can never accidentally affect another user.

To list your current jobs at any time:

crontab -l

Step 3: Learn the five-field schedule syntax

A cron line has five time fields followed by the command:

* * * * * /path/to/command
│ │ │ │ │
│ │ │ │ └── Day of week (0-7, both 0 and 7 mean Sunday)
│ │ │ └──── Month (1-12)
│ │ └────── Day of month (1-31)
│ └──────── Hour (0-23)
└────────── Minute (0-59)

An asterisk means "every". Each field also accepts commas (lists), hyphens (ranges), and slashes (steps). A few examples so the pattern clicks:

  • 0 2 * * * — every day at 2:00 AM.
  • */15 * * * * — every 15 minutes.
  • 0 9 * * 1-5 — 9:00 AM, Monday to Friday.
  • 30 23 1 * * — 11:30 PM on the 1st of every month.
  • 0 0,12 * * * — midnight and noon, every day.

Cron also understands shorthand strings in place of the five fields: @reboot (run once when the machine boots), @hourly, @daily (midnight), @weekly (Sunday midnight), @monthly, and @yearly. They are handy for simple cases like @reboot /home/user/start-my-app.sh.

Watch out for one classic trap: if you restrict both day-of-month and day-of-week, cron runs when either matches (OR logic), not both. 0 0 1 * 1 means "midnight on the 1st of the month OR any Monday" — not "Monday the 1st".

Step 4: Add your first real job

Let us schedule a nightly MySQL backup. First create the script:

nano ~/bin/nightly-backup.sh

Paste this in (replace the placeholders with your real credentials):

#!/bin/bash
set -e
BACKUP_DIR="$HOME/backups"
mkdir -p "$BACKUP_DIR"
FILENAME="mydb-$(date +%F).sql.gz"
mysqldump -u root -p'SECRET' mydb | gzip > "$BACKUP_DIR/$FILENAME"
find "$BACKUP_DIR" -name "mydb-*.sql.gz" -mtime +7 -delete

The script dumps the database, compresses it with a dated filename, and deletes backups older than 7 days so the disk never fills up. Make it executable and run it once by hand to prove it works:

chmod +x ~/bin/nightly-backup.sh
~/bin/nightly-backup.sh

Now schedule it for 2:30 AM daily with crontab -e:

30 2 * * * /home/yourname/bin/nightly-backup.sh >> /home/yourname/backups/backup.log 2>&1

The >> part appends the script's output to a log file and 2>&1 folds error messages into the same log — without this, cron's output vanishes (more on that in Step 6). Note the absolute paths: cron runs jobs with a minimal environment (PATH is just /usr/bin:/bin), so anything cron must find — scripts, and tools installed in odd locations like snap binaries — needs its full path.

Step 5: Use system cron for system-wide tasks

Personal crontabs are for personal work. Jobs that belong to the whole machine — cleaning /tmp, rotating service logs, restarting a service — go in the system cron files instead:

sudo nano /etc/crontab

Lines in /etc/crontab and files in /etc/cron.d/ look almost identical to user crontabs, but they add a sixth field: the username the command runs as:

0 3 * * * root /usr/local/sbin/system-cleanup.sh
*/5 * * * * www-data /usr/bin/php /var/www/app/artisan schedule:run >> /dev/null 2>&1

The first line runs a cleanup script as root at 3 AM; the second runs Laravel's scheduler as the www-data user every 5 minutes. Running each job as the least-privileged user that can do the work is the correct habit — do not reach for root when www-data or your own account will do. The pre-made directories /etc/cron.hourly, /etc/cron.daily, /etc/cron.weekly, and /etc/cron.monthly are the simplest option of all: drop an executable script in, and the system runs it on that cadence with no crontab line needed.

Step 6: Capture output and errors the right way

By default, cron tries to email a job's output to the job owner. On a server with no mail configured, that email silently dies and your debugging evidence goes with it. Two practical approaches:

  1. Log to a file (recommended): append >> /path/to/job.log 2>&1 to the cron line, as in Step 4. You get a timestamped history you can tail whenever something looks wrong.
  2. Control the email: set MAILTO="you@example.com" at the top of the crontab to have output mailed somewhere real, or MAILTO="" to suppress it entirely when you are already logging to files.

A good backup-style cron line therefore looks like this — explicit logging, errors included, and nothing left to chance:

30 2 * * * /home/yourname/bin/nightly-backup.sh >> /home/yourname/backups/backup.log 2>&1

Step 7: Prevent overlapping runs with flock

If a job sometimes takes longer than its interval (a 10-minute sync scheduled every 5 minutes), two copies pile up and fight over the same files. Wrap the command in flock, which takes an exclusive lock and skips the new run if the old one is still going:

*/5 * * * * /usr/bin/flock -n /tmp/sync.lock /home/yourname/bin/sync.sh >> /home/yourname/sync.log 2>&1

flock -n means "do not wait": if the lock file is already held, the new instance exits immediately instead of queueing up. This one flag has prevented more 3 AM disk-full incidents than any other cron trick.

Step 8: Ten ready-to-use schedule examples

Copy, adjust, paste into crontab -e:

# Daily backup at 2:30 AM, with logging
30 2 * * * /home/yourname/bin/nightly-backup.sh >> /home/yourname/backups/backup.log 2>&1

# Restart a service every Sunday at 4 AM
0 4 * * 0 /usr/bin/systemctl restart myapp.service

# Clear old temp files every hour
0 * * * * /usr/bin/find /tmp -type f -atime +1 -delete

# Ping a health check every 5 minutes (dead man's switch)
*/5 * * * * /usr/bin/curl -fsS -m 10 --retry 3 https://hc.example.com/your-uuid > /dev/null

# Renew Let's Encrypt certs twice daily (recommended by Certbot docs)
0 0,12 * * * /usr/bin/certbot renew --quiet

# Pull the latest code and restart, weekdays at 6 PM
0 18 * * 1-5 /home/yourname/bin/deploy.sh >> /home/yourname/deploy.log 2>&1

# Run a Python report on the 1st of each month at 8 AM
0 8 1 * * /usr/bin/python3 /home/yourname/bin/monthly-report.py

# Start a script at boot, 60 seconds after startup settles
@reboot sleep 60 && /home/yourname/bin/on-boot.sh

# Database vacuum every night at 3 AM (PostgreSQL)
0 3 * * * /usr/bin/vacuumdb --all --analyze >> /var/log/vacuum.log 2>&1

# Sync uploads to S3 every 15 minutes, no overlapping runs
*/15 * * * * /usr/bin/flock -n /tmp/s3sync.lock /usr/bin/aws s3 sync /var/www/uploads s3://my-bucket/uploads

Verify it worked

  1. Smoke-test with a harmless job first. Add a line that appends the date to a file every minute, wait two minutes, then check the file — this proves cron is alive and your syntax is valid:
    * * * * * /bin/date >> /tmp/cron-test.txt
    Then cat /tmp/cron-test.txt should show two new timestamps. Delete the test line afterward.
  2. Read the cron logs. Ubuntu records every job cron starts:
    grep CRON /var/log/syslog | tail -20
    You should see entries like (yourname) CMD (/home/yourname/bin/nightly-backup.sh) at the scheduled minute. On systems using the journal, journalctl -u cron --since "1 hour ago" shows the same events.
  3. Check your log files. Whatever you pointed >> at should exist and contain the script's output (or be empty if the script printed nothing — which is fine).
  4. Confirm the system clock and timezone. Cron follows the system timezone, so a server set to UTC fires "2 AM" at a different wall-clock time than your laptop. Check with timedatectl, and if a crontab must run in a specific zone, set TZ=Asia/Kolkata at the top of that crontab.

Common issues and fixes

1. The job never runs, and there is nothing in the logs

Almost always one of three things: the cron service is not running (sudo systemctl enable --now cron), the machine was off or suspended at the scheduled time (cron does not run missed jobs — use a systemd timer or the anacron package for laptops that sleep), or the crontab line has a syntax error. Run crontab -l and re-read every field against the five-field diagram in Step 3.

2. The script works in my terminal but fails in cron

Cron's environment is minimal: no .bashrc, no aliases, no custom PATH. The two fixes are to use absolute paths for everything (the script, the interpreter, any tools) and to define anything the script needs at the top of the crontab, e.g. PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin. Always test the exact cron command with a bare env -i shell: env -i /bin/bash /home/yourname/bin/nightly-backup.sh.

3. Percent signs (%) in the command break everything

In a crontab line, an unescaped % is converted to a newline — so date +%F written directly in a crontab produces garbage. Escape them (date +\%F) or, better, keep complex logic inside a script file and call the script from the crontab.

4. Getting spammed with cron emails

Every byte a job prints becomes an email. Either log to files with >> file.log 2>&1 (recommended) or add MAILTO="" at the top of the crontab to disable mail. For intentionally noisy commands like health checks, end the line with > /dev/null 2>&1.

5. Two copies of the job run at once

A long-running job that overlaps its own schedule causes duplicate work and sometimes corruption. Wrap it with flock -n as shown in Step 7 so the second instance exits instead of stacking up.


Cron is one of those tools that pays for the hour you spend learning it for the rest of your career. Start with one logged, locked backup job, verify it in /var/log/syslog, then automate the next tedious task on your list.

Need help with server automation? XpertBees sets up and manages Ubuntu servers, backups, monitoring, and deployment pipelines for businesses — get in touch and we will take the 3 AM work off your plate.