Table of Contents
Why Use cron for Scheduled Tasks?
cron is the classic Unix tool for running commands on a schedule. For shell scripters, it’s what you use when you want:
- A backup script to run every night.
- Log cleanup to run weekly.
- A monitoring script to run every 5 minutes.
- Reports to be emailed once a day.
This chapter focuses on how to use cron correctly with shell scripts, and how to avoid the common pitfalls.
cron vs at vs systemd timers (briefly)
In many modern systems, systemd timers are an alternative; another chapter may cover those. Here we focus on cron, but you should be aware:
cron: repetitive schedules (every X minutes/hours/days).at: one-time future execution.systemdtimers: more advanced features, dependency-aware.
On most distributions, cron still exists and is widely used, especially for per-user schedules and legacy scripts.
cron Basics: Daemon and Crontab Files
cron runs as a background service (crond, cronie, or similar). It reads configuration from:
- System-wide crontab:
/etc/crontab - System cron directories:
/etc/cron.hourly/,/etc/cron.daily/, etc. - Per-user crontabs (managed via
crontabcommand)
As a script author, you mainly work with user crontabs and occasionally /etc/crontab.
crontab: Managing Per-User Schedules
Use the crontab command (note: not the same as the file /etc/crontab):
crontab -e– edit your personal crontabcrontab -l– list your personal entriescrontab -r– remove your crontab (careful: no undo by default)
Each user can have their own crontab, and jobs run as that user.
Example to edit your crontab:
crontab -eThe first time, you may be asked which editor to use.
The cron Time Format
Each line in a crontab (ignoring comments and environment lines) looks like:
MIN HOUR DOM MON DOW COMMANDWhere:
MIN– minute (0–59)HOUR– hour (0–23)DOM– day of month (1–31)MON– month (1–12 or names like jan, feb)DOW– day of week (0–7, Sunday = 0 or 7, or names like sun, mon)COMMAND– shell command or script to execute
Example: run a script every day at 02:30:
30 2 * * * /home/alex/bin/backup.shCommon syntax elements:
*– any valid value (wildcard)A-B– range (e.g.1-5= Mon–Fri if in DOW)A,B,C– list (e.g.1,15= 1st and 15th of month)/N– step (e.g./5in MIN = every 5 minutes)
Examples:
- Every 5 minutes:
*/5 * * * * /path/to/script.sh- Weekdays at 09:00:
0 9 * * 1-5 /path/to/script.sh- On the 1st of January at midnight:
0 0 1 1 * /path/to/script.shNamed months and weekdays
Instead of numbers, you can use short names (case-insensitive):
- Months:
jan,feb,mar, … - Days:
sun,mon,tue,wed,thu,fri,sat
Example:
0 7 * * mon-fri /path/to/workday-report.shSpecial strings (@reboot, @daily, …)
Many cron implementations support shortcuts:
@reboot– once at boot@yearlyor@annually–0 0 1 1 *@monthly–0 0 1@weekly–0 0 0@daily–0 0 *@hourly–0
Example:
@reboot /home/alex/bin/startup-tasks.sh
@daily /home/alex/bin/rotate-logs.shEnvironment Differences in cron
A crucial point for shell scripts: cron does not run with your interactive shell environment. This is the source of many “it works in the terminal but not in cron” issues.
Key differences:
- Default shell is usually
/bin/shunless changed in the crontab. PATHis very minimal (often just/usr/bin:/binor similar).- No interactive features, no
~/.bashrcor~/.profileloading (for user crontabs). - Working directory is usually your home directory for user crontabs, but you should not rely on that.
Setting SHELL, PATH, and other variables
You can define environment variables at the top of your crontab:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=alexSHELL– which shell runs your commands (if you want Bash-specific features).PATH– add directories so you don’t need full paths for every command.MAILTO– where cron sends output; if empty, output is discarded (on many systems).
Example crontab with environment and entry:
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=admin@example.com
0 3 * * * /home/admin/bin/backup-databases.shYou can also set custom variables that your scripts can read:
BACKUP_DIR=/srv/backups
0 3 * * * /home/admin/bin/backup.sh
Inside backup.sh, you can access $BACKUP_DIR.
Using absolute paths
Always use absolute paths to:
- Scripts:
/home/user/bin/script.sh - Binaries:
/usr/bin/rsync,/usr/bin/find(if PATH might be minimal) - Files and directories used in scripts:
/var/log/myapp.log
Relying on relative paths (./script.sh, logs/output.txt) usually breaks under cron.
Integrating cron with Shell Scripts
For advanced scripts, cron is just the scheduler. Your main work happens inside the script. To make scripts cron-friendly:
1. Use a proper shebang
At the top of your script:
#!/usr/bin/env bashor
#!/bin/bashThen make the script executable:
chmod +x /home/alex/bin/backup.shCrontab entry:
0 2 * * * /home/alex/bin/backup.sh2. Initialize environment in the script
If your script relies on environment variables or profiles, source them explicitly:
#!/bin/bash
# Example: source a profile only if it exists
[ -f "$HOME/.profile" ] && . "$HOME/.profile"
# Or define required variables directly
export PATH=/usr/local/bin:/usr/bin:/bin
export APP_ENV=productionDon’t assume your interactive shell configuration is loaded.
3. Set working directory
If your script assumes a certain working directory:
#!/bin/bash
cd /home/alex/myproject || exit 1
# Now relative paths are safe inside the script
./run-report.sh
Or always use absolute paths and avoid reliance on cd.
Redirecting Output and Logs
By default, cron sends stdout and stderr of each job via email to the user (or MAILTO recipient), if local mail is configured. On many desktops, local mail isn’t set up.
For reliable logs:
- Redirect standard output and error to log files.
- Optionally rotate or truncate logs inside the script or via logrotate.
Examples:
Basic logging to a file
0 1 * * * /home/alex/bin/cleanup.sh >> /home/alex/logs/cleanup.log 2>&1This:
- Appends stdout to the log file.
2>&1redirects stderr into stdout, so both go into the same log.
Daily log overwrite
If you don’t want logs to grow forever:
0 1 * * * /home/alex/bin/cleanup.sh > /home/alex/logs/cleanup.log 2>&1This overwrites the log each run.
Timestamped logs (from script)
In your script, prepend timestamps:
#!/bin/bash
log() {
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*"
}
log "Starting cleanup"
# ...
log "Cleanup finished"Run from cron with:
0 1 * * * /home/alex/bin/cleanup.sh >> /home/alex/logs/cleanup.log 2>&1System-Wide cron: /etc/crontab and cron.* Directories
While user crontabs are common, as an admin or advanced user you may also touch:
/etc/crontab
System-wide crontab usually includes an extra field for the user:
MIN HOUR DOM MON DOW USER COMMANDExample:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# Run a backup as root at 3 AM
0 3 * * * root /usr/local/sbin/system-backup.sh
Use this when you need jobs that must run as specific system users (e.g. root, postgres, www-data).
/etc/cron.hourly, /etc/cron.daily, etc.
These directories are for simple scripts run at fixed frequencies, managed by a system cron job or systemd timer.
Common directories:
/etc/cron.hourly//etc/cron.daily//etc/cron.weekly//etc/cron.monthly/
Typical usage:
- Drop an executable script into
/etc/cron.daily/:
sudo nano /etc/cron.daily/logrotate-extra
sudo chmod +x /etc/cron.daily/logrotate-extra- These scripts usually should not expect arguments and may run at unspecified minute/hour within the period, depending on distro policies.
As a shell scripter, you might package maintenance scripts here for system-level tasks.
Access Control: cron.allow and cron.deny
On multi-user systems, cron usage may be restricted via:
/etc/cron.allow– if this exists, only listed users may usecrontab./etc/cron.deny– ifcron.allowdoesn’t exist, users listed here are denied.
These are plain text; one username per line.
Example /etc/cron.allow:
root
admin
backup
Check your system’s documentation (man 5 crontab) for exact behavior, as implementations vary.
Advanced Scheduling Patterns
For complex schedules, combine ranges, lists, and steps.
Examples
- Every 15 minutes during working hours (9–17, weekdays):
*/15 9-17 * * 1-5 /path/to/script.sh- On the first Monday of each month:
There’s no single cron expression for this, but you can approximate with:
0 9 1-7 * 1 /path/to/script.shThis runs at 09:00 on days 1–7 if it’s Monday; only one of those days will be the first Monday.
- Twice per hour, at minute 0 and 30:
0,30 * * * * /path/to/script.sh- Every 10 minutes between :05 and :55 past the hour:
5-55/10 * * * * /path/to/script.shCoordinating cron with Script Logic
Advanced shell scripts often need to be cron-safe:
1. Prevent overlapping runs (locking)
If your job takes longer than the interval, you can get overlaps. Use a lock:
#!/bin/bash
LOCKFILE=/var/lock/myjob.lock
exec 9>"$LOCKFILE" || exit 1
if ! flock -n 9; then
echo "Another instance is running, exiting."
exit 1
fi
# Your job logic hereCrontab:
*/5 * * * * /usr/local/bin/myjob.sh >> /var/log/myjob.log 2>&1Now, if one run is still working, the next run exits gracefully.
2. Conditional execution based on day/time
Sometimes it’s easier to run frequently and check conditions in the script:
#!/bin/bash
hour=$(date +%H)
if [ "$hour" -ge 9 ] && [ "$hour" -lt 17 ]; then
# Work-hours logic
run_report
fiCron:
*/5 * * * * /home/alex/bin/report.sh3. Exit codes for monitoring
Your script should use meaningful exit codes:
0– success- non-zero – error
This allows monitoring systems or wrapper scripts to detect failures.
Troubleshooting cron Jobs
When a script doesn’t run as expected under cron:
- Check cron logs
Depending on distro:
/var/log/cron/var/log/syslogjournalctl -u cronorjournalctl -u crond
Look for lines stating that the job was executed (or errors like “bad minute”).
- Log everything
Temporarily redirect output and debug info:
* * * * * /home/alex/bin/test.sh >> /tmp/test-cron.log 2>&1- Simulate cron environment
Run your script with a minimal environment:
env -i /bin/bash -c '/home/alex/bin/script.sh'or explicitly set some minimal variables:
env -i PATH=/usr/bin:/bin HOME="$HOME" /bin/bash -c '/home/alex/bin/script.sh'- Test commands with absolute paths
Inside your script, temporarily log:
which rsync
echo "PATH is: $PATH"
pwd
envThen inspect the log generated by cron.
- Validate your crontab syntax
A single invalid line can make cron ignore that job or more, depending on implementation. Use:
crontab -land check for typos like:
- Missing fields
- Extra spaces inside ranges/lists
- Non-existent usernames in
/etc/crontab
Security Considerations
When scheduling scripts with cron:
- Least privilege: Run jobs as the least-privileged user necessary. Reserve root for tasks that truly need it.
- Protect scripts and logs:
- Scripts often contain paths, credentials, or sensitive operations. Restrict permissions:
chmod 700 /home/alex/bin/backup.sh
chmod 600 /home/alex/logs/backup.log- Sanitize environment:
Explicitly set PATH and other variables in the script or crontab to prevent environment-based attacks (e.g., malicious binaries earlier in PATH).
- Validate inputs:
If your script processes files or data from untrusted sources, don’t assume everything is safe just because it runs unattended.
Practical Examples
Example 1: Nightly backup via cron
Script: /home/backup/bin/backup-home.sh:
#!/bin/bash
set -euo pipefail
SRC=/home
DEST=/srv/backups/home
LOG=/var/log/home-backup.log
mkdir -p "$DEST"
{
echo "===== $(date '+%Y-%m-%d %H:%M:%S') Starting backup ====="
rsync -a --delete "$SRC"/ "$DEST"/
echo "Backup completed successfully."
} >> "$LOG" 2>&1Make executable:
chmod 700 /home/backup/bin/backup-home.sh
Crontab for user backup (crontab -e as that user):
SHELL=/bin/bash
PATH=/usr/local/bin:/usr/bin:/bin
MAILTO=backup-admin@example.com
0 2 * * * /home/backup/bin/backup-home.shExample 2: Run a monitoring script every 5 minutes
Script: /usr/local/bin/check-disk-usage.sh:
#!/bin/bash
THRESHOLD=90
EMAIL=admin@example.com
HOST=$(hostname)
usage=$(df -h / | awk 'NR==2 {gsub("%","",$5); print $5}')
if [ "$usage" -ge "$THRESHOLD" ]; then
echo "Disk usage on $HOST is ${usage}% (>= ${THRESHOLD}%)." \
| mail -s "Disk usage warning on $HOST" "$EMAIL"
fi
Crontab for root (sudo crontab -e):
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * /usr/local/bin/check-disk-usage.sh >> /var/log/check-disk-usage.log 2>&1Summary
cronruns commands on a schedule using crontabs.- Each job uses a
MIN HOUR DOM MON DOW COMMANDformat (plus aUSERfield in/etc/crontab). - For shell scripts, always consider cron’s limited environment:
- Set
SHELL,PATH, and other variables. - Use absolute paths.
- Control working directory explicitly.
- Redirect output to logs and use timestamps for easier debugging.
- Use locking, exit codes, and script logic to make cron jobs robust and non-overlapping.
- Pay attention to permissions and security, especially for root or system-wide jobs.
With these practices, your advanced shell scripts will run reliably and predictably under cron.