Mini Project: Automated Backup System with Cron
In this tutorial, you will learn about Mini Project: Automated Backup System with Cron. We cover key concepts, practical examples, and best practices to help you master this topic.
Build a complete automated backup system using cron: database backups, file archiving, log rotation, health checks, monitoring, and alerting for production workloads.
What You Learn
You will apply all cron concepts in a single project: crontab configuration, locking to prevent overlaps, logging and monitoring, health checks with dead man switch, error handling with retries, backup rotation, and verification.
Why It Matters
A backup system is the most common cron use case. It touches every cron concept: scheduling, locking, logging, retries, health checks, and monitoring. Building one end-to-end teaches you how to combine all cron patterns for a production-ready solution.
Real-World Use
DodaTech's backup system runs on 50 servers: database backups every 6 hours, file backups daily, log rotation weekly, with health checks pinging every run. The system has 99.99% reliability and sends a weekly summary of all backup statuses.
Project Structure
# /opt/backup-system/
# ├── backup.sh # Main backup script
# ├── backup.conf # Configuration
# ├── database.sh # Database backup functions
# ├── files.sh # File backup functions
# ├── rotate.sh # Backup rotation
# ├── verify.sh # Backup verification
# ├── healthcheck.sh # Health check pings
# ├── lock.sh # Lock management
# └── logger.sh # Logging functions
Configuration
#!/bin/bash
# /opt/backup-system/backup.conf
BACKUP_ROOT="/backups"
LOG_DIR="/var/log/backup-system"
LOCK_DIR="/var/lock/backup-system"
HEALTHCHECK_KEY="YOUR-HEALTHCHECKS-KEY"
DB_HOST="localhost"
DB_NAME="production"
DB_USER="backup"
BACKUP_RETENTION_DAYS=30
LOG_RETENTION_DAYS=90
OBJECT_STORAGE="s3://dodatech-backups"
ENCRYPTION_KEY="/etc/backup/gpg-key"
Database Backup Script
#!/bin/bash
# /opt/backup-system/database.sh
source "$(dirname $0)/backup.conf"
source "$(dirname $0)/logger.sh"
SOURCE_TYPE="database"
backup_database() {
local BACKUP_FILE="${BACKUP_ROOT}/database/${DB_NAME}-$(date +%Y%m%d-%H%M%S).sql.gz"
local ENCRYPTED_FILE="${BACKUP_FILE}.gpg"
mkdir -p "$(dirname $BACKUP_FILE)"
log_info "Starting database backup: ${DB_NAME}"
pg_dump -h "$DB_HOST" -U "$DB_USER" "$DB_NAME" | gzip > "$BACKUP_FILE"
local EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
log_error "Database dump failed (exit: ${EXIT_CODE})"
rm -f "$BACKUP_FILE"
return 1
fi
local SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log_info "Database dump completed: ${SIZE}"
gpg --encrypt --recipient backup-team \
--output "$ENCRYPTED_FILE" \
"$BACKUP_FILE" && rm "$BACKUP_FILE"
aws s3 cp "$ENCRYPTED_FILE" "${OBJECT_STORAGE}/database/"
log_info "Uploaded to S3: ${OBJECT_STORAGE}/database/"
echo "$ENCRYPTED_FILE"
}
File Backup Script
#!/bin/bash
# /opt/backup-system/files.sh
source "$(dirname $0)/backup.conf"
source "$(dirname $0)/logger.sh"
SOURCE_TYPE="files"
backup_files() {
local BACKUP_FILE="${BACKUP_ROOT}/files/server-$(date +%Y%m%d-%H%M%S).tar.gz"
local DIRS_TO_BACKUP=(
"/etc"
"/var/www"
"/opt/app"
)
mkdir -p "$(dirname $BACKUP_FILE)"
log_info "Starting file backup: ${DIRS_TO_BACKUP[*]}"
tar -czf "$BACKUP_FILE" "${DIRS_TO_BACKUP[@]}"
local EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
log_error "File backup failed (exit: ${EXIT_CODE})"
return 1
fi
local SIZE=$(du -h "$BACKUP_FILE" | cut -f1)
log_info "File backup completed: ${SIZE}"
aws s3 cp "$BACKUP_FILE" "${OBJECT_STORAGE}/files/"
log_info "Uploaded to S3: ${OBJECT_STORAGE}/files/"
echo "$BACKUP_FILE"
}
Logging Module
#!/bin/bash
# /opt/backup-system/logger.sh
LOG_FILE="${LOG_DIR}/backup.log"
log_info() {
local TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${TIMESTAMP}] [INFO] $1" | tee -a "$LOG_FILE"
}
log_error() {
local TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
echo "[${TIMESTAMP}] [ERROR] $1" | tee -a "$LOG_FILE" >&2
}
log_metrics() {
local JOB_NAME="$1"
local STATUS="$2"
local DURATION="$3"
local SIZE="$4"
echo "${JOB_NAME},${STATUS},${DURATION},${SIZE},$(date +%s)" >> "${LOG_DIR}/metrics.csv"
}
Lock Module
#!/bin/bash
# /opt/backup-system/lock.sh
acquire_lock() {
local LOCK_NAME="$1"
local LOCK_FILE="${LOCK_DIR}/${LOCK_NAME}.lock"
mkdir -p "$LOCK_DIR"
exec 200>"$LOCK_FILE"
if ! flock -n 200; then
echo "Another ${LOCK_NAME} backup is running"
return 1
fi
echo $$ > "$LOCK_FILE"
return 0
}
release_lock() {
local LOCK_NAME="$1"
local LOCK_FILE="${LOCK_DIR}/${LOCK_NAME}.lock"
if [ -f "$LOCK_FILE" ]; then
rm -f "$LOCK_FILE"
fi
}
Health Check Module
#!/bin/bash
# /opt/backup-system/healthcheck.sh
PING_URL="https://hc-ping.com/${HEALTHCHECK_KEY}"
send_start_ping() {
curl -fsS -m 10 "${PING_URL}/start" > /dev/null 2>&1
}
send_success_ping() {
local DURATION="$1"
curl -fsS -m 10 "${PING_URL}/${DURATION}" > /dev/null 2>&1
}
send_failure_ping() {
curl -fsS -m 10 "${PING_URL}/fail" > /dev/null 2>&1
}
Main Backup Script
#!/bin/bash
# /opt/backup-system/backup.sh
# Main orchestration script
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/backup.conf"
source "${SCRIPT_DIR}/lock.sh"
source "${SCRIPT_DIR}/logger.sh"
source "${SCRIPT_DIR}/healthcheck.sh"
OVERALL_STATUS=0
START_TIME=$(date +%s)
# Acquire global backup lock
if ! acquire_lock "full-backup"; then
log_error "Could not acquire backup lock"
exit 1
fi
send_start_ping
log_info "=== Full Backup Started ==="
# Database backup
log_info "Phase 1: Database backup"
if "${SCRIPT_DIR}/database.sh"; then
log_info "Database backup: OK"
else
log_error "Database backup: FAILED"
OVERALL_STATUS=1
fi
# File backup
log_info "Phase 2: File backup"
if "${SCRIPT_DIR}/files.sh"; then
log_info "File backup: OK"
else
log_error "File backup: FAILED"
OVERALL_STATUS=1
fi
# Log rotation
log_info "Phase 3: Log rotation"
if "${SCRIPT_DIR}/rotate.sh"; then
log_info "Log rotation: OK"
else
log_error "Log rotation: FAILED"
fi
# Backup verification
log_info "Phase 4: Verification"
if "${SCRIPT_DIR}/verify.sh"; then
log_info "Verification: OK"
else
log_error "Verification: FAILED"
OVERALL_STATUS=1
fi
END_TIME=$(date +%s)
DURATION=$((END_TIME - START_TIME))
if [ $OVERALL_STATUS -eq 0 ]; then
log_info "=== Full Backup Completed in ${DURATION}s ==="
send_success_ping "$DURATION"
else
log_error "=== Full Backup FAILED after ${DURATION}s ==="
send_failure_ping
fi
release_lock "full-backup"
exit $OVERALL_STATUS
Rotation Script
#!/bin/bash
# /opt/backup-system/rotate.sh
source "$(dirname $0)/backup.conf"
source "$(dirname $0)/logger.sh"
log_info "Starting backup rotation"
# Remove local backups older than retention period
find "${BACKUP_ROOT}/database" -type f -mtime +${BACKUP_RETENTION_DAYS} -delete
find "${BACKUP_ROOT}/files" -type f -mtime +${BACKUP_RETENTION_DAYS} -delete
# Remove old logs
find "$LOG_DIR" -type f -mtime +${LOG_RETENTION_DAYS} -delete
# List S3 backups older than 90 days for manual review
aws s3 ls "${OBJECT_STORAGE}/database/" --recursive | \
awk -v cutoff="$(date -d '-90 days' +%Y-%m-%d)" '$1 < cutoff {print $4}' \
> /tmp/old-s3-backups.txt
log_info "Rotation completed"
Verification Script
#!/bin/bash
# /opt/backup-system/verify.sh
source "$(dirname $0)/backup.conf"
source "$(dirname $0)/logger.sh"
log_info "Starting backup verification"
DATABASE_BACKUPS=$(ls -t "${BACKUP_ROOT}/database/"*.gpg 2>/dev/null | head -1)
FILE_BACKUPS=$(ls -t "${BACKUP_ROOT}/files/"*.tar.gz 2>/dev/null | head -1)
VERIFIED=true
if [ -z "$DATABASE_BACKUPS" ]; then
log_error "No database backups found"
VERIFIED=false
else
log_info "Latest database backup: $(basename $DATABASE_BACKUPS)"
# Verify GPG encryption
if gpg --verify "$DATABASE_BACKUPS" 2>/dev/null; then
log_info "Database backup signature: valid"
else
log_info "Database backup exists (verification skipped)"
fi
fi
if [ -z "$FILE_BACKUPS" ]; then
log_error "No file backups found"
VERIFIED=false
else
log_info "Latest file backup: $(basename $FILE_BACKUPS)"
# Test archive integrity
if gzip -t "$FILE_BACKUPS" 2>/dev/null; then
log_info "File backup integrity: OK"
else
log_error "File backup integrity: FAILED"
VERIFIED=false
fi
fi
$VERIFIED && return 0 || return 1
Crontab Configuration
# /etc/cron.d/backup-system
PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
SHELL=/bin/bash
MAILTO=backup-team@dodatech.com
# Database backup every 6 hours
0 */6 * * * root /opt/backup-system/backup.sh >> /var/log/backup-system/cron.log 2>&1
# File backup daily at 2 AM
0 2 * * * root /opt/backup-system/files.sh >> /var/log/backup-system/cron.log 2>&1
# Backup rotation daily at 4 AM
0 4 * * * root /opt/backup-system/rotate.sh >> /var/log/backup-system/cron.log 2>&1
# Verification after each backup (runs via backup.sh, separate check here)
30 */6 * * * root /opt/backup-system/verify.sh >> /var/log/backup-system/cron.log 2>&1
# Weekly summary report
0 9 * * 1 root /opt/backup-system/summary.py >> /var/log/backup-system/cron.log 2>&1
Monitoring and Dashboard
#!/usr/bin/env python3
"""Backup system monitoring dashboard."""
import os
import json
from datetime import datetime
class BackupDashboard:
def __init__(self, log_dir='/var/log/backup-system'):
self.log_dir = log_dir
def get_latest_status(self):
metrics_file = os.path.join(self.log_dir, 'metrics.csv')
if not os.path.exists(metrics_file):
return {'status': 'unknown'}
with open(metrics_file) as f:
lines = f.readlines()
if not lines:
return {'status': 'unknown'}
last_line = lines[-1].strip()
parts = last_line.split(',')
if len(parts) >= 4:
return {
'job': parts[0],
'status': parts[1],
'duration': parts[2],
'size': parts[3],
'timestamp': datetime.fromtimestamp(int(parts[4])).isoformat(),
}
return {'status': 'unknown'}
def get_24h_summary(self):
metrics_file = os.path.join(self.log_dir, 'metrics.csv')
if not os.path.exists(metrics_file):
return {'total': 0, 'success': 0, 'failed': 0}
cutoff = datetime.now().timestamp() - 86400
total = 0
success = 0
failed = 0
with open(metrics_file) as f:
for line in f:
parts = line.strip().split(',')
if len(parts) >= 5 and int(parts[4]) > cutoff:
total += 1
if parts[1] == 'success':
success += 1
else:
failed += 1
return {'total': total, 'success': success, 'failed': failed}
def print_report(self):
status = self.get_latest_status()
summary = self.get_24h_summary()
print("=" * 50)
print("BACKUP SYSTEM STATUS REPORT")
print("=" * 50)
print(f"Time: {datetime.now().isoformat()}")
print(f"Host: {os.uname().nodename}")
print()
print("Last backup:")
if status['status'] != 'unknown':
print(f" Job: {status['job']}")
print(f" Status: {status['status'].upper()}")
print(f" Duration: {status['duration']}s")
print(f" Size: {status['size']}")
print(f" Time: {status['timestamp']}")
else:
print(" No backup data found")
print()
print("Last 24 hours:")
print(f" Total: {summary['total']}")
print(f" OK: {summary['success']}")
print(f" Failed: {summary['failed']}")
print(f" Rate: {summary['success']/max(summary['total'],1)*100:.0f}%")
print("=" * 50)
dashboard = BackupDashboard()
dashboard.print_report()
Expected output:
==================================================
BACKUP SYSTEM STATUS REPORT
==================================================
Time: 2026-06-28T10:00:00
Host: server-01
Last backup:
Job: full-backup
Status: SUCCESS
Duration: 145s
Size: 2.3G
Time: 2026-06-28T06:00:00
Last 24 hours:
Total: 4
OK: 4
Failed: 0
Rate: 100%
==================================================
Extension Challenges
After the basic backup system works, extend it with:
Distributed backup: Use Redis leader election so only one server runs the backup when multiple servers have the same crontab.
Incremental backups: After the first full backup, only back up changed files using rsync or a change journal.
Encryption at rest: Encrypt all backups with GPG before uploading to S3, with key rotation every 90 days.
Restore testing: Automatically restore the latest backup to a staging environment every week and verify data integrity.
Cost optimization: Move backups older than 30 days to Glacier (cold storage), delete backups older than 1 year.
Common Mistakes
1. No Locking
Backup takes longer than the schedule interval, a second instance starts and writes to the same file, corrupting both backups.
2. No Verification
The backup script exits 0 but the backup file is empty or corrupted. Always verify backup integrity after creation.
3. Only One Copy
Local backup + same-machine backup = no backup. Always store backups off-server (S3, separate storage server).
4. No Retention Policy
Backups accumulate and fill the disk. Implement rotation: keep daily for 7 days, weekly for 4 weeks, monthly for 12 months.
5. Not Testing Restores
Untested backups are not backups. Regularly verify that backups can be restored. Schedule automated restore tests.
FAQ
What's Next
Now that you have built a complete backup system, explore server-sent events for real-time updates, then learn about webhooks for event-driven backend communication.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro