Key Takeaways
- Five-field syntax:
MIN HOUR DOM MON DOW command— minute, hour, day-of-month, month, day-of-week. Use*for “every”,*/5for “every 5”,1,15for “1st and 15th”,1-5for “1 through 5”. - Always full paths in cron: Cron’s
PATHis/usr/bin:/bin. Commands likedocker,python3,nodeneed full paths (/usr/bin/docker) or an explicitPATH=line at the top of the crontab. - Redirect output: A cron job with no output redirection sends email to the local root mailbox. Add
>> /var/log/myjob.log 2>&1or> /dev/null 2>&1to control where output goes. - Prefer systemd timers for new system services: Better logging (journald), catch-up on missed runs, dependency management, and
systemctlintegration make timers the right choice for services that run as part of the OS.
Introduction
Direct Answer: How do I schedule a cron job on Ubuntu 24.04 in 2026?
Run crontab -e to open your user’s crontab in the default editor. Add a line with the format MIN HOUR DOM MON DOW /full/path/to/command. For example, 0 3 * * * /usr/bin/python3 /home/user/backup.py >> /var/log/backup.log 2>&1 runs a Python backup script every day at 3:00 AM. The five time fields are: minute (0–59), hour (0–23), day of month (1–31), month (1–12), and day of week (0–7, where 0 and 7 both mean Sunday). Use * for “every unit”, */5 for “every 5 units”, and 1,15 for “on the 1st and 15th”. Verify with crontab -l to list current jobs. Check cron daemon logs with grep CRON /var/log/syslog | tail -20. For system-wide jobs (not tied to a user), create a file in /etc/cron.d/ with an additional username field: 0 3 * * * root /usr/bin/python3 /opt/backup.py.
Part 1: Crontab Syntax
┌────────────── 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)
│ │ │ │ │
* * * * * /path/to/command arg1 arg2
Common patterns:
# Every minute
* * * * * /usr/bin/command
# Every 5 minutes
*/5 * * * * /usr/bin/command
# Every day at 3:30 AM
30 3 * * * /usr/bin/command
# Every weekday (Mon–Fri) at 9:00 AM
0 9 * * 1-5 /usr/bin/command
# Every Monday at midnight
0 0 * * 1 /usr/bin/command
# 1st and 15th of every month at 6:00 AM
0 6 1,15 * * /usr/bin/command
# Every hour during business hours (9am–6pm) on weekdays
0 9-18 * * 1-5 /usr/bin/command
# Every 30 minutes between midnight and 6am
*/30 0-6 * * * /usr/bin/command
# Shorthand aliases (supported by vixie-cron)
@reboot /usr/bin/command # Run once at startup
@daily /usr/bin/command # Same as: 0 0 * * *
@weekly /usr/bin/command # Same as: 0 0 * * 0
@monthly /usr/bin/command # Same as: 0 0 1 * *
@hourly /usr/bin/command # Same as: 0 * * * *
Validate with crontab.guru:
# Test your expression locally (shows next 5 execution times)
python3 -c "
from datetime import datetime
import re
# Basic visualization — use crontab.guru for full validation
exprs = [
('*/5 * * * *', 'Every 5 minutes'),
('0 3 * * *', 'Daily at 3am'),
('0 9 * * 1-5', 'Weekdays 9am'),
('0 0 1,15 * *', '1st and 15th'),
]
for expr, desc in exprs:
print(f' {expr:20s} → {desc}')
"
Expected output:
*/5 * * * * → Every 5 minutes
0 3 * * * → Daily at 3am
0 9 * * 1-5 → Weekdays 9am
0 0 1,15 * * → 1st and 15th
Part 2: User Crontab — crontab -e
# Edit your user's crontab
crontab -e # Opens in $EDITOR (nano by default)
# List current jobs
crontab -l
# Remove all cron jobs for current user (careful!)
# crontab -r
# View another user's crontab (requires root)
sudo crontab -l -u www-data
Adding your first cron job:
# Create a simple script to schedule
cat > ~/scripts/daily-cleanup.sh << 'EOF'
#!/bin/bash
set -euo pipefail
LOG="/var/log/daily-cleanup.log"
echo "[$(date)] Starting cleanup" >> "$LOG"
# Delete files older than 7 days in /tmp
find /tmp -type f -mtime +7 -delete 2>/dev/null || true
# Rotate logs older than 30 days
find /var/log -name "*.log.*" -mtime +30 -delete 2>/dev/null || true
echo "[$(date)] Cleanup done" >> "$LOG"
EOF
chmod +x ~/scripts/daily-cleanup.sh
# Add to crontab — run at 2:00 AM daily
(crontab -l 2>/dev/null; echo "0 2 * * * /home/$USER/scripts/daily-cleanup.sh") | crontab -
crontab -l
Expected output:
0 2 * * * /home/ubuntu/scripts/daily-cleanup.sh
Critical: Always set PATH in cron scripts:
# Cron's default PATH is minimal — add this to the TOP of your crontab
(crontab -l 2>/dev/null | head -0; \
echo 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'; \
echo 'SHELL=/bin/bash'; \
crontab -l 2>/dev/null) | crontab -
crontab -l
Expected output:
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SHELL=/bin/bash
0 2 * * * /home/ubuntu/scripts/daily-cleanup.sh
Part 3: System Cron — /etc/cron.d/
For system-level jobs not tied to a specific user, use /etc/cron.d/ — these run as any specified user and persist across user account changes.
# Create a system cron job (requires root)
sudo tee /etc/cron.d/db-backup << 'EOF'
# Database backup — runs at 3:00 AM as the postgres user
# Format: MIN HOUR DOM MON DOW USER COMMAND
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
# PostgreSQL backup — daily at 3am
0 3 * * * postgres /usr/bin/pg_dumpall --clean | gzip > /var/backups/postgres-$(date +\%Y\%m\%d).sql.gz
# Cleanup old backups — weekly on Sunday at 4am
0 4 * * 0 root find /var/backups -name "postgres-*.sql.gz" -mtime +30 -delete
EOF
# Verify syntax (cron doesn't validate until runtime, so check manually)
sudo cat /etc/cron.d/db-backup
Expected output:
SHELL=/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
0 3 * * * postgres /usr/bin/pg_dumpall --clean | gzip > /var/backups/postgres-$(date +%Y%m%d).sql.gz
0 4 * * 0 root find /var/backups -name "postgres-*.sql.gz" -mtime +30 -delete
Pre-existing cron directories:
ls -la /etc/cron.{hourly,daily,weekly,monthly}/
Expected output:
/etc/cron.daily:
logrotate
man-db
apt-compat
dpkg
/etc/cron.weekly:
man-db
/etc/cron.monthly:
(empty)
Drop executable scripts directly into these directories — they run at the appropriate interval without needing a crontab entry.
# Example: create a daily cleanup script
sudo tee /etc/cron.daily/app-cleanup << 'EOF'
#!/bin/bash
# Runs daily via /etc/cron.daily
set -euo pipefail
find /opt/app/tmp -type f -mtime +1 -delete
find /var/log/app -name "*.log" -mtime +90 -delete
EOF
sudo chmod +x /etc/cron.daily/app-cleanup
Part 4: Monitoring Cron Jobs
# View cron execution logs
grep CRON /var/log/syslog | tail -20
Expected output:
Apr 22 02:00:01 hetzner CRON[12345]: (ubuntu) CMD (/home/ubuntu/scripts/daily-cleanup.sh)
Apr 22 02:00:01 hetzner CRON[12345]: (CRON) info (No MTA installed, discarding output)
# View cron journal (systemd-based Ubuntu 24.04)
journalctl -u cron --since today | tail -20
# List all user crontabs on the system (root only)
for user in $(cut -f1 -d: /etc/passwd); do
crontab -l -u "$user" 2>/dev/null | grep -v "^#" | grep -v "^$" | \
while read -r line; do echo " [$user] $line"; done
done
# Check if cron is running
systemctl is-active cron && echo "Cron is running"
Expected output:
Cron is running
Part 5: systemd Timers
systemd timers are the modern alternative to cron. They log to journald, support catch-up execution, and integrate with systemctl.
How systemd timers work: A timer unit (.timer) triggers a service unit (.service). The service contains the command to run; the timer contains the schedule.
# Create a service unit (the command to run)
sudo tee /etc/systemd/system/db-backup.service << 'EOF'
[Unit]
Description=PostgreSQL Database Backup
After=postgresql.service
Requires=postgresql.service
[Service]
Type=oneshot
User=postgres
ExecStart=/usr/bin/pg_dumpall --clean
StandardOutput=append:/var/log/db-backup.log
StandardError=append:/var/log/db-backup.log
EOF
# Create the timer unit (the schedule)
sudo tee /etc/systemd/system/db-backup.timer << 'EOF'
[Unit]
Description=Run PostgreSQL backup daily at 3am
Requires=db-backup.service
[Timer]
# Run at 3:00 AM every day
OnCalendar=*-*-* 03:00:00
# If the timer was missed (e.g., server was off), run immediately on next boot
Persistent=true
# Randomise the start time by up to 5 minutes (avoids thundering herd)
RandomizedDelaySec=300
[Install]
WantedBy=timers.target
EOF
# Enable and start the timer
sudo systemctl daemon-reload
sudo systemctl enable --now db-backup.timer
# Verify it's active and shows next trigger time
sudo systemctl status db-backup.timer --no-pager
Expected output:
● db-backup.timer - Run PostgreSQL backup daily at 3am
Loaded: loaded (/etc/systemd/system/db-backup.timer; enabled)
Active: active (waiting) since Wed 2026-04-22 09:00:00 UTC
Trigger: Thu 2026-04-23 03:00:00 UTC; 17h left
Triggers: ● db-backup.service
# List all active timers
systemctl list-timers
Expected output:
NEXT LEFT LAST PASSED UNIT
Thu 2026-04-23 03:00:00 UTC 17h left Wed 2026-04-22 03:00:00 UTC 6h ago db-backup.timer
Thu 2026-04-23 00:00:00 UTC 14h left Wed 2026-04-22 00:00:00 UTC 9h ago logrotate.timer
Part 6: OnCalendar Syntax Reference
OnCalendar=daily # Every day at midnight (0:00:00)
OnCalendar=weekly # Every Monday at midnight
OnCalendar=monthly # 1st of each month at midnight
OnCalendar=hourly # Every hour at :00
OnCalendar=*-*-* 03:00:00 # Every day at 3am
OnCalendar=*-*-* 03:30:00 # Every day at 3:30am
OnCalendar=Mon *-*-* 04:00:00 # Every Monday at 4am
OnCalendar=Mon..Fri 09:00:00 # Weekdays at 9am
OnCalendar=Sat,Sun 08:00:00 # Weekends at 8am
OnCalendar=*-*-1 00:00:00 # 1st of each month at midnight
OnCalendar=*-*-1,15 00:00:00 # 1st and 15th at midnight
OnCalendar=*:0/5 # Every 5 minutes
OnCalendar=*:0/15 # Every 15 minutes
OnCalendar=*-01-01 00:00:00 # Every year on Jan 1st
Verify an expression:
# Test OnCalendar syntax before deploying
systemd-analyze calendar "*-*-* 03:00:00"
Expected output:
Original form: *-*-* 03:00:00
Normalized form: *-*-* 03:00:00
Next elapse: Thu 2026-04-23 03:00:00 UTC
(in UTC): Thu 2026-04-23 03:00:00 UTC
From now: 17h 0min left
Part 7: Real-World Timer — Docker Image Cleanup
# Remove unused Docker images weekly to reclaim disk space
sudo tee /etc/systemd/system/docker-cleanup.service << 'EOF'
[Unit]
Description=Remove unused Docker images and volumes
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
ExecStart=/usr/bin/docker system prune --all --volumes --force
StandardOutput=journal
StandardError=journal
EOF
sudo tee /etc/systemd/system/docker-cleanup.timer << 'EOF'
[Unit]
Description=Weekly Docker cleanup
Requires=docker-cleanup.service
[Timer]
OnCalendar=Sun 03:00:00
Persistent=true
RandomizedDelaySec=600
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now docker-cleanup.timer
# Manually trigger (test without waiting for schedule)
sudo systemctl start docker-cleanup.service
# View output
journalctl -u docker-cleanup.service --since today
Expected output (after manual trigger):
Apr 22 10:00:01 server docker-cleanup[12345]: Deleted Containers:
Apr 22 10:00:01 server docker-cleanup[12345]: Deleted Images:
Apr 22 10:00:01 server docker-cleanup[12345]: Total reclaimed space: 2.34GB
Cron vs systemd Timers
| Feature | Cron | systemd Timer |
|---|---|---|
| Logging | Syslog only | journald (queryable) |
| Missed run recovery | No | Yes (Persistent=true) |
| Dependencies | No | Yes (After=, Requires=) |
| Enable/disable | Manual edit | systemctl enable/disable |
| Run status | grep CRON /var/log/syslog | systemctl status name.timer |
| Per-user scheduling | Yes (crontab) | Limited |
| Catch-up on boot | Only with anacron | Built-in |
| Minimum interval | 1 minute | 1 second |
Decision guide:
- Use cron for: simple user-level tasks, quick one-liners, scripts that don’t need dependency management
- Use systemd timers for: system services, tasks with dependencies (e.g., PostgreSQL must be up), tasks where you need logging and catch-up
Troubleshooting
Cron job doesn’t run
Check 1: systemctl is-active cron — cron daemon must be running.
Check 2: grep CRON /var/log/syslog — look for CMD entries or errors.
Check 3: Test the command manually as the cron user: sudo -u www-data /full/path/to/command.
Check 4: PATH issue — add PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin at the top of the crontab.
systemd timer not firing
sudo systemctl status myapp.timer # Check active and next trigger
systemctl list-timers | grep myapp # Confirm it's listed
journalctl -u myapp.service -n 50 # Check last run output
sudo systemctl daemon-reload # Required after editing unit files
sudo systemctl restart myapp.timer # Force restart
Cron output missing
Cause: If the system has no MTA (mail transfer agent), cron output is discarded with No MTA installed, discarding output.
Fix: Redirect output in the crontab: 0 3 * * * /path/cmd >> /var/log/mycron.log 2>&1
Conclusion
You now have two reliable scheduling tools: cron for quick user-level tasks (edit with crontab -e, validate at crontab.guru, add PATH at the top), and systemd timers for production services (.service + .timer pair, managed with systemctl, logged to journald). Use systemd timers for all new system-level scheduled work in 2026.
Schedule the scripts you built in Bash Scripting Guide 2026 and the Python automation scripts from Python for DevOps Automation.
People Also Ask
How do I check if a cron job ran successfully?
Check the syslog: grep CRON /var/log/syslog | grep "CMD.*your-script". This shows the time cron attempted to run the job. For output, redirect the script’s stdout/stderr to a log file: 0 3 * * * /path/script.sh >> /var/log/script.log 2>&1, then check that log after the scheduled time. With systemd timers, use journalctl -u servicename.service --since "24h ago" for full output.
What is the difference between /etc/crontab and crontab -e?
crontab -e edits the current user’s personal crontab, stored in /var/spool/cron/crontabs/USERNAME. /etc/crontab is the system-wide crontab with an extra USER field: MIN HOUR DOM MON DOW USER COMMAND. Don’t edit /etc/crontab directly for application jobs — use /etc/cron.d/ files (same format as /etc/crontab) for system jobs, keeping each application’s schedule in its own file.
How do I run a cron job every 10 seconds?
Cron’s minimum resolution is 1 minute. To run every 10 seconds, add three entries offset by sleep: * * * * * /path/cmd; sleep 10; /path/cmd; sleep 10; /path/cmd. This is a workaround — for sub-minute scheduling, use a systemd timer with OnBootSec=10s and OnUnitActiveSec=10s, or a dedicated process manager like supervisord.
Further Reading
- Bash Scripting Guide 2026 — write the scripts that cron and timers execute
- Python for DevOps Automation — Python scripts schedulable by cron
- Ubuntu 24.04 LTS Server Setup Checklist — includes cron-based log rotation setup
- SSH Hardening Guide 2026 — cron-based key rotation patterns
Tested on: Ubuntu 24.04 LTS. Cron (cronie) 1.5.7, systemd 255. Last verified: April 22, 2026.