Shell Scripting Bash Cron Intermediate May 2026

Shell Scripting Scheduling & Cron

Master cron syntax, crontab management, special strings, environment pitfalls, cron logging, the at command for one-time jobs, anacron for missed jobs, and real-world production scheduling patterns.

Cron is the backbone of server automation — backups, reports, cleanup, health checks, database maintenance. Understanding it completely, including its environment quirks and logging gaps, is what prevents the classic "runs manually but not in cron" problem.

CRON
# Cron field layout:
# ┌─────────── minute        (0-59)
# │ ┌───────── hour          (0-23)
# │ │ ┌─────── day of month  (1-31)
# │ │ │ ┌───── month         (1-12 or JAN-DEC)
# │ │ │ │ ┌─── day of week   (0-7 or SUN-SAT, 0 and 7 = Sunday)
# │ │ │ │ │
# * * * * *  command

# ── Common patterns ───────────────────────────────────────
*/5  *  *  *  *   /scripts/health_check.sh    # every 5 minutes
0    *  *  *  *   /scripts/hourly_report.sh   # every hour at :00
30   6  *  *  *   /scripts/daily_backup.sh    # daily at 06:30
0    2  *  *  0   /scripts/weekly_clean.sh    # Sunday at 02:00
0    3  1  *  *   /scripts/monthly_stats.sh   # 1st of month at 03:00
0    9  *  *  1-5 /scripts/weekday_report.sh  # Mon-Fri at 09:00
0    8  1  1,7 *   /scripts/biannual.sh        # Jan 1 and Jul 1

# ── Step values ───────────────────────────────────────────
*/10 *  *  *  *   cmd  # every 10 minutes
0  */2 *  *  *   cmd  # every 2 hours
0    0  */3 *  *   cmd  # every 3 days

# ── Special strings (easier to read) ─────────────────────
@reboot   /scripts/on_boot.sh       # on system startup
@hourly   /scripts/hourly.sh        # same as: 0 * * * *
@daily    /scripts/daily.sh         # same as: 0 0 * * *
@weekly   /scripts/weekly.sh        # same as: 0 0 * * 0
@monthly  /scripts/monthly.sh       # same as: 0 0 1 * *
@yearly   /scripts/yearly.sh        # same as: 0 0 1 1 *
BASH
# ── crontab commands ──────────────────────────────────────
crontab -e              # edit current user's crontab
crontab -l              # list current crontab
crontab -r              # remove crontab (dangerous!)
crontab -u vriddh -l    # list another user's crontab (root only)
crontab -u vriddh -e    # edit another user's crontab

# ── Install crontab from file ──────────────────────────────
crontab mycrons.txt     # installs file as crontab (replaces existing)

# ── System-wide cron directories ─────────────────────────
# Drop scripts into these dirs — no crontab editing needed
# /etc/cron.hourly/    — runs every hour
# /etc/cron.daily/     — runs daily
# /etc/cron.weekly/    — runs weekly
# /etc/cron.monthly/   — runs monthly
# /etc/cron.d/         — custom cron files with full syntax

# Example /etc/cron.d/myapp file:
# SHELL=/bin/bash
# PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
# 0 2 * * * vriddh /opt/myapp/scripts/daily_backup.sh

# ── Backup and restore crontab ────────────────────────────
crontab -l > ~/crontab.bak    # backup
crontab ~/crontab.bak          # restore

The most common cron problem: scripts work manually but fail silently in cron. The cause is always the environment — cron runs with a minimal PATH and no shell config is sourced.

BASH
# ── Cron's minimal PATH ───────────────────────────────────
# Default PATH in cron: /usr/bin:/bin
# Your interactive PATH: /usr/local/bin:/usr/bin:/bin:/opt/...etc
# mysql, python3, node etc may NOT be found in cron!

# ── Solution 1: Set PATH in crontab ──────────────────────
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 2 * * * /opt/myapp/backup.sh

# ── Solution 2: Use full paths in scripts ────────────────
# Instead of: mysql -h host
# Use:        /usr/bin/mysql -h host

# Find full path of a command:
which mysql       # /usr/bin/mysql
which python3     # /usr/bin/python3

# ── Solution 3: Source profile in script ─────────────────
#!/usr/bin/env bash
source /etc/profile 2>/dev/null || true
source ~/.bashrc    2>/dev/null || true
# ... rest of script

# ── Cron has no HOME, DISPLAY, etc ───────────────────────
# Always set HOME explicitly if needed
HOME=/home/vriddh
0 2 * * * /opt/scripts/backup.sh
BASH
# ── Default: cron emails output to local user ─────────────
# Suppress all output (silent cron job):
0 2 * * * /scripts/backup.sh >/dev/null 2>&1

# ── Log to file ───────────────────────────────────────────
0 2 * * * /scripts/backup.sh >> /var/log/backup.log 2>&1

# ── Log with timestamp ────────────────────────────────────
0 2 * * * /scripts/backup.sh >> /var/log/backup.log 2>&1; \
          echo "[$(date)] Exit: $?" >> /var/log/backup.log

# ── Send output to email ─────────────────────────────────
MAILTO=ops@example.com
0 2 * * * /scripts/backup.sh    # output emailed to MAILTO

# ── Only email on failure ─────────────────────────────────
0 2 * * * /scripts/backup.sh >> /var/log/backup.log 2>&1 \
          || mail -s "BACKUP FAILED" ops@example.com < /var/log/backup.log

# ── View cron logs ────────────────────────────────────────
grep CRON /var/log/syslog       # Debian/Ubuntu
grep CRON /var/log/cron         # CentOS/RHEL
journalctl -u cron              # systemd systems
BASH
# ── at — run a command once at a specific time ────────────
at 22:30                        # at 10:30 PM today
at 09:00 tomorrow               # tomorrow morning
at noon                         # today at 12:00
at midnight                     # tonight at 00:00
at now + 30 minutes             # 30 minutes from now
at now + 2 hours                # 2 hours from now
at 14:00 Jul 15                 # specific date

# Interactive mode
$ at 23:00
at> /scripts/maintenance.sh
at>                      # submit the job

# Non-interactive (better for scripts)
echo "/scripts/maintenance.sh" | at 23:00
at 23:00 << EOF
/scripts/maintenance.sh --mode full
/scripts/send_report.sh
EOF

# Manage at jobs
atq                             # list pending at jobs
atrm 3                          # remove job number 3
at -l                           # same as atq
at -d 3                         # same as atrm 3
CRON
# /etc/cron.d/myapp — Production cron for myapp
# Managed by: Vriddh Technologies
# Last updated: 2026-05-01

SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
MAILTO=ops@example.com

# ── Health checks (every 5 min) ───────────────────────────
*/5 * * * * vriddh /opt/myapp/scripts/health_check.sh >> /var/log/myapp/health.log 2>&1

# ── Database backup (daily at 02:00) ─────────────────────
0 2 * * * vriddh /opt/myapp/scripts/db_backup.sh >> /var/log/myapp/backup.log 2>&1

# ── Log cleanup (weekly Sunday 03:00) ────────────────────
0 3 * * 0 vriddh /opt/myapp/scripts/rotate_logs.sh >> /var/log/myapp/rotate.log 2>&1

# ── Monthly usage report (1st of month 08:00) ────────────
0 8 1 * * vriddh /opt/myapp/scripts/monthly_report.sh | mail -s "Monthly Report" management@example.com

# ── Session cleanup (every 6 hours) ──────────────────────
0 */6 * * * vriddh /opt/myapp/scripts/cleanup_sessions.sh >/dev/null 2>&1

# ── Disk alert (every 30 min) ─────────────────────────────
*/30 * * * * vriddh /opt/myapp/scripts/disk_alert.sh >/dev/null 2>&1

# ── Replication check (every 10 min, business hours only) ─
*/10 8-18 * * 1-5 vriddh /opt/myapp/scripts/check_replication.sh >> /var/log/myapp/repl.log 2>&1
bash — crontab management
vriddh@prod-01:~$crontab -l
# Vriddh production crontab
*/5 * * * * /opt/myapp/scripts/health_check.sh >> /var/log/myapp/health.log 2>&1
0 2 * * * /opt/myapp/scripts/db_backup.sh >> /var/log/myapp/backup.log 2>&1
vriddh@prod-01:~$grep CRON /var/log/syslog | tail -5
May 1 10:00:01 prod-01 CRON[18421]: (vriddh) CMD (/opt/myapp/scripts/health_check.sh)
May 1 10:00:02 prod-01 CRON[18421]: (CRON) INFO (No MTA installed)
May 1 10:05:01 prod-01 CRON[18539]: (vriddh) CMD (/opt/myapp/scripts/health_check.sh)
✔ Cron production checklist — Always set SHELL and PATH at the top of every crontab. Always redirect output — either >> logfile 2>&1 or >/dev/null 2>&1. Always use absolute paths. Test scripts with env -i /bin/bash --login -c 'your_script.sh' to simulate cron's environment. Use MAILTO to send failure alerts automatically.