Skip to content
Open
135 changes: 135 additions & 0 deletions backup-cron.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#!/bin/bash

# Arcadia Backup Cron Wrapper
# This script is designed to be run by cron for scheduled backups
# It loads configuration from backup.conf and handles logging/locking
#
# Installation:
# 1. Copy backup.conf.example to backup.conf and customize
# 2. chmod +x backup-cron.sh
# 3. Add to crontab: 0 3 * * * /path/to/backup-cron.sh
#
# Logs are written to /var/log/arcadia-backup.log by default

set -e

# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Default configuration
LOG_FILE="${BACKUP_LOG_FILE:-/var/log/arcadia-backup.log}"
LOCK_FILE="/tmp/arcadia-backup.lock"

# ============================================================================
# LOGGING
# ============================================================================

log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" >> "$LOG_FILE"
}

log_separator() {
echo "========================================" >> "$LOG_FILE"
}

# ============================================================================
# LOCK HANDLING
# ============================================================================

# Prevent concurrent runs
if [ -f "$LOCK_FILE" ]; then
# Check if the process is still running
if [ -f "$LOCK_FILE" ]; then
OLD_PID=$(cat "$LOCK_FILE" 2>/dev/null)
if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
log "ERROR: Backup already running (PID: $OLD_PID), skipping"
exit 0
else
log "WARNING: Stale lock file found, removing"
rm -f "$LOCK_FILE"
fi
fi
fi

# Create lock file with our PID
echo $$ > "$LOCK_FILE"
trap "rm -f $LOCK_FILE" EXIT
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Fix trap expansion to prevent variable substitution at invocation time.

Use single quotes in trap to defer variable expansion until the trap is triggered:

- trap "rm -f $LOCK_FILE" EXIT
+ trap 'rm -f "$LOCK_FILE"' EXIT

As per shellcheck (SC2064), this ensures $LOCK_FILE is evaluated when the trap runs, not when it's defined.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
trap "rm -f $LOCK_FILE" EXIT
trap 'rm -f "$LOCK_FILE"' EXIT
🧰 Tools
🪛 Shellcheck (0.11.0)

[warning] 56-56: Use single quotes, otherwise this expands now rather than when signalled.

(SC2064)

🤖 Prompt for AI Agents
In backup-cron.sh around line 56 the trap is defined with double quotes which
expands $LOCK_FILE immediately; change the trap to use single quotes so
$LOCK_FILE is expanded when the trap runs (e.g., trap 'rm -f "$LOCK_FILE"' EXIT)
and keep the inner double-quotes around $LOCK_FILE to handle spaces or empty
values.


# ============================================================================
# LOAD CONFIGURATION
# ============================================================================

if [ -f "$SCRIPT_DIR/backup.conf" ]; then
source "$SCRIPT_DIR/backup.conf"
log "Configuration loaded from backup.conf"
else
log "WARNING: No backup.conf found, using defaults"
fi

# ============================================================================
# RUN BACKUP
# ============================================================================

log_separator
log "Starting scheduled backup"

cd "$SCRIPT_DIR"

# Build command arguments
BACKUP_ARGS="--db-docker"

[ "$BACKUP_ENCRYPT" = "true" ] && BACKUP_ARGS="$BACKUP_ARGS --encrypt"
[ -n "$BACKUP_PASSWORD" ] && BACKUP_ARGS="$BACKUP_ARGS --password \"$BACKUP_PASSWORD\""
[ -n "$BACKUP_PASSWORD_FILE" ] && BACKUP_ARGS="$BACKUP_ARGS --password-file \"$BACKUP_PASSWORD_FILE\""
[ -n "$BACKUP_DESTINATION" ] && BACKUP_ARGS="$BACKUP_ARGS --destination \"$BACKUP_DESTINATION\""
[ -n "$BACKUP_REMOTE_TYPE" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-type \"$BACKUP_REMOTE_TYPE\""
[ -n "$BACKUP_REMOTE_HOST" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-host \"$BACKUP_REMOTE_HOST\""
[ -n "$BACKUP_REMOTE_USER" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-user \"$BACKUP_REMOTE_USER\""
[ -n "$BACKUP_REMOTE_PATH" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-path \"$BACKUP_REMOTE_PATH\""
[ -n "$BACKUP_REMOTE_KEY" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-key \"$BACKUP_REMOTE_KEY\""
[ -n "$BACKUP_REMOTE_PORT" ] && BACKUP_ARGS="$BACKUP_ARGS --remote-port \"$BACKUP_REMOTE_PORT\""
[ "$BACKUP_DELETE_AFTER_UPLOAD" = "true" ] && BACKUP_ARGS="$BACKUP_ARGS --delete-after-upload"

# Run backup
log "Executing: ./backup.sh $BACKUP_ARGS"
eval "./backup.sh $BACKUP_ARGS" >> "$LOG_FILE" 2>&1
STATUS=$?

if [ $STATUS -eq 0 ]; then
log "Backup completed successfully"
else
log "ERROR: Backup failed with exit code $STATUS"
fi

# ============================================================================
# RETENTION POLICY
# ============================================================================

if [ -n "$BACKUP_RETENTION_DAYS" ] && [ -n "$BACKUP_DESTINATION" ]; then
log "Applying retention policy: deleting backups older than $BACKUP_RETENTION_DAYS days"

# Count files before deletion
OLD_COUNT=$(find "$BACKUP_DESTINATION" -name "arcadia_backup_*" -mtime +$BACKUP_RETENTION_DAYS 2>/dev/null | wc -l)

if [ "$OLD_COUNT" -gt 0 ]; then
find "$BACKUP_DESTINATION" -name "arcadia_backup_*" -mtime +$BACKUP_RETENTION_DAYS -delete
log "Deleted $OLD_COUNT old backup(s)"
else
log "No old backups to delete"
fi
fi

# ============================================================================
# SUMMARY
# ============================================================================

if [ -n "$BACKUP_DESTINATION" ] && [ -d "$BACKUP_DESTINATION" ]; then
BACKUP_COUNT=$(find "$BACKUP_DESTINATION" -name "arcadia_backup_*" 2>/dev/null | wc -l)
BACKUP_SIZE=$(du -sh "$BACKUP_DESTINATION" 2>/dev/null | cut -f1)
log "Current backups: $BACKUP_COUNT files, total size: $BACKUP_SIZE"
fi

log "Scheduled backup job finished"
log_separator

exit $STATUS
54 changes: 54 additions & 0 deletions backup-entrypoint.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/bin/bash

# Arcadia Backup Entrypoint
# Supports two modes:
# - oneshot: Run backup once and exit (default)
# - daemon: Run crond in foreground with scheduled backups

set -e

BACKUP_MODE="${BACKUP_MODE:-oneshot}"
BACKUP_CRON_SCHEDULE="${BACKUP_CRON_SCHEDULE:-0 3 * * *}"

# ============================================================================
# DAEMON MODE
# ============================================================================

if [ "$BACKUP_MODE" = "daemon" ]; then
echo "============================================"
echo "Arcadia Backup Daemon"
echo "============================================"
echo "Mode: daemon"
echo "Schedule: $BACKUP_CRON_SCHEDULE"
echo "Timezone: ${TZ:-UTC}"
echo ""

# Create log file
touch /var/log/arcadia-backup.log

# Create crontab entry
# The cron job runs backup-cron.sh which handles locking, logging, and retention
echo "$BACKUP_CRON_SCHEDULE /app/backup-cron.sh >> /var/log/arcadia-backup.log 2>&1" > /etc/crontabs/root

echo "Crontab configured:"
cat /etc/crontabs/root
echo ""
echo "Backup daemon started. Logs: /var/log/arcadia-backup.log"
echo "To view logs: docker logs -f arcadia_backup_daemon"
echo "============================================"
echo ""

# Tail the log file in background so docker logs shows backup output
tail -F /var/log/arcadia-backup.log 2>/dev/null &

# Run crond in foreground
exec crond -f -l 2

# ============================================================================
# ONESHOT MODE (default)
# ============================================================================

else
echo "Mode: oneshot"
exec /app/backup.sh "$@"
fi
93 changes: 93 additions & 0 deletions backup.conf.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Arcadia Backup Configuration
# =============================
# Copy this file to backup.conf and customize for your environment
#
# This configuration is used by backup-cron.sh for scheduled backups
# You can also set these as environment variables

# ============================================================================
# ENCRYPTION
# ============================================================================

# Enable encryption (AES256 via GPG)
BACKUP_ENCRYPT=true

# Encryption password (REQUIRED if BACKUP_ENCRYPT=true)
# Choose ONE of the following methods:
BACKUP_PASSWORD=your_secure_passphrase_here

# OR use a password file (more secure for automated systems)
# BACKUP_PASSWORD_FILE=/path/to/secure/password/file

# ============================================================================
# LOCAL STORAGE
# ============================================================================

# Directory where backups will be stored
BACKUP_DESTINATION=/home/ubuntu/arcadia-backups

# ============================================================================
# REMOTE STORAGE (Optional)
# ============================================================================

# Upload method: rsync or scp
# BACKUP_REMOTE_TYPE=rsync

# Remote server hostname
# BACKUP_REMOTE_HOST=backup.example.com

# Remote server username
# BACKUP_REMOTE_USER=backup

# Remote destination path
# BACKUP_REMOTE_PATH=/backups/arcadia

# SSH private key file (optional, uses default SSH key if not set)
# BACKUP_REMOTE_KEY=/home/ubuntu/.ssh/backup_key

# SSH port (default: 22)
# BACKUP_REMOTE_PORT=22

# Delete local backup after successful remote upload
# BACKUP_DELETE_AFTER_UPLOAD=false

# ============================================================================
# RETENTION POLICY
# ============================================================================

# Number of days to keep backups (older backups are automatically deleted)
BACKUP_RETENTION_DAYS=7

# ============================================================================
# LOGGING
# ============================================================================

# Log file path
BACKUP_LOG_FILE=/var/log/arcadia-backup.log

# ============================================================================
# DAEMON MODE (Docker only)
# ============================================================================
# When using backup-daemon service, cron runs inside the container.
# Set the schedule here (default: daily at 3:00 AM)

# BACKUP_CRON_SCHEDULE="0 3 * * *"

# Schedule examples:
# "0 3 * * *" - Daily at 3:00 AM
# "0 */6 * * *" - Every 6 hours
# "0 2 * * 0" - Weekly on Sunday at 2:00 AM
# "0 4 1 * *" - Monthly on the 1st at 4:00 AM
# "*/5 * * * *" - Every 5 minutes (for testing)

# ============================================================================
# MANUAL CRON SETUP (Alternative to daemon mode)
# ============================================================================
# If you prefer to use the host OS cron instead of the daemon container,
# add one of these to your crontab (crontab -e):
#
# Using backup-cron.sh directly:
# 0 3 * * * /home/ubuntu/arcadia-services/backup-cron.sh
#
# Using Docker oneshot mode:
# 0 3 * * * cd /home/ubuntu/arcadia-services && docker compose --profile backup run --rm backup
Loading