Skip to main content

cron — Time-Based Task Scheduler

Learning Focus

By the end of this lesson you will be able to write a five-field cron expression, configure environment variables in a crontab header, redirect output to a log file, prevent job overlap with flock, schedule WordPress maintenance tasks, and debug a failing cron job from first principles.

Overview

cron is the standard time-based job scheduler on Unix and Linux systems. It runs commands automatically at fixed intervals — every minute, every night at 2 AM, every Monday, or once a month. On a WordPress VPS, you use cron to schedule backups, clear caches, rotate logs, run WP-CLI maintenance, and trigger monitoring checks. Cron is simple and universally available, but it has sharp edges: a minimal environment, no built-in output capture, and a syntax that is easy to misread. This page covers every feature of cron in detail so you can build reliable, production-safe scheduled jobs.

Tool Snapshot
  • Core Function: Execute shell commands at scheduled times defined by a five-field time expression.
  • Primary Benefit: Universal availability — installed and active by default on every major Linux distribution.
  • Where to Use: Any time-based automation on a Linux VPS — backups, WP-CLI tasks, log rotation, monitoring.
  • Key Limitation: Minimal environment, no missed-run catch-up, no structured logging — requires explicit setup.

System Check

which crontab # Expected: /usr/bin/crontab
sudo systemctl status cron --no-pager --full # Verify cron service is running
crontab -l # List your current crontab (empty is fine)

How cron Works

Before writing any crontab entries, understand the five-field time model and the execution lifecycle:

  1. The crond daemon reads all crontab files every minute.
  2. It compares each job's five time fields to the current time.
  3. If all fields match, it executes the command in a minimal shell environment.
  4. Output goes nowhere unless you redirect it explicitly.
cron-field-diagram.txt
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12 or jan-dec)
│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday, or sun-sat)
│ │ │ │ │
* * * * * command_to_execute

Lifecycle Diagram

The crond daemon is stateless — it does not remember whether previous runs succeeded or failed, and it will not catch up missed runs (e.g., if the server was down at 2 AM). For catch-up behavior, use anacron or systemd timer Persistent=true.

Execution Environment

Cron strips your interactive shell's environment before running any job. This is the most common source of "works on the terminal, fails in cron" bugs:

What cron providesWhat cron does NOT provide
PATH=/usr/bin:/binYour full interactive $PATH
HOME=/root (or user's home)NVM, rbenv, pyenv, conda
SHELL=/bin/shYour .bashrc / .profile aliases
MAILTO=<crontab owner>Any export from your ~/.bashrc

Solution: Set PATH, SHELL, and any required variables at the top of the crontab, or use absolute paths for every command.


Where Cron Jobs Live

Cron jobs come from two categories:

User crontabs

Each user has a personal crontab edited with crontab -e. Jobs run as that user.

list-user-crontabs.sh
crontab -l # Your crontab
sudo crontab -l # Root's crontab
sudo crontab -u www-data -l # Another user's crontab

System cron files and directories

LocationFormatExecuted By
/etc/crontabIncludes a user columnThe cron daemon directly
/etc/cron.d/*Drop-in files, same format as /etc/crontabThe cron daemon directly
/etc/cron.hourly/Executable scripts (no cron syntax)run-parts via anacron or cron
/etc/cron.daily/Executable scriptsrun-parts
/etc/cron.weekly/Executable scriptsrun-parts
/etc/cron.monthly/Executable scriptsrun-parts
list-system-cron-locations.sh
sudo ls -lah /etc/cron.d/ 2>/dev/null || true
sudo ls -lah /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly 2>/dev/null || true
sudo run-parts --test /etc/cron.daily # Preview scripts that would run

Your First Cron Job in 5 Minutes

This hands-on walkthrough creates a trivial heartbeat job so you can see the full lifecycle before diving into reference material.

Step 1 — Create the script

The script defines what to run. Never put logic directly in the crontab.

create-heartbeat-script.sh
sudo tee /usr/local/bin/heartbeat.sh > /dev/null << 'EOF'
#!/usr/bin/env bash
echo "[$(date +%F\ %T)] heartbeat" >> /tmp/heartbeat.log
EOF
sudo chmod +x /usr/local/bin/heartbeat.sh

Always make scripts executable with chmod +x before referencing them in cron.

Step 2 — Test the script manually

Run it now so you know it works before adding it to cron:

test-heartbeat-manual.sh
/usr/local/bin/heartbeat.sh
cat /tmp/heartbeat.log
expected-output.txt
[2026-03-02 04:10:00] heartbeat

Step 3 — Open your crontab

open-crontab.sh
crontab -e

Step 4 — Add the job entry

Paste this into the editor. The * * * * * means "every minute":

heartbeat-crontab-entry.txt
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

* * * * * /usr/local/bin/heartbeat.sh

Save and close. crontab -e validates syntax before saving.

Step 5 — Verify the entry was saved

verify-crontab.sh
crontab -l

Step 6 — Wait one minute and check the log

check-heartbeat-log.sh
sleep 65 && cat /tmp/heartbeat.log
expected-output.txt
[2026-03-02 04:10:00] heartbeat
[2026-03-02 04:11:00] heartbeat

You now know the complete lifecycle: write script → test manually → edit crontab → verify → observe output. Every cron job you build follows the same pattern.


Syntax & Fields Reference

Core Syntax (user crontab)

crontab-core-syntax.txt
# minute hour day-of-month month day-of-week command
15 2 * * * /usr/local/bin/wp-backup.sh

System crontab format (/etc/crontab and /etc/cron.d/*)

System crontab files include an extra user field between the time fields and the command:

system-crontab-format.txt
# m h dom mon dow user command
15 2 * * * root /usr/local/bin/wp-backup-run

Field Value Reference

FieldAllowed ValuesNames Allowed
Minute0–59No
Hour0–23No
Day of month1–31No
Month1–12jan, feb, mar, apr, may, jun, jul, aug, sep, oct, nov, dec
Day of week0–7 (0 and 7 = Sunday)sun, mon, tue, wed, thu, fri, sat
Day of month AND day of week

When both day-of-month and day-of-week are set (not *), the job runs when either condition is true — OR logic, not AND. This is one of the most common sources of unexpected cron schedules.


Operators

OperatorNameMeaningExampleTranslates to
*WildcardEvery value in the field* * * * *Every minute
*/nStepEvery n-th value*/5 * * * *Every 5 minutes (0, 5, 10…)
a,bListSpecific values0 9,17 * * *At 9:00 and 17:00
a-bRangeConsecutive values0 9-17 * * 1-5Every hour from 9-17, Mon-Fri
a-b/nRange + StepEvery n-th within range0 0-12/4 * * *At hours 0, 4, 8, 12

Special Strings

These shortcuts replace the five time fields entirely:

StringEquivalentMeaning
@reboot(runs once at boot)Run once when the system starts
@yearly / @annually0 0 1 1 *Once a year (Jan 1 midnight)
@monthly0 0 1 * *Once a month (1st day midnight)
@weekly0 0 * * 0Once a week (Sunday midnight)
@daily / @midnight0 0 * * *Once a day (midnight)
@hourly0 * * * *Once an hour (minute 0)

Crontab Environment Variables

You can set environment variables at the top of a crontab file. They apply to all entries below them.

VariablePurposeDefaultExample
PATHSearch path for commands/usr/bin:/binPATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SHELLShell used to run commands/bin/shSHELL=/bin/bash
MAILTOSend job output to this addressUser who owns the crontabMAILTO=ops@example.com or MAILTO=""
HOMEWorking directory for jobsUser's home directoryHOME=/var/www/html
CRON_TZTimezone for schedule evaluationSystem timezoneCRON_TZ=UTC

Recommended crontab header:

crontab-header.txt
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
HOME=/var/www/html

# Jobs below this line use the above environment

crontab Command Reference

CommandDescription
crontab -eEdit the current user's crontab
crontab -lList the current user's crontab
crontab -rRemove the current user's entire crontab
crontab -riRemove with confirmation prompt (safer)
crontab -u USER -eEdit another user's crontab (requires root)
crontab -u USER -lList another user's crontab (requires root)
crontab FILEInstall a crontab from a file (replaces current)
crontab -r is destructive

It deletes your entire crontab instantly with no confirmation. On many keyboards, r is next to e. Consider aliasing crontab -r to crontab -ri to require confirmation.


Percent Sign (%) Gotcha

In crontab entries, % is interpreted as a newline. The first % ends the command; everything after it becomes stdin fed to the command. This commonly breaks date +%F patterns.

Failing example:

broken-cron-percent.txt
# BROKEN — the % splits the command
15 2 * * * /usr/local/bin/backup.sh >> /var/log/backup-$(date +%F).log 2>&1

Working solutions:

cron-percent-fix.txt
# Option 1: Escape the percent sign
15 2 * * * /usr/local/bin/backup.sh >> /var/log/backup-$(date +\%F).log 2>&1

# Option 2 (recommended): Move complexity into a script
15 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1

Output, Logging & MAILTO

Cron does not capture stdout or stderr by default. If output is produced and MAILTO is set (or defaults to the crontab owner), cron tries to email it. On most VPS environments, local mail is not configured, so output is silently lost.

Always redirect output to a log file:

cron-logging-pattern.txt
15 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
PartMeaning
>>Append stdout to the log file (never truncate)
2>&1Redirect stderr into the same file as stdout
MAILTO=""Suppress any email attempt (set at top of crontab)

Preventing Overlap with flock

If a job takes longer than the scheduling interval, cron launches another instance on top of it. This can cause double CPU/IO usage, corrupt backups, or create race conditions.

Use flock to ensure only one instance runs:

cron-flock-pattern.txt
15 2 * * * flock -n /var/lock/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
flock FlagMeaning
-nNon-blocking — exit immediately if lock is held
-w SECONDSWait up to SECONDS for the lock, then give up
-E CODEExit with CODE if lock cannot be acquired (default is 1)

Practical Examples

Minutes

1. Every minute (health check)

cron-every-minute.txt
* * * * * /usr/local/bin/health-check.sh >> /var/log/health.log 2>&1

Schedule: * * * * * = every minute of every hour of every day. Use case: Lightweight health pings or queue workers.


2. Every 5 minutes (disk monitoring)

cron-every-5-min.txt
*/5 * * * * /usr/local/bin/check-disk.sh >> /var/log/disk-check.log 2>&1

Schedule: */5 = at minutes 0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55. Use case: Disk usage monitoring, cache age checks.


3. Every 15 minutes (WordPress cron runner)

cron-every-15-min.txt
*/15 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now --path=/var/www/html >> /var/log/wp-cron.log 2>&1

Schedule: */15 = minutes 0, 15, 30, 45. Use case: Replace WordPress's default wp-cron.php page-visit trigger. Requires define('DISABLE_WP_CRON', true); in wp-config.php.


4. Every 30 minutes (CDN sync)

cron-every-30-min.txt
*/30 * * * * /usr/local/bin/sync-cdn.sh >> /var/log/cdn-sync.log 2>&1

Use case: CDN cache warm or asset sync.


5. Every 10 minutes during peak hours only

cron-10min-peak-hours.txt
*/10 8-20 * * * /usr/local/bin/cache-warm.sh >> /var/log/cache-warm.log 2>&1

Schedule: every 10 minutes, but only from 8 AM to 8 PM. Use case: Keep the cache warm during peak traffic hours only.


Hours

6. Every hour (at minute 0)

cron-every-hour.txt
0 * * * * /usr/local/bin/hourly-report.sh >> /var/log/hourly-report.log 2>&1

Use case: Hourly summary reports, log rotation checks.


7. Every 2 hours (media sync)

cron-every-2-hours.txt
0 */2 * * * /usr/local/bin/sync-media.sh >> /var/log/media-sync.log 2>&1

Schedule: 0 */2 = minute 0 of hours 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22. Use case: Sync uploaded media to offsite storage every 2 hours.


8. Every 6 hours (cache flush)

cron-flush-cache.txt
0 */6 * * * cd /var/www/html && /usr/local/bin/wp cache flush --path=/var/www/html >> /var/log/cache-flush.log 2>&1

Schedule: minute 0 of hours 0, 6, 12, 18. Use case: Periodically flush a stale or large object cache.


9. Business hours only — every hour 9-17, Mon-Fri

cron-business-hours-only.txt
0 9-17 * * 1-5 /usr/local/bin/sync-crm.sh >> /var/log/crm-sync.log 2>&1

Use case: Sync WordPress orders with a CRM only during business hours.


Daily

10. Daily at 2:15 AM (WordPress backup with flock)

cron-wp-backup-flock.txt
15 2 * * * flock -n /var/lock/wp-backup.lock /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1

Use case: Nightly WordPress full backup during low-traffic hours. flock skips if a previous run is still going.


11. Daily WordPress database backup with flock

cron-wp-db-backup.txt
30 2 * * * flock -n /var/lock/wp-db-backup.lock /usr/local/bin/wp db export /mnt/backups/wp-db-$(date +\%F).sql --path=/var/www/html >> /var/log/wp-db-backup.log 2>&1

Schedule: daily at 2:30 AM. Note the escaped \%F. Use case: Export the WordPress database to the backup volume.


12. Daily at midnight (log rotation)

cron-daily-midnight.txt
0 0 * * * /usr/local/bin/log-rotate.sh >> /var/log/log-rotate.log 2>&1

13. Daily using @daily shortcut

cron-at-daily.txt
@daily /usr/local/bin/cleanup-tmp.sh >> /var/log/cleanup.log 2>&1

Equivalent to 0 0 * * * (midnight).


14. Twice a day (9 AM and 9 PM)

cron-twice-daily.txt
0 9,21 * * * /usr/local/bin/uptime-check.sh >> /var/log/uptime.log 2>&1

15. Purge old backups (daily at 4 AM)

cron-purge-old-backups.txt
0 4 * * * find /mnt/backups -name "*.sql" -mtime +14 -delete >> /var/log/backup-purge.log 2>&1

Use case: Retention management — delete database backups older than 14 days.


16. Daily disk usage alert

cron-disk-alert.txt
*/10 * * * * /usr/local/bin/disk-alert.sh >> /var/log/disk-alert.log 2>&1
disk-alert.sh
#!/usr/bin/env bash
set -euo pipefail
THRESHOLD=85
USAGE=$(df -h / | awk 'NR==2 {gsub(/%/,""); print $5}')
if [ "$USAGE" -ge "$THRESHOLD" ]; then
echo "[$(date -Is)] WARNING: Root disk at ${USAGE}%"
fi

Use case: Log a warning when root partition usage exceeds 85%.


17. Daily SSL certificate renewal check

cron-certbot-renew.txt
0 3 * * * /usr/bin/certbot renew --quiet --deploy-hook "systemctl reload nginx" >> /var/log/certbot.log 2>&1

Use case: Certbot checks if any certificate is within 30 days of expiry and renews it. The deploy hook reloads Nginx only if a renewal happened.


Weekly

18. Every Monday at 3 AM (weekly cleanup)

cron-weekly-monday.txt
0 3 * * 1 /usr/local/bin/weekly-cleanup.sh >> /var/log/weekly-cleanup.log 2>&1

Use case: Weekly WordPress plugin update check, old revision cleanup, wp db optimize.


19. Weekdays at 6 AM (business digest)

cron-weekdays-6am.txt
0 6 * * 1-5 /usr/local/bin/daily-digest.sh >> /var/log/digest.log 2>&1

Use case: Email a daily business digest on weekdays only.


20. Saturday maintenance window at 2 AM

cron-saturday-maintenance.txt
0 2 * * 6 /usr/local/bin/maintenance.sh >> /var/log/maintenance.log 2>&1

Use case: Run heavier tasks (database optimization, full backup, filesystem check) on a weekend night.


21. Weekly PHP-FPM restart (reclaim memory)

cron-restart-phpfpm.txt
0 5 * * 0 /usr/bin/systemctl restart php8.2-fpm >> /var/log/phpfpm-restart.log 2>&1

Schedule: Sunday at 5 AM. Use case: Reclaim leaked memory from PHP-FPM workers during the lowest traffic window.


Monthly and Beyond

22. 1st and 15th of every month

cron-bimonthly.txt
0 4 1,15 * * /usr/local/bin/bimonthly-report.sh >> /var/log/bimonthly.log 2>&1

Use case: Semi-monthly billing reports or archive rotation.


23. First of every month at midnight

cron-monthly-1st.txt
0 0 1 * * /usr/local/bin/monthly-report.sh >> /var/log/monthly.log 2>&1

24. Once a year (January 1)

cron-yearly.txt
0 0 1 1 * /usr/local/bin/annual-cleanup.sh >> /var/log/annual.log 2>&1

Special

25. At reboot (one-time startup task)

cron-at-reboot.txt
@reboot /usr/local/bin/start-monitoring.sh >> /var/log/monitoring-boot.log 2>&1

Runs once, immediately after the system boots. Use case: Start a custom monitoring daemon or one-shot setup task after a reboot.


26. Nginx log rotation

cron-nginx-log-rotate.txt
0 0 * * * /usr/sbin/logrotate /etc/logrotate.d/nginx >> /var/log/logrotate-nginx.log 2>&1

27. Place in /etc/cron.d/ as a drop-in file

System-managed jobs can be placed in /etc/cron.d/ as files — no crontab -e needed:

/etc/cron.d/wp-backup
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""

# m h dom mon dow user command
15 2 * * * www-data flock -n /var/lock/wp-backup.lock /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1

Safe Cron Script Template

Use a proper script for anything beyond a single command. This template includes every defensive pattern:

safe-cron-script-template.sh
#!/usr/bin/env bash
set -euo pipefail

# Explicit environment
umask 077
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"

# Lock and log
LOCK="/var/lock/myjob.lock"
LOG="/var/log/myjob.log"

mkdir -p "$(dirname "$LOG")"

{
echo "[$(date -Is)] start"
echo "user=$(id -un) uid=$(id -u)"
echo "pwd=$(pwd)"

# Acquire lock (non-blocking)
flock -n "$LOCK" /usr/local/bin/myjob

echo "[$(date -Is)] done (exit $?)"
} >> "$LOG" 2>&1

Corresponding crontab entry:

cron-entry-for-script.txt
15 2 * * * /usr/local/bin/myjob-wrapper.sh

Debugging Workflow

Step-by-step debug protocol

  1. Run the command manually — execute the exact command from the crontab:
    /usr/local/bin/myjob.sh
    echo "Exit code: $?"
  2. Verify absolute paths — replace every bare command with its full path.
  3. Simulate the cron environment — strip your interactive shell:
    simulate-cron-environment.sh
    env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /usr/local/bin/myjob.sh
    echo $?
  4. Check the output log:
    tail -f /var/log/myjob.log
  5. Inspect cron's own execution log:
    check-cron-logs.sh
    journalctl -u cron --since '2 hours ago' --no-pager | tail -20
    grep CRON /var/log/syslog | tail -20

Common failure patterns

SymptomLikely CauseFix
Job does not run at allCron service not running, or wrong crontabsystemctl status cron; verify with crontab -l
Job runs but "command not found"Minimal PATH in cronUse absolute paths or set PATH= at top of crontab
Job runs but no output fileWrong redirect path, or directory doesn't existCreate the log directory; test the redirect manually
Job runs differently than interactiveMissing environment variablesSet PATH, HOME, SHELL in crontab header or script
Job runs twice / overlapsSchedule interval shorter than runtimeAdd flock -n /var/lock/job.lock
% breaks the commandUnescaped percent sign in crontab entryEscape as \% or move the command into a script
Job fires at wrong timeDay-of-month AND day-of-week both setRemember: OR logic — either condition fires the job
No evidence it ran at allMAILTO issue masking errorRedirect to log file; set MAILTO="" in crontab

WordPress VPS Cron Patterns

TaskScheduleCommand Pattern
WordPress cron events*/15 * * * *wp cron event run --due-now --path=/var/www/html
Nightly full backup15 2 * * *backup.sh + flock
Database export30 2 * * *wp db export /mnt/backups/db.sql + flock
Backup retention0 4 * * *find /mnt/backups -mtime +14 -delete
Cache flush0 */6 * * *wp cache flush
Disk usage alert*/10 * * * *Script checking df -h output
SSL certificate renewal0 3 * * *certbot renew --quiet
PHP-FPM restart0 5 * * 0systemctl restart php8.2-fpm
Log rotation0 0 * * *logrotate /etc/logrotate.d/nginx

Benefits

  • Universal availability — installed and active by default on virtually every Linux distribution.
  • Simple, proven model — five time fields plus a command; nothing more to learn to get started.
  • Per-user isolation — each user has their own crontab; jobs run under that user's UID.
  • System-level directories/etc/cron.d/, /etc/cron.daily/, etc. allow drop-in job management without editing a monolithic file.
  • Special strings@reboot, @daily, @hourly etc. make common schedules readable at a glance.
  • Email notification — built-in MAILTO variable can send job output to an address when a mail subsystem is configured.
  • Low overhead — the cron daemon is lightweight and consumes negligible resources.

Best Practices

  • Always use absolute paths — cron runs with a minimal PATH (/usr/bin:/bin). Use /usr/local/bin/wp instead of wp.
  • Redirect output to a log — cron does not capture stdout/stderr by default. Always append >> /var/log/jobname.log 2>&1.
  • Use flock to prevent overlap — if a job takes longer than the interval, the next run starts on top of it. Use flock -n /var/lock/job.lock to skip the duplicate.
  • Edit with crontab -e — never edit spool files directly; crontab -e validates syntax before saving.
  • Set PATH at the top of the crontab — one line at the top eliminates most "command not found" failures.
  • Escape % signs — in crontab entries, % is interpreted as a newline. Escape as \% or move the command into a script.
  • Test the exact command manually first — run the command interactively in a stripped environment, confirm it works, then add it to cron.
  • Use a script for anything non-trivial — keep the crontab entry short; put logic in a bash script with set -euo pipefail.
  • Set MAILTO="" — even if mail isn't configured, suppresses attempted mail delivery noise.
  • Monitor cron logs — check journalctl -u cron or /var/log/syslog regularly to confirm jobs actually fire.

Tips & Strategy

Use crontab.guru to verify your schedule

Before adding a job, paste your five time fields into crontab.guru to see a plain-English translation. This catches off-by-one hour mistakes and ambiguous day-of-week values instantly.

One job, one script, one log

Keep your crontab entries minimal:

15 2 * * * /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1

Put all logic inside wp-backup.sh. This makes debugging, testing, and version control trivial.

Always wrap with flock for long-running jobs
15 2 * * * flock -n /var/lock/wp-backup.lock /usr/local/bin/wp-backup.sh >> /var/log/wp-backup.log 2>&1

If the previous run is still going, flock -n makes the new instance exit immediately instead of doubling CPU and I/O.

Test under the cron environment

Cron strips your interactive shell's environment. To simulate:

env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /usr/local/bin/myjob.sh

If this fails but running the script normally works, you have an environment or PATH issue.

Use @reboot for one-time startup tasks
@reboot /usr/local/bin/start-monitoring.sh >> /var/log/monitoring.log 2>&1

Runs once at system boot — useful for restarting custom daemons or one-shot setup scripts.


Hands-On Practice

Quick Lab (5 Exercises)

  1. Run crontab -l to view your current crontab. If empty, add a test job that appends the current date to a file every minute.
  2. Create a script at /tmp/cron-test.sh that echoes [date] hello from cron. Schedule it to run every minute. Verify output appears in the log file.
  3. Edit the crontab to set PATH= at the top. Remove all absolute paths from one entry and confirm it still works.
  4. Add flock -n /tmp/cron-test.lock to the entry. Open two terminals and test that the second invocation exits without running.
  5. After testing, remove the test entry with crontab -e. Verify with crontab -l.

Task: Build a Cron-Based Backup System

Write a complete crontab (with header variables) that:

  • Runs a WordPress database backup at 2:30 AM daily
  • Purges backups older than 14 days at 4 AM daily
  • Checks disk usage every 10 minutes
  • Flushes the WordPress object cache every 6 hours
Solution
wordpress-backup-crontab.txt
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=""
HOME=/var/www/html

# WordPress database backup (daily at 2:30 AM, with lock)
30 2 * * * flock -n /var/lock/wp-db-backup.lock /usr/local/bin/wp db export /mnt/backups/wp-db-$(date +\%F).sql --path=/var/www/html >> /var/log/wp-db-backup.log 2>&1

# Purge old backups (daily at 4 AM)
0 4 * * * find /mnt/backups -name "*.sql" -mtime +14 -delete >> /var/log/backup-purge.log 2>&1

# Disk usage alert (every 10 minutes)
*/10 * * * * /usr/local/bin/disk-alert.sh >> /var/log/disk-alert.log 2>&1

# Flush WordPress object cache (every 6 hours)
0 */6 * * * cd /var/www/html && /usr/local/bin/wp cache flush --path=/var/www/html >> /var/log/cache-flush.log 2>&1

Mini Quiz

  1. What are the five time fields in a cron entry, in order?
  2. What does */5 mean in the minute field?
  3. What is the difference between a user crontab and /etc/crontab?
  4. Why should you always use absolute paths in cron jobs?
  5. What does % do inside a crontab entry and how do you escape it?
  6. How does flock -n prevent overlapping jobs?
  7. What does @reboot do?
  8. How do you redirect both stdout and stderr to a log file?
  9. When both day-of-month and day-of-week are set, does cron use AND or OR logic?
  10. What command shows cron's own execution log on a systemd-based system?

Cheat Sheet
cron-cheat-sheet.sh
# ── Crontab management ────────────────────────────────────────────────────────
crontab -e # Edit your crontab
crontab -l # List your crontab
crontab -ri # Delete with confirmation (safer than -r)
sudo crontab -u www-data -e # Edit another user's crontab
sudo crontab -u www-data -l # List another user's crontab

# ── System cron locations ─────────────────────────────────────────────────────
ls /etc/cron.d/ # Drop-in cron files
sudo run-parts --test /etc/cron.daily # Preview daily jobs

# ── Cron service ──────────────────────────────────────────────────────────────
sudo systemctl status cron # Check cron daemon status
journalctl -u cron --since '1 hour ago' --no-pager # Execution log
grep CRON /var/log/syslog | tail -20 # Syslog-based systems

# ── Simulate cron environment ─────────────────────────────────────────────────
env -i HOME=$HOME SHELL=/bin/sh PATH=/usr/bin:/bin /usr/local/bin/myjob.sh

# ── Common schedule patterns ──────────────────────────────────────────────────
# * * * * * Every minute
# */5 * * * * Every 5 minutes
# */15 * * * * Every 15 minutes
# 0 * * * * Every hour (at :00)
# 0 */2 * * * Every 2 hours
# 0 0 * * * Daily at midnight
# 15 2 * * * Daily at 2:15 AM
# 0 9,17 * * * Twice a day (9 AM and 5 PM)
# 0 6 * * 1-5 Weekdays at 6 AM
# 0 3 * * 1 Every Monday at 3 AM
# 0 0 1 * * First of every month
# 0 0 1 1 * January 1st (yearly)
# @reboot Once at boot
# @daily Shortcut for midnight
# @hourly Shortcut for every hour

# ── Safe cron entry pattern ───────────────────────────────────────────────────
15 2 * * * flock -n /var/lock/job.lock /usr/local/bin/job.sh >> /var/log/job.log 2>&1

What's Next