#!/bin/bash # ============================================================================== # Script: gitea_backup.sh # Description: Backs up Gitea (repositories, database, configuration, avatars, # LFS files, and attachments) to timestamped directories on a mounted # drive using Gitea's native `gitea dump` command via Docker exec. # Compatible with non-POSIX filesystems (exFAT, NTFS, FAT32) and # automatically prunes backups older than RETENTION_DAYS. # Usage: sudo ./gitea_backup.sh # Notes: Must be run with root/sudo privileges. # ============================================================================== # ============================================================================== # Define Variables for Source, Destination, and Retention # ============================================================================== BACKUP_MOUNT_POINT="/mnt/5TB-Disk1" GITEA_DATA_DIR="/srv/docker/gitea" GITEA_DESTINATION="$BACKUP_MOUNT_POINT/backup/services/gitea" CONTAINER_NAME="gitea" RETENTION_DAYS=10 # Timestamp and Folder Setup TIMESTAMP=$(date +"%Y-%m-%d_%H%M%S") CURRENT_BACKUP_DIR="$GITEA_DESTINATION/backup_$TIMESTAMP" LATEST_TXT_FILE="$GITEA_DESTINATION/latest.txt" # ntfy Notification Configuration NTFY_TOPIC="Server" NTFY_SERVER="https://ntfy.speerfam.net" NTFY_TOKEN="tk_3xo9mejjtgemyowzckqw2souo3mux" # ============================================================================== # Pre-flight Checks # ============================================================================== # Check if the mount point directory exists and is a mounted filesystem. if ! mountpoint -q "$BACKUP_MOUNT_POINT"; then echo "Error: '$BACKUP_MOUNT_POINT' is not a mount point. Please ensure the drive is mounted." >&2 curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Error: Script failed. Destination '$BACKUP_MOUNT_POINT' is not mounted." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1 exit 1 fi # Check if the Gitea data directory exists on the host. if [ ! -d "$GITEA_DATA_DIR" ]; then echo "Error: Gitea data directory '$GITEA_DATA_DIR' not found." >&2 curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Error: Script failed. Gitea data directory '$GITEA_DATA_DIR' not found." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1 exit 1 fi # Check if the Docker container is running. if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER_NAME}$"; then echo "Error: Docker container '$CONTAINER_NAME' is not running." >&2 curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Error: Script failed. Docker container '$CONTAINER_NAME' is not running." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1 exit 1 fi # Create target directory for current run mkdir -p "$CURRENT_BACKUP_DIR" # ============================================================================== # Gitea Backup Execution # ============================================================================== echo "Starting Gitea backup ($TIMESTAMP)..." # Step 1: Ensure staging folder exists and trigger in-container dump echo "Executing 'gitea dump' inside container '$CONTAINER_NAME'..." docker exec -u git -w /tmp "$CONTAINER_NAME" bash -c 'mkdir -p /data/gitea/tmp && /usr/local/bin/gitea dump -c /data/gitea/conf/app.ini -f /data/gitea/tmp/gitea-dump.zip' DUMP_STATUS=$? if [ $DUMP_STATUS -ne 0 ]; then echo "Error: 'gitea dump' command failed with exit code $DUMP_STATUS." >&2 curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Error: Gitea internal dump command failed." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1 rm -rf "$CURRENT_BACKUP_DIR" exit 1 fi # Step 2: Verify dump file exists on host data volume GITEA_DUMP_FILE="$GITEA_DATA_DIR/gitea/tmp/gitea-dump.zip" if [ ! -f "$GITEA_DUMP_FILE" ]; then echo "Error: Could not locate generated 'gitea-dump.zip' in '$GITEA_DATA_DIR/gitea/tmp'." >&2 curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Error: Gitea backup zip file missing after exec." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1 rm -rf "$CURRENT_BACKUP_DIR" exit 1 fi # Step 3: Rsync dump archive to destination directory echo "Rsyncing Gitea dump archive to '$CURRENT_BACKUP_DIR/gitea-dump.zip'..." rsync -av --no-owner --no-group "$GITEA_DUMP_FILE" "$CURRENT_BACKUP_DIR/gitea-dump.zip" RSYNC_STATUS=$? # Cleanup temporary dump file from host source data directory rm -f "$GITEA_DUMP_FILE" if [ $RSYNC_STATUS -ne 0 ]; then echo "Error: rsync failed to copy Gitea dump archive file." >&2 curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Error: rsync failed during Gitea backup transfer." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1 exit 1 fi # Step 4: Record current backup path to latest.txt echo "$CURRENT_BACKUP_DIR" > "$LATEST_TXT_FILE" # ============================================================================== # Retention Cleanup (Prune Backups Older than RETENTION_DAYS) # ============================================================================== echo "Checking for backups older than $RETENTION_DAYS days..." find "$GITEA_DESTINATION" -maxdepth 1 -type d -name "backup_*" -mtime +$RETENTION_DAYS -exec rm -rf {} + # ============================================================================== # Completion and Notification # ============================================================================== echo "Success: Gitea backup completed at '$CURRENT_BACKUP_DIR'." curl -H "Authorization: Bearer $NTFY_TOKEN" -d "Success: Gitea backup completed ($TIMESTAMP). Retaining $RETENTION_DAYS days." "$NTFY_SERVER/$NTFY_TOPIC" > /dev/null 2>&1