Automate Anything with Linux Cron Jobs — A Practical Guide
You SSH into the server every morning to rotate logs. You manually run backups every Friday. You check disk space by hand when you remember. Stop. Every repetitive task on a Linux server should be automated. Cron jobs are the oldest, simplest, and most reliable way to do it.
Crontab Syntax — The Five Fields
Every cron expression has five time fields followed by the command to run:
┌───────────── minute (0-59)
│ ┌───────────── hour (0-23)
│ │ ┌───────────── day of month (1-31)
│ │ │ ┌───────────── month (1-12)
│ │ │ │ ┌───────────── day of week (0-7, 0 and 7 = Sunday)
│ │ │ │ │
* * * * * command_to_run
| Symbol | Meaning | Example |
|---|---|---|
* | Every | * * * * * = every minute |
, | Multiple values | 1,15,30 * * * * = minute 1, 15, and 30 |
- | Range | 0 9-17 * * * = every hour from 9 AM to 5 PM |
/ | Step | */5 * * * * = every 5 minutes |
@reboot | On system startup | @reboot /opt/start.sh |
# Common cron schedules — memorize these
*/5 * * * * # Every 5 minutes
0 * * * * # Every hour (at minute 0)
0 */6 * * * # Every 6 hours
0 0 * * * # Every day at midnight
0 0 * * 0 # Every Sunday at midnight
0 0 1 * * # First day of every month at midnight
0 0 1 1 * # January 1st at midnight (yearly)
Managing Crontabs
# Edit your crontab
crontab -e
# View your current crontab
crontab -l
# Edit crontab for another user (needs root)
sudo crontab -u deploy -e
sudo crontab -u deploy -l
# Remove your entire crontab (careful!)
crontab -r
# System-wide cron jobs (different format — includes username field)
sudo cat /etc/crontab
# Drop-in directories for system cron jobs
ls /etc/cron.d/ # Custom schedule
ls /etc/cron.hourly/ # Runs every hour
ls /etc/cron.daily/ # Runs every day
ls /etc/cron.weekly/ # Runs every week
ls /etc/cron.monthly/ # Runs every month
Scripts in /etc/cron.daily/ and similar directories run automatically. Just drop an executable script in there — no crontab syntax needed.
Real-World Cron Jobs
1. Database Backup
# Daily PostgreSQL backup at 2 AM, keep 7 days
0 2 * * * /usr/local/bin/backup-db.sh >> /var/log/backup-db.log 2>&1
The backup script:
#!/bin/bash
# /usr/local/bin/backup-db.sh
set -euo pipefail
BACKUP_DIR="/backup/postgres"
TIMESTAMP=$(date +\%Y\%m\%d_\%H\%M\%S)
DB_NAME="production"
echo "[$(date)] Starting backup..."
# Create backup
pg_dump -U postgres -Fc "$DB_NAME" > "${BACKUP_DIR}/${DB_NAME}_${TIMESTAMP}.dump"
# Delete backups older than 7 days
find "$BACKUP_DIR" -name "*.dump" -mtime +7 -delete
echo "[$(date)] Backup completed: ${DB_NAME}_${TIMESTAMP}.dump"
echo "[$(date)] Remaining backups: $(ls ${BACKUP_DIR}/*.dump | wc -l)"
2. Disk Space Alert
# Check disk space every 30 minutes
*/30 * * * * /usr/local/bin/check-disk.sh
#!/bin/bash
# /usr/local/bin/check-disk.sh
THRESHOLD=80
HOSTNAME=$(hostname)
df -H | awk -v threshold="$THRESHOLD" -v host="$HOSTNAME" '
NR>1 && +$5 >= threshold {
printf "ALERT: %s — %s is %s full (%s used of %s)\n", host, $6, $5, $3, $2
}
' | while read -r alert; do
echo "$alert" | mail -s "Disk Space Alert on $HOSTNAME" ops-team@example.com
logger -t disk-check "$alert"
done
3. Health Check with Alerting
# Check app health every minute
* * * * * /usr/local/bin/healthcheck.sh >> /var/log/healthcheck.log 2>&1
#!/bin/bash
# /usr/local/bin/healthcheck.sh
URL="http://localhost:8080/health"
TIMEOUT=5
HTTP_CODE=$(curl -o /dev/null -s -w "%{http_code}" --max-time $TIMEOUT "$URL" || echo "000")
if [ "$HTTP_CODE" != "200" ]; then
echo "[$(date)] UNHEALTHY — HTTP $HTTP_CODE from $URL"
# Optionally restart the service
# sudo systemctl restart myapp
else
echo "[$(date)] OK — HTTP $HTTP_CODE"
fi
4. Log Rotation and Cleanup
# Clean old logs every day at 3 AM
0 3 * * * find /var/log/myapp/ -name "*.log" -mtime +30 -delete
# Compress logs older than 1 day
0 4 * * * find /var/log/myapp/ -name "*.log" -mtime +1 ! -name "*.gz" -exec gzip {} \;
5. SSL Certificate Expiry Check
# Weekly SSL check on Monday at 9 AM
0 9 * * 1 /usr/local/bin/check-ssl.sh
#!/bin/bash
# /usr/local/bin/check-ssl.sh
DOMAINS=("goelacademy.com" "api.goelacademy.com")
WARN_DAYS=30
for domain in "${DOMAINS[@]}"; do
expiry_date=$(echo | openssl s_client -connect "${domain}:443" -servername "$domain" 2>/dev/null \
| openssl x509 -noout -enddate 2>/dev/null | cut -d= -f2)
if [ -n "$expiry_date" ]; then
expiry_epoch=$(date -d "$expiry_date" +%s)
now_epoch=$(date +%s)
days_left=$(( (expiry_epoch - now_epoch) / 86400 ))
if [ "$days_left" -lt "$WARN_DAYS" ]; then
echo "WARNING: $domain SSL expires in $days_left days ($expiry_date)"
else
echo "OK: $domain SSL expires in $days_left days"
fi
else
echo "ERROR: Could not check SSL for $domain"
fi
done
Common Cron Pitfalls (and How to Avoid Them)
Pitfall 1: Environment Variables
Cron runs with a minimal environment. Your script works in terminal but fails in cron because PATH, HOME, and other variables are missing.
# WRONG — depends on user's PATH
0 * * * * backup.sh
# RIGHT — use full paths
0 * * * * /usr/local/bin/backup.sh
# Or set PATH in your crontab
PATH=/usr/local/bin:/usr/bin:/bin
0 * * * * backup.sh
# Inside scripts, set the full PATH explicitly
#!/bin/bash
export PATH="/usr/local/bin:/usr/bin:/bin"
Pitfall 2: No Output Capture
If your cron job produces output and you don't redirect it, cron sends it as email (or it vanishes).
# WRONG — output goes to email or nowhere
0 2 * * * /usr/local/bin/backup.sh
# RIGHT — capture both stdout and stderr
0 2 * * * /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# Suppress all output (only if you truly don't care)
0 2 * * * /usr/local/bin/backup.sh > /dev/null 2>&1
Pitfall 3: Overlapping Runs
A job that takes 10 minutes is scheduled every 5 minutes. Two instances run simultaneously and corrupt data.
# Use flock to prevent overlapping
*/5 * * * * flock -n /tmp/backup.lock /usr/local/bin/backup.sh >> /var/log/backup.log 2>&1
# -n = non-blocking: skip this run if lock is held
Pitfall 4: Percent Signs
In crontab, % is a special character (newline). You must escape it.
# WRONG — % breaks the command
0 2 * * * echo "Backup on $(date +%Y-%m-%d)"
# RIGHT — escape percent signs
0 2 * * * echo "Backup on $(date +\%Y-\%m-\%d)"
systemd Timers — The Modern Alternative
systemd timers are more powerful than cron: they support randomized delays, dependency management, proper logging (journalctl), and are easier to monitor.
# Create a timer unit: /etc/systemd/system/backup.timer
cat << 'EOF' | sudo tee /etc/systemd/system/backup.timer
[Unit]
Description=Daily database backup timer
[Timer]
OnCalendar=*-*-* 02:00:00
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Create the corresponding service unit: /etc/systemd/system/backup.service
cat << 'EOF' | sudo tee /etc/systemd/system/backup.service
[Unit]
Description=Database backup
After=postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/local/bin/backup-db.sh
StandardOutput=journal
StandardError=journal
EOF
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
# Check timer status
systemctl list-timers --all
systemctl status backup.timer
# View logs for the service
journalctl -u backup.service --since today
| Feature | Cron | systemd Timer |
|---|---|---|
| Setup complexity | Simple | More files |
| Logging | Manual (redirect) | Automatic (journalctl) |
| Missed runs | Lost | Persistent=true catches up |
| Random delay | Not built-in | RandomizedDelaySec |
| Dependencies | None | After=, Requires= |
| Resource limits | None | MemoryLimit, CPUQuota |
| Monitoring | crontab -l | systemctl list-timers |
The at Command — One-Time Scheduling
Need to run something once at a specific time? Use at instead of cron.
# Install at
sudo apt install at
sudo systemctl enable --now atd
# Schedule a command for later
echo "/usr/local/bin/deploy.sh" | at 02:00
echo "sudo reboot" | at now + 2 hours
echo "/opt/cleanup.sh" | at midnight
# List scheduled jobs
atq
# View a specific job's commands
at -c 5
# Remove a scheduled job
atrm 5
Debugging Cron Jobs
# Check if cron is running
systemctl status cron # Debian/Ubuntu
systemctl status crond # RHEL/CentOS
# View cron logs
grep CRON /var/log/syslog # Debian/Ubuntu
grep CRON /var/log/cron # RHEL/CentOS
journalctl -u cron --since "1 hour ago"
# Test your cron command manually first
# Run it exactly as cron would:
env -i /bin/bash -c '/usr/local/bin/backup.sh'
Next up: SSH Mastery — Keys, Tunnels, Config and Security — still typing passwords? Here's how to SSH like a pro.
