Linux: Task Automation — cron / systemd / journalctl
Everything on a server should be automated — scheduled backups, log cleanup, health checks. Linux provides automation solutions ranging from simple (cron) to modern (systemd timers).
📋 Prerequisites: You should already know
- Lesson 21: Bash Scripting Advanced
1. What You'll Learn
- crontab scheduled task syntax
- systemd service unit authoring
- systemd timer as a cron replacement
- journalctl log queries
- logrotate log rotation
2. A Story of "Never Waking Up at 2 AM to Run a Script Again"
(1) The Pain Point: Manual Daily Backups
Xiaoming's backup script needed to be run manually every day. He set an alarm — waking up at 2 AM to run ./backup.sh. After a week, he was exhausted.
(2) cron Set Him Free
Bob taught him crontab:
crontab -e
# Add:
0 2 * * * /home/alice/scripts/backup.sh > /dev/null 2>&1
From then on, the system automatically ran the backup every day at 2 AM. Xiaoming only needed to check the logs occasionally.
(3) The Benefit: Let Machines Do What Machines Should Do
After learning cron, Xiaoming automated everything at once: daily backups, weekly reports, log cleanup, health check pings. His daily ops time dropped from 2 hours to 10 minutes.
3. Knowledge Points
(1) crontab — Scheduled Tasks
cron syntax: minute hour day month weekday command
min hour day month weekday command
(0-59)(0-23)(1-31)(1-12)(0-7)
(0=Sunday 7=Sunday)
# Special syntax
@reboot # Run on reboot
@daily # Every day at 0:00 (same as 0 0 * * *)
@hourly # Every hour (same as 0 * * * *)
@weekly # Every Monday at 0:00
@monthly # 1st of every month at 0:00
Common Examples:
# View current user's cron
crontab -l
# Edit crontab
crontab -e
# Delete all cron jobs
crontab -r
# Common examples
0 2 * * * /scripts/backup.sh # Daily at 2:00
*/5 * * * * /scripts/health-check.sh # Every 5 minutes
0 0 * * 0 /scripts/weekly-report.sh # Every Sunday at 0:00
0 0 1 * * /scripts/monthly-cleanup.sh # 1st of every month
0 9-17 * * 1-5 /scripts/business-hours.sh # Weekdays 9-17, every hour
crontab -r deletes all cron jobs for the current user with no confirmation prompt! Accidental deletion can only be recovered from backup. Always use crontab -l to back up before deleting, or develop the habit of editing with crontab -e rather than deleting with crontab -r.
~/.bashrc, ~/.profile, etc., and PATH typically only includes /usr/bin:/bin. If your script depends on custom environment variables, manually source config files at the top, or use absolute paths for commands in crontab.
(2) systemd service
# Service unit path
/etc/systemd/system/myapp.service
# Basic structure
[Unit]
Description=My Web Application
After=network.target
[Service]
Type=simple
User=alice
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node server.js
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
# Usage
sudo systemctl daemon-reload # Reload configuration
sudo systemctl start myapp # Start
sudo systemctl enable myapp # Enable on boot
sudo systemctl status myapp # View status
sudo journalctl -u myapp -f # View logs
(3) systemd timer
Persistent=true). For simple periodic scripts without these needs, cron is more lightweight and intuitive. New projects should try systemd timer first.
# Timer unit (replaces cron)
/etc/systemd/system/backup.timer
[Unit]
Description=Daily Backup Timer
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=timers.target
# Corresponding service
/etc/systemd/system/backup.service
[Unit]
Description=Daily Backup Service
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
# Usage
sudo systemctl enable --now backup.timer
sudo systemctl list-timers --all
OnCalendar Syntax:
OnCalendar=daily # Every day at 0:00
OnCalendar=hourly # Every hour
OnCalendar=*-*-* 02:00:00 # Every day at 2:00
OnCalendar=Mon *-*-* 03:00:00 # Every Monday at 3:00
OnCalendar=*-*-1 00:00:00 # 1st of every month
OnCalendar=*:0/5 # Every 5 minutes
(4) journalctl — Log Viewer
# Basic
journalctl # All logs
journalctl -u nginx # Specific service
journalctl -u nginx -f # Follow in real time
journalctl -u nginx --since "1 hour ago"
journalctl -u nginx --until "2026-07-07 10:00"
journalctl -p err # Error level and above only
journalctl -p err -u nginx # nginx error logs
# Management
journalctl --disk-usage # Check log disk usage
sudo journalctl --vacuum-size=500M # Limit log size
sudo journalctl --vacuum-time=7d # Keep 7 days
(5) logrotate — Log Rotation
/etc/cron.daily/logrotate). You only need to write configuration in /etc/logrotate.d/. After modifying a config, use logrotate -d /etc/logrotate.d/your-config in debug mode to preview, then let it take effect automatically.
# Configuration locations
/etc/logrotate.conf # Global config
/etc/logrotate.d/ # Per-app configs
# Example: Nginx log rotation
/var/log/nginx/*.log {
daily
rotate 7
compress
delaycompress
missingok
notifempty
create 640 www-data adm
postrotate
[ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid`
endscript
}
Common Directives:
| Directive | Meaning |
|---|---|
daily/weekly/monthly |
Rotation frequency |
rotate N |
Keep N archived copies |
compress |
Compress archives (gzip) |
delaycompress |
Delay compression by one cycle |
missingok |
Don't error if file is missing |
notifempty |
Don't rotate empty files |
size 100M |
Only rotate when file reaches specified size |
maxage 30 |
Delete archives older than 30 days |
(6) ▶ Example: crontab Configuration
# Edit crontab
crontab -e
# Write the following content
# ┌───── minute (0-59)
# │ ┌───── hour (0-23)
# │ │ ┌───── day of month (1-31)
# │ │ │ ┌───── month (1-12)
# │ │ │ │ ┌───── day of week (0-7, 0=Sunday)
# * * * * * command
# Daily 2:00 AM automated backup
0 2 * * * /home/alice/scripts/backup.sh
# Every 5 minutes health check
*/5 * * * * /home/alice/scripts/health-check.sh
# Every Monday 3:00 AM weekly report
0 3 * * 1 /home/alice/scripts/weekly-report.sh
# 1st of every month archive cleanup
0 0 1 * * /home/alice/scripts/cleanup.sh
(7) ▶ Example: Creating a systemd Service
# Create a systemd service for a Node.js application
sudo tee /etc/systemd/system/webapp.service << 'EOF'
[Unit]
Description=Node.js Web Application
Documentation=https://example.com/docs
After=network.target
[Service]
Type=simple
User=alice
Group=alice
WorkingDirectory=/opt/webapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node /opt/webapp/server.js
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=10
# Security options
ProtectSystem=full
NoNewPrivileges=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now webapp
sudo systemctl status webapp
Output:
TEXT● webapp.service - Node.js Web Application Loaded: loaded (/etc/systemd/system/webapp.service; enabled) Active: active (running) since Tue 2026-07-07 10:23:45 UTC Main PID: 5678 (node) Tasks: 12 (limit: 4915) Memory: 45.2M CPU: 1.234s
(8) ▶ Example: systemd Timer Replacing cron
# Backup service
sudo tee /etc/systemd/system/backup.service << 'EOF'
[Unit]
Description=Daily Backup
[Service]
Type=oneshot
ExecStart=/opt/scripts/backup.sh
User=alice
EOF
# Backup timer
sudo tee /etc/systemd/system/backup.timer << 'EOF'
[Unit]
Description=Run backup daily at 2am
Requires=backup.service
[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true
[Install]
WantedBy=timers.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now backup.timer
sudo systemctl list-timers --all
Output:
TEXTNEXT LEFT LAST PASSED Tue 2026-07-07 02:00:00 UTC 15h left Mon 2026-07-06 02:00:00 UTC 9h ago n/a n/a n/a n/a 1 timers listed.
(9) ▶ Example: journalctl Log Queries
# View nginx errors from the last hour
journalctl -u nginx -p err --since "1 hour ago"
# View all logs from yesterday
journalctl --since yesterday --until today
# View logs for a specific PID
journalctl _PID=1234
# Follow multiple services in real time
journalctl -u nginx -u webapp -f
# Export logs to file
journalctl -u webapp --since "2026-07-06" > webapp-logs.txt
# Check log disk usage
journalctl --disk-usage
# Archived and active journals take up 512.0M
# Clean up
sudo journalctl --vacuum-size=200M
Output:
TEXTJul 07 10:23:45 server nginx[1234]: GET /api/users 200 1234 Jul 07 10:23:50 server nginx[1234]: POST /api/data 500 0 Jul 07 10:24:01 server webapp[5678]: Error: ECONNREFUSED -- Logs begin at Mon 2026-07-06 00:00:01 -- Jul 07 09:00:00 server sshd[890]: Accepted publickey for alice Archived and active journals take up 512.0M Deleted archived journals: 23 Vacuuming done, freed 312.0M of archived journals
(10) ▶ Example: logrotate Configuration
# Configure log rotation for a custom application
sudo tee /etc/logrotate.d/custom-app << 'EOF'
/var/log/custom-app/*.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
create 640 www-data www-data
sharedscripts
postrotate
systemctl reload custom-app > /dev/null 2>&1 || true
endscript
}
EOF
# Test the configuration
sudo logrotate -d /etc/logrotate.d/custom-app
# Manually force execution
sudo logrotate -f /etc/logrotate.d/custom-app
Output:
TEXTreading config file /etc/logrotate.d/custom-app Handling 1 logs rotating pattern: /var/log/custom-app/*.log rotating 14 kept versions considering log /var/log/custom-app/app.log log needs rotating renaming /var/log/custom-app/app.log to /var/log/custom-app/app.log.1 creating new /var/log/custom-app/app.log mode=0640 user=www-data group=www-data
(11) ▶ Comprehensive Example: Automated Ops System
#!/bin/bash
# setup-automation.sh - Configure automated operations system
set -euo pipefail
echo "=== Setting Up Automated Ops System ==="
# 1. Create scripts directory
mkdir -p /opt/scripts
# 2. Backup script
cat > /opt/scripts/backup.sh << 'BACKUPEOF'
#!/bin/bash
# Auto backup - triggered daily at 2 AM by cron or systemd timer
SOURCE="/var/www"
DEST="/backup"
RETENTION=30
mkdir -p "$DEST"
BACKUP_FILE="${DEST}/backup_$(date +%Y%m%d).tar.gz"
tar -czf "$BACKUP_FILE" \
--exclude=node_modules \
--exclude=.git \
"$SOURCE" 2>/dev/null
# Clean expired backups
find "$DEST" -name "backup_*.tar.gz" -mtime +$RETENTION -delete
# Log
echo "[$(date)] Backup: $BACKUP_FILE ($(du -h "$BACKUP_FILE" | cut -f1))" >> /var/log/backup.log
BACKUPEOF
chmod +x /opt/scripts/backup.sh
# 3. Health check script
cat > /opt/scripts/health-check.sh << 'HEALTHEOF'
#!/bin/bash
# Health check - runs every 5 minutes
WEBHOOK_URL="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
SERVICES=("nginx" "postgresql" "webapp")
for svc in "${SERVICES[@]}"; do
if ! systemctl is-active --quiet "$svc"; then
MESSAGE="❌ $svc service abnormal $(date)"
echo "$MESSAGE" | mail -s "Service Alert" admin@example.com
# curl -X POST -d "text=$MESSAGE" "$WEBHOOK_URL"
fi
done
# Disk check
DISK_USAGE=$(df -h / | tail -1 | awk '{print $5}' | tr -d '%')
if [ "$DISK_USAGE" -gt 85 ]; then
echo "⚠️ Disk usage: ${DISK_USAGE}%" >> /var/log/disk-alert.log
fi
HEALTHEOF
chmod +x /opt/scripts/health-check.sh
# 4. Configure cron
(crontab -l 2>/dev/null; echo "") | crontab -
(crontab -l 2>/dev/null; echo "# Auto backup - daily at 2:00") | crontab -
(crontab -l 2>/dev/null; echo "0 2 * * * /opt/scripts/backup.sh > /dev/null 2>&1") | crontab -
(crontab -l 2>/dev/null; echo "# Health check - every 5 minutes") | crontab -
(crontab -l 2>/dev/null; echo "*/5 * * * * /opt/scripts/health-check.sh > /dev/null 2>&1") | crontab -
(crontab -l 2>/dev/null; echo "# Log cleanup - every Sunday") | crontab -
(crontab -l 2>/dev/null; echo "0 0 * * 0 journalctl --vacuum-size=200M > /dev/null 2>&1") | crontab -
echo ""
echo "=== Automated Ops System Setup Complete ==="
echo "Configured tasks:"
crontab -l | grep -v "^#"
echo ""
echo "Manual verification: crontab -l"
❓ FAQ
Q: Are crontab environment variables the same as in the terminal? A: No! The PATH in cron's execution environment is very minimal (usually
/usr/bin:/bin). If your script depends on PATH defined in~/.bashrc, either set PATH at the top of the script or use absolute paths in the cron command.
Q: What advantages does systemd timer have over cron? A: 1) Dependency management: timers can depend on other services. 2) Log integration: automatic journalctl logging. 3) Missed execution:
Persistent=truecatches up on next boot. 4) More precise: supports millisecond accuracy. 5) Unified management:systemctl list-timers.
Q: Will journalctl logs fill up the hard drive? A: Default maximum is 10% of disk (but no more than 4GB). Use
journalctl --vacuum-size=500Mor configureSystemMaxUse=500Min/etc/systemd/journald.confto limit.
Q: Does logrotate run automatically or do I need to configure cron? A: logrotate is executed daily by the system's built-in cron job (at
/etc/cron.daily/logrotate). You only need to configure rules in/etc/logrotate.d/— no additional cron setup needed.
Q: How to debug crontab not executing? A: Three-step troubleshooting: 1) Check if the cron service is running:
systemctl status cron. 2) Check cron logs:grep CRON /var/log/syslog. 3) Manually run the script to confirm it works:bash /path/to/script.sh.
📖 Summary
- crontab:
minute hour day month weekday command,crontab -eto edit - systemd service:
[Service] ExecStart=,Restart=on-failure - systemd timer:
OnCalendar=,Persistent=true - journalctl:
-u serviceby service,-p errby level,-ffollow real-time - logrotate:
daily / rotate 7 / compressautomatic log rotation
📝 Exercises
- Basic (⭐): Configure crontab to run
dateevery hour and append to/tmp/cron-test.log, then use journalctl to view nginx or sshd startup logs - Intermediate (⭐⭐): Create a simple
hello.serviceand start it (Type=simple), then create a logrotate config for/var/log/test-app/*.logwith daily rotation keeping 7 days - Challenge (⭐⭐⭐): Write a systemd timer that cleans expired files in
/tmpdaily at 3:00 AM, with a corresponding service file, and configure journalctl to view its execution logs