Introduction to crontab
The crontab command is used to schedule tasks (cron jobs) to run automatically at specified times or intervals in Unix/Linux systems. It's an essential tool for automating repetitive tasks like backups, system maintenance, log rotation, and monitoring.
Key Concepts
- Cron: The daemon that executes scheduled commands
- Crontab: The file containing cron job definitions
- Cron Jobs: Individual scheduled commands
- Schedule Expression: Time specification for when to run jobs
- Environment: Cron jobs run with limited environment variables
1. Basic Crontab Commands
Managing Crontab Files
# List current user's crontab crontab -l # Edit current user's crontab crontab -e # Remove current user's crontab crontab -r # View another user's crontab (requires root) sudo crontab -u username -l # Edit another user's crontab (requires root) sudo crontab -u username -e # List with line numbers crontab -l | cat -n # Backup crontab crontab -l > my_crontab_backup.txt # Restore from backup crontab my_crontab_backup.txt # Check if crontab exists if crontab -l &>/dev/null; then echo "Crontab exists" else echo "No crontab for user" fi
Crontab File Format
# Each line has 6 fields: # minute hour day month day_of_week command # * * * * * command_to_execute # │ │ │ │ │ # │ │ │ │ └── day of week (0-7, 0 and 7 = Sunday) # │ │ │ └──── month (1-12) # │ │ └────── day of month (1-31) # │ └──────── hour (0-23) # └────────── minute (0-59) # Example: Run every day at 2:30 AM 30 2 * * * /home/user/backup.sh # Run every hour 0 * * * * /usr/bin/php /var/www/html/cron.php # Run every Monday at 5 PM 0 17 * * 1 /home/user/weekly_report.sh # Run on the 1st of every month at midnight 0 0 1 * * /home/user/monthly_cleanup.sh
2. Time Specification Syntax
Basic Time Values
# Numeric values # Minute: 0-59 # Hour: 0-23 # Day of month: 1-31 # Month: 1-12 # Day of week: 0-7 (0 and 7 = Sunday) # Examples 30 4 * * * /script.sh # Daily at 4:30 AM 0 22 * * * /script.sh # Daily at 10:00 PM 15 10 * * * /script.sh # Daily at 10:15 AM 0 0 * * * /script.sh # Daily at midnight
Using Ranges and Lists
# Ranges (using hyphen) 0 9-17 * * * /script.sh # Every hour from 9 AM to 5 PM 30 1-5 * * * /script.sh # At 1:30, 2:30, 3:30, 4:30, 5:30 # Lists (using commas) 15,30,45 * * * * /script.sh # At 15, 30, and 45 minutes past every hour 0 9,13,17 * * * /script.sh # At 9 AM, 1 PM, and 5 PM # Combined ranges and lists 0 9-17/2 * * * /script.sh # Every 2 hours from 9 AM to 5 PM 15,45 9-17 * * * /script.sh # At 9:15, 9:45, 10:15, 10:45, etc. # Multiple ranges 0 9-12,14-17 * * * /script.sh # Every hour from 9-12 and 14-17
Using Steps
# Steps (using /) */15 * * * * /script.sh # Every 15 minutes 0 */2 * * * /script.sh # Every 2 hours 0 0 */3 * * /script.sh # Every 3 days */10 9-17 * * * /script.sh # Every 10 minutes between 9 AM and 5 PM # Common step examples */5 * * * * /script.sh # Every 5 minutes */10 * * * * /script.sh # Every 10 minutes */30 * * * * /script.sh # Every 30 minutes 0 */6 * * * /script.sh # Every 6 hours 0 0 */2 * * /script.sh # Every 2 days
Special Keywords
# Special time specifications @reboot # Run once at startup @yearly # Run once a year (0 0 1 1 *) @annually # Same as @yearly @monthly # Run once a month (0 0 1 * *) @weekly # Run once a week (0 0 * * 0) @daily # Run once a day (0 0 * * *) @midnight # Same as @daily @hourly # Run once an hour (0 * * * *) # Examples @reboot /home/user/startup.sh @daily /home/user/daily_backup.sh @weekly /home/user/weekly_report.sh @monthly /home/user/monthly_cleanup.sh @yearly /home/user/yearly_archive.sh # Note: These may not be available on all systems # For maximum compatibility, use numeric equivalents: # @daily -> 0 0 * * * # @hourly -> 0 * * * *
3. Practical Cron Job Examples
System Maintenance Jobs
# Edit crontab crontab -e # Daily system update (at 2 AM) 0 2 * * * /usr/bin/apt-get update && /usr/bin/apt-get upgrade -y >> /var/log/update.log 2>&1 # Weekly disk cleanup (Sunday at 3 AM) 0 3 * * 0 /home/user/scripts/cleanup.sh # Monthly log rotation (1st of month at 4 AM) 0 4 1 * * /usr/sbin/logrotate /etc/logrotate.conf # Check disk space every hour and alert if low 0 * * * * /home/user/scripts/check_disk.sh # Backup database daily at 1 AM 0 1 * * * /usr/bin/mysqldump -u root mydb > /backup/mydb_$(date +\%Y\%m\%d).sql # Remove old backups (older than 30 days) daily at 2 AM 0 2 * * * find /backup -type f -name "*.sql" -mtime +30 -delete
Backup Script with Crontab
#!/bin/bash
# backup.sh - Comprehensive backup script
# Configuration
BACKUP_DIR="/backup"
LOG_FILE="/var/log/backup.log"
DATABASES=("db1" "db2" "db3")
FILES=("/home/user/documents" "/home/user/pictures")
# Logging function
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}
# Create backup directory if not exists
mkdir -p "$BACKUP_DIR"
# Database backups
for db in "${DATABASES[@]}"; do
log "Starting database backup: $db"
if mysqldump -u backup "$db" | gzip > "$BACKUP_DIR/${db}_$(date +\%Y\%m\%d).sql.gz"; then
log "Database backup successful: $db"
else
log "Database backup failed: $db"
fi
done
# File backups
for dir in "${FILES[@]}"; do
name=$(basename "$dir")
log "Starting file backup: $name"
if tar -czf "$BACKUP_DIR/${name}_$(date +\%Y\%m\%d).tar.gz" "$dir" 2>/dev/null; then
log "File backup successful: $name"
else
log "File backup failed: $name"
fi
done
# Clean old backups (keep 30 days)
find "$BACKUP_DIR" -type f -name "*.gz" -mtime +30 -delete
log "Backup process completed"
Crontab Entries for the Script
# Database backup every 6 hours 0 */6 * * * /home/user/scripts/backup.sh # Daily backup at 1 AM 0 1 * * * /home/user/scripts/backup.sh >> /var/log/backup_cron.log 2>&1 # Weekly full backup (Sunday at 2 AM) 0 2 * * 0 /home/user/scripts/full_backup.sh # Monthly archive (1st of month at 3 AM) 0 3 1 * * /home/user/scripts/archive_monthly.sh
Monitoring Script
#!/bin/bash # monitor.sh - System monitoring script # Configuration THRESHOLD_CPU=80 THRESHOLD_MEM=90 THRESHOLD_DISK=85 ALERT_EMAIL="[email protected]" LOG_FILE="/var/log/monitor.log" # Get current timestamp timestamp=$(date '+%Y-%m-%d %H:%M:%S') # Check CPU load load=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1 | xargs) cores=$(nproc) cpu_percent=$(echo "$load * 100 / $cores" | bc) if (( $(echo "$cpu_percent > $THRESHOLD_CPU" | bc -l) )); then message="High CPU usage: ${cpu_percent}% (threshold: ${THRESHOLD_CPU}%)" echo "$timestamp - $message" >> "$LOG_FILE" echo "$message" | mail -s "CPU Alert" "$ALERT_EMAIL" fi # Check memory usage mem_used=$(free | awk '/Mem:/ {print int($3/$2 * 100)}') if [ "$mem_used" -gt "$THRESHOLD_MEM" ]; then message="High memory usage: ${mem_used}% (threshold: ${THRESHOLD_MEM}%)" echo "$timestamp - $message" >> "$LOG_FILE" echo "$message" | mail -s "Memory Alert" "$ALERT_EMAIL" fi # Check disk usage df -h | grep -E '^/dev/' | while read line; do usage=$(echo "$line" | awk '{print $5}' | sed 's/%//') mount=$(echo "$line" | awk '{print $6}') if [ "$usage" -gt "$THRESHOLD_DISK" ]; then message="High disk usage on $mount: ${usage}%" echo "$timestamp - $message" >> "$LOG_FILE" echo "$message" | mail -s "Disk Alert" "$ALERT_EMAIL" fi done # Check if critical services are running services=("ssh" "cron" "nginx") for service in "${services[@]}"; do if ! systemctl is-active --quiet "$service"; then message="Service $service is not running" echo "$timestamp - $message" >> "$LOG_FILE" echo "$message" | mail -s "Service Alert" "$ALERT_EMAIL" # Attempt to restart systemctl start "$service" if [ $? -eq 0 ]; then echo "$timestamp - Restarted $service successfully" >> "$LOG_FILE" fi fi done
Crontab for Monitoring
# Monitor every 5 minutes */5 * * * * /home/user/scripts/monitor.sh # Check disk space every hour 0 * * * * /home/user/scripts/check_disk.sh # Monitor services every minute * * * * * /home/user/scripts/check_services.sh # Generate daily report at 11:55 PM 55 23 * * * /home/user/scripts/daily_report.sh
4. Environment and Output Handling
Setting Environment Variables
# Set environment variables in crontab SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin [email protected] HOME=/home/user LOG_DIR=/var/log/myapp # Use variables in commands 0 2 * * * $HOME/scripts/backup.sh >> $LOG_DIR/backup.log 2>&1 # Example crontab with environment SHELL=/bin/bash PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin [email protected] HOME=/home/user # Daily backup with logging 0 2 * * * /home/user/scripts/backup.sh >> /var/log/backup.log 2>&1 # Weekly report 0 9 * * 1 /home/user/scripts/weekly_report.sh | mail -s "Weekly Report" [email protected]
Handling Output and Errors
# Redirect stdout and stderr to file 0 2 * * * /home/user/script.sh >> /var/log/script.log 2>&1 # Different files for stdout and stderr 0 2 * * * /home/user/script.sh >> /var/log/script.out 2>> /var/log/script.err # Suppress all output 0 2 * * * /home/user/script.sh > /dev/null 2>&1 # Send output by email (default if MAILTO is set) [email protected] 0 2 * * * /home/user/script.sh # Pipe output to another command 0 2 * * * /home/user/script.sh | logger -t myscript # Append timestamp to log 0 2 * * * echo "$(date): Starting backup" >> /var/log/backup.log && /home/user/backup.sh >> /var/log/backup.log 2>&1 # Rotate log files 0 0 * * * mv /var/log/script.log /var/log/script.log.$(date +\%Y\%m\%d) && touch /var/log/script.log
Using Logger for Syslog
# Log to syslog */5 * * * * /home/user/script.sh | logger -t myscript -p user.info # Log with different priorities 0 * * * * echo "Hourly check" | logger -t cron -p cron.info 0 2 * * * /home/user/backup.sh || logger -t cron -p cron.err "Backup failed" # View cron logs grep CRON /var/log/syslog journalctl -u cron -f
5. Advanced Crontab Techniques
Multiple Commands in One Line
# Run multiple commands (separate with && or ;) 0 2 * * * cd /home/user && ./backup.sh && echo "Backup complete" >> /var/log/backup.log # Run commands sequentially regardless of success 0 2 * * * command1; command2; command3 # Run only if previous succeeded 0 2 * * * command1 && command2 && command3 # Conditional execution with if 0 2 * * * if [ -f /tmp/lock ]; then exit; fi; /home/user/script.sh # Complex command chains 0 2 * * * /home/user/script.sh || /home/user/fallback.sh | mail -s "Alert" [email protected]
Using Conditional Execution
# Run only on weekdays
0 2 * * 1-5 /home/user/script.sh
# Run only on weekends
0 2 * * 0,6 /home/user/weekend_script.sh
# Run only if file exists
0 2 * * * [ -f /tmp/run ] && /home/user/script.sh
# Run only if not already running
0 2 * * * /usr/bin/flock -n /tmp/myscript.lock /home/user/script.sh
# Run only if load average is low
0 2 * * * [ $(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1 | cut -d. -f1) -lt 5 ] && /home/user/script.sh
# Check network connectivity before running
0 2 * * * ping -c 1 google.com &>/dev/null && /home/user/online_script.sh || /home/user/offline_script.sh
Preventing Overlapping Runs
# Using flock to prevent overlapping */5 * * * * /usr/bin/flock -n /tmp/myapp.lock /home/user/script.sh # Create PID file check 0 2 * * * /home/user/script_with_pid.sh # script_with_pid.sh contents: #!/bin/bash PIDFILE=/tmp/myscript.pid if [ -f "$PIDFILE" ] && kill -0 $(cat "$PIDFILE") 2>/dev/null; then echo "Script already running" exit 1 fi echo $$ > "$PIDFILE" # ... main script ... rm "$PIDFILE" # Using mkdir as atomic operation 0 2 * * * /home/user/script_with_lockdir.sh # script_with_lockdir.sh: #!/bin/bash LOCKDIR=/tmp/myscript.lock if mkdir "$LOCKDIR" 2>/dev/null; then trap "rm -rf '$LOCKDIR'" EXIT # ... main script ... else echo "Script already running" exit 1 fi
Dynamic Time Specifications
# Run at random minute to avoid contention
RANDOM_MINUTE=$((RANDOM % 60))
echo "$RANDOM_MINUTE 2 * * * /home/user/script.sh" | crontab -
# Run at calculated times
# Calculate next run time in script and schedule
#!/bin/bash
# dynamic_scheduler.sh
next_run=$(date -d "tomorrow 2am" +%M %H %d %m %w)
echo "$next_run /home/user/script.sh" | crontab -
# Use system load to determine if should run
0 2 * * * /home/user/conditional_runner.sh
# conditional_runner.sh:
#!/bin/bash
load=$(uptime | awk -F'load average:' '{print $2}' | cut -d, -f1)
if (( $(echo "$load < 2" | bc -l) )); then
/home/user/main_script.sh
else
echo "Load too high, skipping run" >> /var/log/skipped.log
fi
6. Managing Multiple Crontabs
User-Specific Crontabs
# Each user has their own crontab crontab -u user1 -l crontab -u user2 -e crontab -u user3 -r # System-wide crontab (/etc/crontab) sudo cat /etc/crontab # Format for /etc/crontab includes user field: # minute hour day month day-of-week user command 0 2 * * * root /usr/bin/backup.sh # Drop-in directories /etc/cron.d/ # System cron files /etc/cron.hourly/ # Scripts run hourly /etc/cron.daily/ # Scripts run daily /etc/cron.weekly/ # Scripts run weekly /etc/cron.monthly/ # Scripts run monthly
System Cron Directories
# Place scripts in these directories to run at intervals # Hourly scripts sudo cp myscript.sh /etc/cron.hourly/ sudo chmod +x /etc/cron.hourly/myscript.sh # Daily scripts sudo cp backup.sh /etc/cron.daily/ sudo chmod +x /etc/cron.daily/backup.sh # Weekly scripts sudo cp report.sh /etc/cron.weekly/ sudo chmod +x /etc/cron.weekly/report.sh # Monthly scripts sudo cp cleanup.sh /etc/cron.monthly/ sudo chmod +x /etc/cron.monthly/cleanup.sh # Note: Scripts in these directories should: # 1. Not require terminal input # 2. Handle errors gracefully # 3. Log output appropriately # 4. Be idempotent
Crontab Management Script
#!/bin/bash
# manage_crontab.sh - Manage multiple crontab configurations
CRONTAB_DIR="/home/user/crontabs"
mkdir -p "$CRONTAB_DIR"
# Function to backup current crontab
backup_crontab() {
local user="${1:-$USER}"
local backup_file="$CRONTAB_DIR/${user}_crontab_$(date +%Y%m%d_%H%M%S).txt"
if crontab -u "$user" -l &>/dev/null; then
crontab -u "$user" -l > "$backup_file"
echo "Backed up crontab for $user to $backup_file"
else
echo "No crontab for $user"
fi
}
# Function to load crontab from file
load_crontab() {
local file="$1"
local user="${2:-$USER}"
if [ ! -f "$file" ]; then
echo "File not found: $file"
return 1
fi
crontab -u "$user" "$file"
echo "Loaded crontab for $user from $file"
}
# Function to list all user crontabs
list_all_crontabs() {
echo "=== User Crontabs ==="
for user in $(cut -d: -f1 /etc/passwd); do
if crontab -u "$user" -l &>/dev/null; then
echo "--- $user ---"
crontab -u "$user" -l | head -5
echo "..."
fi
done
}
# Function to enable/disable cron jobs
toggle_job() {
local pattern="$1"
local action="${2:-disable}" # disable or enable
crontab -l | while read line; do
if [[ "$line" =~ $pattern && "$action" == "disable" ]]; then
echo "# DISABLED: $line"
elif [[ "$line" =~ ^#.*DISABLED.*$pattern && "$action" == "enable" ]]; then
echo "${line#*DISABLED: }"
else
echo "$line"
fi
done | crontab -
echo "Jobs matching '$pattern' ${action}d"
}
# Function to validate crontab syntax
validate_crontab() {
local file="${1:-/dev/stdin}"
if crontab -l 2>/dev/null | crontab - 2>&1; then
echo "✓ Crontab syntax is valid"
else
echo "✗ Crontab syntax error"
fi
}
# Function to show next run times
next_runs() {
local count="${1:-5}"
crontab -l | grep -v '^#' | while read line; do
if [ -n "$line" ]; then
echo "Next run for: $line"
echo " $(echo "$line" | awk '{print $1" "$2" "$3" "$4" "$5}')"
fi
done
}
# Menu
case "${1:-}" in
backup)
backup_crontab "${2:-$USER}"
;;
load)
load_crontab "$2" "${3:-$USER}"
;;
list)
list_all_crontabs
;;
toggle)
toggle_job "$2" "${3:-disable}"
;;
validate)
validate_crontab
;;
next)
next_runs "${2:-5}"
;;
*)
echo "Usage: $0 {backup|load|list|toggle|validate|next}"
;;
esac
7. Cron Job Best Practices
Script Best Practices
#!/bin/bash
# Example of a well-behaved cron script
# 1. Use full paths
BACKUP_SCRIPT="/home/user/scripts/backup.sh"
LOG_FILE="/var/log/backup.log"
# 2. Set environment
export PATH=/usr/local/bin:/usr/bin:/bin
export HOME=/home/user
# 3. Use locking to prevent overlap
LOCKFILE="/tmp/$(basename "$0").lock"
if [ -f "$LOCKFILE" ] && kill -0 $(cat "$LOCKFILE") 2>/dev/null; then
echo "$(date): Script already running" >> "$LOG_FILE"
exit 1
fi
echo $$ > "$LOCKFILE"
trap "rm -f '$LOCKFILE'" EXIT
# 4. Handle errors gracefully
error_handler() {
echo "$(date): Error on line $1" >> "$LOG_FILE"
exit 1
}
trap 'error_handler $LINENO' ERR
# 5. Log everything
log() {
echo "$(date): $1" >> "$LOG_FILE"
}
# 6. Check prerequisites
check_prereqs() {
for cmd in mysqldump gzip tar; do
if ! command -v "$cmd" &>/dev/null; then
log "ERROR: $cmd not found"
exit 1
fi
done
}
# 7. Main script logic
main() {
log "Starting backup"
# Your backup logic here
if /usr/bin/mysqldump -u root mydb | gzip > "/backup/mydb_$(date +%Y%m%d).sql.gz"; then
log "Backup successful"
else
log "Backup failed"
exit 1
fi
log "Backup completed"
}
# 8. Run main function
check_prereqs
main
Logging and Monitoring
# Create a logging wrapper for cron jobs
#!/bin/bash
# cron_wrapper.sh - Wrapper script for cron jobs
JOB_NAME=$(basename "$1")
LOG_DIR="/var/log/cron"
LOG_FILE="$LOG_DIR/${JOB_NAME}_$(date +%Y%m%d).log"
PID_FILE="/tmp/${JOB_NAME}.pid"
# Create log directory
mkdir -p "$LOG_DIR"
# Log function
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# Check if already running
if [ -f "$PID_FILE" ] && kill -0 $(cat "$PID_FILE") 2>/dev/null; then
log "Job already running with PID $(cat "$PID_FILE")"
exit 1
fi
echo $$ > "$PID_FILE"
trap "rm -f '$PID_FILE'" EXIT
log "Starting job: $@"
# Execute the actual job
"$@" >> "$LOG_FILE" 2>&1
JOB_EXIT=$?
if [ $JOB_EXIT -eq 0 ]; then
log "Job completed successfully"
else
log "Job failed with exit code $JOB_EXIT"
# Send alert on failure
if [ -n "$ALERT_EMAIL" ]; then
tail -50 "$LOG_FILE" | mail -s "Cron job failed: $JOB_NAME" "$ALERT_EMAIL"
fi
fi
exit $JOB_EXIT
Error Handling and Notifications
#!/bin/bash
# error_handler.sh - Robust error handling for cron jobs
# Configuration
SCRIPT_NAME=$(basename "$0")
LOG_FILE="/var/log/${SCRIPT_NAME%.sh}.log"
ALERT_EMAIL="[email protected]"
MAX_RETRIES=3
RETRY_DELAY=60
# Logging
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
echo "$1"
}
# Error handling
handle_error() {
local exit_code=$1
local error_msg=$2
log "ERROR: $error_msg (exit code: $exit_code)"
# Send alert email
{
echo "Cron job failed: $SCRIPT_NAME"
echo "Exit code: $exit_code"
echo "Error: $error_msg"
echo "Time: $(date)"
echo ""
echo "Last 50 lines of log:"
tail -50 "$LOG_FILE"
} | mail -s "Cron Alert: $SCRIPT_NAME" "$ALERT_EMAIL"
# Optional: notify via other channels
# curl -X POST https://api.telegram.org/bot$TOKEN/sendMessage -d "chat_id=$CHAT_ID&text=$error_msg"
}
# Retry function
retry() {
local n=1
local cmd="$@"
until eval "$cmd"; do
log "Command failed (attempt $n/$MAX_RETRIES): $cmd"
if [ $n -lt $MAX_RETRIES ]; then
log "Retrying in $RETRY_DELAY seconds..."
sleep $RETRY_DELAY
((n++))
else
handle_error $? "Command failed after $MAX_RETRIES attempts: $cmd"
return 1
fi
done
log "Command succeeded: $cmd"
return 0
}
# Main execution with error handling
main() {
log "Starting $SCRIPT_NAME"
# Check dependencies
for cmd in curl wget mysql; do
if ! command -v "$cmd" &>/dev/null; then
handle_error 1 "Required command not found: $cmd"
exit 1
fi
done
# Example: retry a database backup
if retry mysqldump -u backup mydb > /tmp/backup.sql; then
log "Database backup successful"
else
log "Database backup failed after retries"
exit 1
fi
# Example: upload with retry
if retry curl -T /tmp/backup.sql ftp://backup-server/; then
log "Upload successful"
else
log "Upload failed after retries"
exit 1
fi
log "$SCRIPT_NAME completed successfully"
}
# Run main with trap
trap 'handle_error $? "Script interrupted"' INT TERM
main
8. Testing and Debugging Cron Jobs
Testing Cron Syntax
# Check crontab syntax crontab -l | crontab - 2>&1 # Test run a cron job manually /home/user/scripts/test.sh # Simulate cron environment env -i SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin HOME=/home/user /home/user/script.sh # Test with specific time # Use date command to set time for testing sudo date -s "2024-01-01 02:00:00" # Run job # Set date back sudo ntpdate -u pool.ntp.org # Use run-parts to test system cron directories run-parts --test /etc/cron.daily
Debugging Tools
# Check cron logs grep CRON /var/log/syslog tail -f /var/log/syslog | grep CRON # Check if cron service is running systemctl status cron service cron status ps aux | grep cron # Check mail for cron output mail # View mail for specific user sudo cat /var/mail/$USER # Test individual commands # Run with full logging /home/user/script.sh >> /tmp/debug.log 2>&1 # Use logger for debugging echo "Debug: Starting job" | logger -t cron-debug # Check environment variables env | sort > /tmp/env_before.txt # Run job env | sort > /tmp/env_after.txt diff /tmp/env_before.txt /tmp/env_after.txt
Debugging Script
#!/bin/bash
# debug_cron.sh - Debug cron job issues
CRON_LOG="/var/log/syslog"
USER=${1:-$USER}
echo "=== Cron Debug Tool ==="
echo "User: $USER"
echo
# Check if cron is running
echo "1. Cron service status:"
if systemctl is-active --quiet cron 2>/dev/null; then
echo " ✓ Cron is running"
else
echo " ✗ Cron is NOT running"
fi
echo
# Check user's crontab
echo "2. User crontab:"
if crontab -l &>/dev/null; then
crontab -l | cat -n
else
echo " No crontab for user"
fi
echo
# Check recent cron activity
echo "3. Recent cron activity (last 10 entries):"
sudo grep "CRON" "$CRON_LOG" 2>/dev/null | tail -10
echo
# Check for errors
echo "4. Recent cron errors:"
sudo grep -i "error\|fail" "$CRON_LOG" 2>/dev/null | grep CRON | tail -5
echo
# Check mail
echo "5. Pending mail:"
if [ -f "/var/mail/$USER" ]; then
ls -l "/var/mail/$USER"
echo " Mail size: $(du -h /var/mail/$USER | cut -f1)"
else
echo " No mail file"
fi
echo
# Test environment
echo "6. Cron environment simulation:"
env -i SHELL=/bin/bash PATH=/usr/local/bin:/usr/bin:/bin HOME="/home/$USER" bash -c 'echo " PATH=$PATH"'
echo
# Test execute permissions
echo "7. Checking script permissions:"
crontab -l | grep -v '^#' | grep -E '^[^ ]' | while read line; do
cmd=$(echo "$line" | awk '{for(i=6;i<=NF;i++) printf "%s ", $i}')
script=$(echo "$cmd" | awk '{print $1}')
if [ -f "$script" ]; then
if [ -x "$script" ]; then
echo " ✓ $script is executable"
else
echo " ✗ $script is NOT executable"
fi
else
echo " ? $script not found"
fi
done
9. Security Considerations
Securing Crontab Access
# Restrict crontab access to specific users # /etc/cron.allow - users allowed to use crontab # /etc/cron.deny - users denied from using crontab # Example: Only allow root and backup user echo "root" >> /etc/cron.allow echo "backup" >> /etc/cron.allow echo "ALL" >> /etc/cron.deny # Deny all others # Secure permissions chmod 600 /etc/cron.allow chmod 600 /etc/cron.deny chmod 600 /var/spool/cron/* # Protect crontab files # Run cron jobs with minimal privileges # Create dedicated user for cron jobs sudo useradd -r -s /bin/false cronjobs sudo -u cronjobs crontab -e # Use sudo for specific tasks # In crontab: 0 2 * * * sudo -u backup /usr/local/bin/backup.sh
Safe Practices
# 1. Don't run as root unless necessary
# Bad
0 2 * * * /home/user/script.sh
# Good
0 2 * * * sudo -u user /home/user/script.sh
# 2. Use absolute paths
# Bad
0 2 * * * backup.sh
# Good
0 2 * * * /home/user/scripts/backup.sh
# 3. Validate input in scripts
#!/bin/bash
# Safe script example
if [ -z "$1" ]; then
echo "Usage: $0 <argument>"
exit 1
fi
# 4. Avoid using environment variables that could be manipulated
# Bad
0 2 * * * $HOME/script.sh
# Good
0 2 * * * /home/user/script.sh
# 5. Use locking to prevent resource exhaustion
0 2 * * * /usr/bin/flock -n /tmp/myscript.lock /home/user/script.sh
# 6. Log all actions
0 2 * * * /home/user/script.sh >> /var/log/script.log 2>&1
# 7. Set umask for secure file creation
0 2 * * * umask 077 && /home/user/script.sh
# 8. Monitor for unusual cron jobs
find /var/spool/cron -type f -exec ls -l {} \;
grep -r "curl\|wget\|nc\|bash -i" /var/spool/cron/
10. Advanced Examples
Complex Scheduling Patterns
# Run on specific days of month # 1st and 15th at 2 AM 0 2 1,15 * * /home/user/script.sh # Run on last day of month (using date command) 55 23 28-31 * * [ $(date -d tomorrow +%d) -eq 1 ] && /home/user/script.sh # Run every weekday except holidays 0 9 * * 1-5 [ ! -f /etc/holidays/$(date +%Y%m%d) ] && /home/user/script.sh # Run at specific times with offset # Every 2 hours, starting at 1 AM 1 */2 * * * /home/user/script.sh # Run every 3 hours between 9 AM and 6 PM 0 9-18/3 * * * /home/user/script.sh # Run twice daily at random minutes RANDOM1=$((RANDOM % 60)) RANDOM2=$((RANDOM % 60)) echo "$RANDOM1 9 * * * /home/user/morning.sh" | crontab - echo "$RANDOM2 17 * * * /home/user/evening.sh" | crontab -
Weather-Aware Scheduling
#!/bin/bash
# weather_aware.sh - Run based on weather conditions
# Get weather data (example using wttr.in)
weather=$(curl -s "wttr.in?format=%t+%w+%p")
temp=$(echo "$weather" | awk '{print $1}' | sed 's/[^0-9-]//g')
wind=$(echo "$weather" | awk '{print $2}' | sed 's/[^0-9]//g')
precip=$(echo "$weather" | awk '{print $3}' | sed 's/[^0-9]//g')
LOG_FILE="/var/log/weather_job.log"
log() {
echo "$(date): $1" >> "$LOG_FILE"
}
# Decision logic
if [ "$precip" -gt 0 ] && [ "$precip" -lt 30 ]; then
log "Light rain detected, running job"
/home/user/rain_job.sh
elif [ "$wind" -gt 20 ]; then
log "High winds, delaying job"
# Reschedule for later
echo "$((RANDOM % 60)) $((RANDOM % 23)) * * * /home/user/weather_aware.sh" | crontab -
elif [ "$temp" -lt 0 ]; then
log "Freezing temperature, running cold-weather job"
/home/user/cold_job.sh
else
log "Normal conditions, running regular job"
/home/user/regular_job.sh
fi
Load-Aware Scheduling
#!/bin/bash
# load_aware.sh - Run based on system load
LOAD_THRESHOLD=2.0
MAX_RETRIES=5
RETRY_DELAY=300 # 5 minutes
get_load() {
uptime | awk -F'load average:' '{print $2}' | cut -d, -f1 | xargs
}
run_with_load_check() {
local cmd="$@"
local attempt=1
while [ $attempt -le $MAX_RETRIES ]; do
load=$(get_load)
if (( $(echo "$load < $LOAD_THRESHOLD" | bc -l) )); then
echo "$(date): Load $load below threshold, running command"
eval "$cmd"
return $?
else
echo "$(date): Load $load above threshold, attempt $attempt/$MAX_RETRIES"
sleep $RETRY_DELAY
((attempt++))
fi
done
echo "$(date): Maximum retries reached, command skipped"
return 1
}
# Usage
run_with_load_check /home/user/heavy_job.sh
Chained Jobs with Dependencies
#!/bin/bash
# job_chain.sh - Chain multiple jobs with dependencies
JOB_DIR="/home/user/jobs"
LOG_DIR="/var/log/job_chain"
LOCK_DIR="/tmp/job_chain.lock"
mkdir -p "$LOG_DIR" "$LOCK_DIR"
run_job() {
local job="$1"
local log="$LOG_DIR/$(basename "$job")_$(date +%Y%m%d).log"
echo "$(date): Starting $job" >> "$log"
if bash "$job" >> "$log" 2>&1; then
echo "$(date): Job $job completed" >> "$log"
touch "$LOCK_DIR/$(basename "$job").done"
return 0
else
echo "$(date): Job $job failed" >> "$log"
return 1
fi
}
# Define job dependencies
jobs=(
"01_prepare.sh"
"02_process.sh:01_prepare.sh.done"
"03_analyze.sh:02_process.sh.done"
"04_cleanup.sh:03_analyze.sh.done"
)
for job_spec in "${jobs[@]}"; do
job=$(echo "$job_spec" | cut -d: -f1)
dep=$(echo "$job_spec" | cut -d: -f2-)
# Check dependency
if [ -n "$dep" ] && [ ! -f "$LOCK_DIR/$dep" ]; then
echo "Skipping $job, dependency $dep not satisfied"
continue
fi
# Run job with exclusive lock
(
flock -n 9 || exit 1
run_job "$JOB_DIR/$job"
) 9>"$LOCK_DIR/$(basename "$job").lock"
if [ $? -ne 0 ]; then
echo "Job chain failed at $job"
exit 1
fi
done
echo "Job chain completed successfully"
11. Monitoring and Alerting
Cron Job Monitoring System
#!/bin/bash # cron_monitor.sh - Monitor cron jobs and send alerts CONFIG_FILE="/etc/cron_monitor.conf" ALERT_EMAIL="[email protected]" CHECK_INTERVAL=3600 # 1 hour # Load configuration [ -f "$CONFIG_FILE" ] && source "$CONFIG_FILE" # Check if a job is running too long check_job_duration() { local job_pattern="$1" local max_minutes="$2" pgrep -f "$job_pattern" | while read pid; do start_time=$(ps -o lstart= -p $pid) start_seconds=$(date -d "$start_time" +%s) now_seconds=$(date +%s) duration=$(( (now_seconds - start_seconds) / 60 )) if [ $duration -gt $max_minutes ]; then echo "Job $job_pattern (PID $pid) running for ${duration} minutes (max: $max_minutes)" return 1 fi done return 0 } # Check if job ran within expected time window check_job_timing() { local job_name="$1" local expected_time="$2" local log_file="$3" last_run=$(grep "$job_name" "$log_file" | tail -1 | cut -d' ' -f1-2) last_seconds=$(date -d "$last_run" +%s 2>/dev/null) now_seconds=$(date +%s) if [ -n "$last_seconds" ]; then diff_hours=$(( (now_seconds - last_seconds) / 3600 )) if [ $diff_hours -gt $expected_time ]; then echo "Job $job_name last ran $diff_hours hours ago (expected within $expected_time hours)" return 1 fi else echo "Job $job_name never ran" return 1 fi return 0 } # Send alert send_alert() { local subject="$1" local message="$2" echo "$message" | mail -s "$subject" "$ALERT_EMAIL" # Optional: Send to Slack if [ -n "$SLACK_WEBHOOK" ]; then curl -X POST -H 'Content-type: application/json' \ --data "{\"text\":\"$subject\n$message\"}" \ "$SLACK_WEBHOOK" fi # Optional: Send to Telegram if [ -n "$TELEGRAM_BOT" ] && [ -n "$TELEGRAM_CHAT" ]; then curl -s "https://api.telegram.org/bot$TELEGRAM_BOT/sendMessage" \ -d "chat_id=$TELEGRAM_CHAT" \ -d "text=$subject\n$message" fi } # Main monitoring loop while true; do alerts="" # Check each configured job while IFS= read -r line; do [[ "$line" =~ ^#.*$ || -z "$line" ]] && continue job_pattern=$(echo "$line" | cut -d'|' -f1) max_duration=$(echo "$line" | cut -d'|' -f2) expected_interval=$(echo "$line" | cut -d'|' -f3) log_file=$(echo "$line" | cut -d'|' -f4) # Check duration if ! check_job_duration "$job_pattern" "$max_duration"; then alerts+="Job $job_pattern running too long\n" fi # Check timing if [ -n "$expected_interval" ] && [ -n "$log_file" ]; then if ! check_job_timing "$job_pattern" "$expected_interval" "$log_file"; then alerts+="Job $job_pattern not running on schedule\n" fi fi done < "$CONFIG_FILE" # Send alerts if any if [ -n "$alerts" ]; then send_alert "Cron Monitor Alert" "$alerts" fi sleep $CHECK_INTERVAL done
Configuration File Example
# /etc/cron_monitor.conf # Format: job_pattern|max_duration_minutes|expected_interval_hours|log_file # Database backup - should run within 30 minutes, expected every 24 hours mysqldump|30|24|/var/log/backup.log # Log rotation - should complete in 10 minutes, expected every 24 hours logrotate|10|24|/var/log/logrotate.log # System update - can take up to 60 minutes, expected weekly (168 hours) apt-get|60|168|/var/log/update.log # Monitoring job - should run continuously monitor.sh|60||
12. Crontab Cheat Sheet
Quick Reference
# Field Format # ┌──────── minute (0-59) # │ ┌────── hour (0-23) # │ │ ┌──── day of month (1-31) # │ │ │ ┌── month (1-12) # │ │ │ │ ┌ day of week (0-6 or 0-7, 0/7 = Sunday) # │ │ │ │ │ # * * * * * command # Common Examples * * * * * command # Every minute */5 * * * * command # Every 5 minutes 0 * * * * command # Every hour 0 0 * * * command # Daily at midnight 0 2 * * * command # Daily at 2 AM 0 0 * * 0 command # Weekly on Sunday 0 0 1 * * command # Monthly on 1st 0 0 1 1 * command # Yearly on Jan 1st # Ranges and Lists 0 9-17 * * * command # Every hour 9 AM - 5 PM 0 9,13,17 * * * command # At 9 AM, 1 PM, 5 PM 15,30,45 * * * * command # At 15, 30, 45 minutes 0 */2 * * * command # Every 2 hours # Special Keywords @reboot # At startup @yearly # 0 0 1 1 * @monthly # 0 0 1 * * @weekly # 0 0 * * 0 @daily # 0 0 * * * @midnight # 0 0 * * * @hourly # 0 * * * * # Common crontab commands crontab -l # List current crontab crontab -e # Edit current crontab crontab -r # Remove current crontab crontab -u user -l # List user's crontab crontab file.txt # Load crontab from file
Quick Setup Examples
# Daily backup at 2 AM with logging echo "0 2 * * * /home/user/backup.sh >> /var/log/backup.log 2>&1" | crontab - # Multiple jobs cat << EOF | crontab - # Database backup at 1 AM 0 1 * * * /home/user/db_backup.sh # Log rotation at 2 AM 0 2 * * * /home/user/rotate_logs.sh # Weekly report on Monday at 9 AM 0 9 * * 1 /home/user/weekly_report.sh # Monitoring every 5 minutes */5 * * * * /home/user/monitor.sh EOF # Conditional job 0 2 * * * [ $(date +\%u) -le 5 ] && /home/user/weekday_job.sh # Job with flock to prevent overlap */5 * * * * /usr/bin/flock -n /tmp/mylock /home/user/script.sh
Conclusion
Crontab is an essential tool for automating tasks in Unix/Linux systems:
Key Takeaways
- Simple Syntax: Five time fields followed by command
- Flexible Scheduling: Ranges, lists, steps, and special keywords
- User-Specific: Each user can have their own crontab
- System-Wide: /etc/crontab and cron directories for system tasks
- Environment: Cron provides limited environment, set explicitly
- Logging: Always redirect output for debugging
- Security: Use least privilege, validate inputs, prevent overlaps
Best Practices Summary
| Area | Best Practice |
|---|---|
| Paths | Use absolute paths for all commands and files |
| Logging | Redirect stdout/stderr to log files |
| Environment | Set PATH and other variables explicitly |
| Testing | Test commands manually before adding to crontab |
| Error Handling | Check exit codes and handle failures |
| Locking | Prevent overlapping runs with flock |
| Permissions | Run jobs with minimal privileges |
| Monitoring | Monitor cron logs and job execution |
| Documentation | Comment complex crontab entries |
| Backup | Regularly backup crontab configurations |
Common Issues and Solutions
| Issue | Solution |
|---|---|
| Job not running | Check cron service, syntax, permissions |
| PATH issues | Set full PATH in crontab |
| Script fails | Test manually with same environment |
| Overlapping runs | Use flock or PID files |
| No output | Check MAILTO or redirect to file |
| Wrong time | Verify system timezone |
| Permission denied | Check script executable bits |
| Command not found | Use full paths |
Mastering crontab is essential for system administration and automation. Start with simple schedules and gradually incorporate more complex patterns as needed.