#!/bin/bash # backup-gitea.sh - Backup Gitea, Postgres, and Runner # Enhanced for NAS support and Runner integration set -e # Colors for output GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' # No Color # ========================================== # 1. CONFIGURATION # ========================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" COMPOSE_DIR="/home/acedanger/docker/gitea" BACKUP_DIR="/home/acedanger/backups/gitea" NAS_DIR="/mnt/share/media/backups/gitea" COMPOSE_FILE="$COMPOSE_DIR/docker-compose.yml" LOG_FILE="$SCRIPT_DIR/logs/gitea-backup.log" DATE=$(date +%Y%m%d_%H%M%S) # Ensure directories exist mkdir -p "$(dirname "$LOG_FILE")" mkdir -p "$BACKUP_DIR" # Load .env variables from the COMPOSE_DIR to ensure DB credentials match if [ -f "$COMPOSE_DIR/.env" ]; then export $(grep -v '^#' "$COMPOSE_DIR/.env" | xargs) fi # Logging function (Fixed to interpret colors correctly) log() { # Print to console with colors (interpreting escapes with -e) echo -e "$(date '+%Y-%m-%d %H:%M:%S') - $1" # Strip colors for the log file to keep it clean echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | sed 's/\x1b\[[0-9;]*m//g' >> "$LOG_FILE" } # Display usage information usage() { echo "Usage: $0 [OPTIONS]" echo "" echo "Backup Gitea data, Runner, and PostgreSQL database" echo "Options:" echo " -h, --help Show this help message" echo " -l, --list List available local backups" echo " -n, --no-nas Skip copying to NAS (Local only)" echo "" } # Check dependencies check_dependencies() { if ! command -v docker &> /dev/null; then log "${RED}Error: docker is not installed.${NC}" exit 1 fi # Verify the compose file exists where we expect it if [ ! -f "$COMPOSE_FILE" ]; then log "${RED}Error: Docker compose file not found at: $COMPOSE_FILE${NC}" log "${YELLOW}Please update the COMPOSE_DIR variable in this script.${NC}" exit 1 fi } # List available backups list_backups() { echo -e "${BLUE}=== Available Gitea Backups ===${NC}" ls -lh "$BACKUP_DIR"/*.tar.gz 2>/dev/null || echo "No backups found." } # ========================================== # 2. BACKUP LOGIC # ========================================== perform_backup() { local SKIP_NAS=$1 log "Starting backup process..." # Switch context to the directory where Gitea is actually running cd "$COMPOSE_DIR" || { log "${RED}Could not change to directory $COMPOSE_DIR${NC}"; exit 1; } # PRE-FLIGHT CHECK: Is the DB actually running? if ! docker compose ps --services --filter "status=running" | grep -q "db"; then log "${RED}CRITICAL ERROR: The 'db' service is not running in $COMPOSE_DIR${NC}" log "${YELLOW}Docker sees these running services:$(docker compose ps --services --filter "status=running" | xargs)${NC}" log "Aborting backup to prevent empty files." exit 1 fi # Create a temporary staging directory for this specific backup TEMP_BACKUP_PATH="$BACKUP_DIR/temp_$DATE" mkdir -p "$TEMP_BACKUP_PATH" # 1. Backup Database log "Step 1/5: Dumping PostgreSQL database..." # Using -T to disable TTY allocation (fixes some cron issues) if docker compose exec -T db pg_dump -U "${POSTGRES_USER:-gitea}" "${POSTGRES_DB:-gitea}" > "$TEMP_BACKUP_PATH/database.sql"; then echo -e "${GREEN}Database dump successful.${NC}" else log "${RED}Database dump failed!${NC}" rm -rf "$TEMP_BACKUP_PATH" exit 1 fi # 2. Backup Gitea Data log "Step 2/5: Backing up Gitea data volume..." docker run --rm \ --volumes-from gitea \ -v "$TEMP_BACKUP_PATH":/backup \ alpine tar czf /backup/gitea_data.tar.gz -C /data . # 3. Backup Runner Data log "Step 3/5: Backing up Runner data..." # Check if runner exists before backing up to avoid errors if you removed it if docker compose ps --services | grep -q "runner"; then docker run --rm \ --volumes-from gitea-runner \ -v "$TEMP_BACKUP_PATH":/backup \ alpine tar czf /backup/runner_data.tar.gz -C /data . else log "${YELLOW}Runner service not found, skipping runner backup.${NC}" fi # 4. Config Files & Restore Script log "Step 4/5: Archiving configurations and generating restore script..." cp "$COMPOSE_FILE" "$TEMP_BACKUP_PATH/" [ -f ".env" ] && cp ".env" "$TEMP_BACKUP_PATH/" # Generate the Restore Script inside the backup folder create_restore_script "$TEMP_BACKUP_PATH" # 5. Final Archive Creation log "Step 5/5: Compressing full backup..." FINAL_ARCHIVE_NAME="gitea_backup_$DATE.tar.gz" # Tar the temp folder into one final file tar -czf "$BACKUP_DIR/$FINAL_ARCHIVE_NAME" -C "$TEMP_BACKUP_PATH" . # Remove temp folder rm -rf "$TEMP_BACKUP_PATH" log "${GREEN}Local Backup completed: $BACKUP_DIR/$FINAL_ARCHIVE_NAME${NC}" # 6. NAS Transfer if [[ "$SKIP_NAS" != "true" ]]; then if [ -d "$NAS_DIR" ]; then log "Copying to NAS ($NAS_DIR)..." cp "$BACKUP_DIR/$FINAL_ARCHIVE_NAME" "$NAS_DIR/" if [ $? -eq 0 ]; then log "${GREEN}NAS Copy Successful.${NC}" else log "${RED}NAS Copy Failed. Check permissions on $NAS_DIR${NC}" fi else log "${YELLOW}NAS Directory $NAS_DIR not found. Skipping NAS copy.${NC}" fi else log "NAS copy skipped by user request." fi # 7. Cleanup Old Local Backups (Keep 7 Days) find "$BACKUP_DIR" -name "gitea_backup_*.tar.gz" -mtime +7 -exec rm {} \; log "Cleanup of old local backups complete." } # Function to generate the restore script create_restore_script() { local TARGET_DIR=$1 cat > "$TARGET_DIR/restore.sh" << 'EOF' #!/bin/bash # RESTORE SCRIPT echo "WARNING: This will overwrite your current Gitea/DB/Runner data." read -p "Are you sure? (y/N): " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1; fi docker compose down echo "Restoring Database Volume..." docker compose up -d db echo "Waiting for DB to initialize..." sleep 15 cat database.sql | docker compose exec -T db psql -U ${POSTGRES_USER:-gitea} -d ${POSTGRES_DB:-gitea} echo "Restoring Gitea Files..." docker run --rm --volumes-from gitea -v $(pwd):/backup alpine tar xzf /backup/gitea_data.tar.gz -C /data echo "Restoring Runner Files..." docker run --rm --volumes-from gitea-runner -v $(pwd):/backup alpine tar xzf /backup/runner_data.tar.gz -C /data echo "Restarting stack..." docker compose up -d echo "Restore Complete." EOF chmod +x "$TARGET_DIR/restore.sh" } # ========================================== # 3. EXECUTION FLOW # ========================================== check_dependencies # Parse Arguments if [ $# -eq 0 ]; then perform_backup "false" exit 0 fi while [[ "$#" -gt 0 ]]; do case $1 in -h|--help) usage; exit 0 ;; -l|--list) list_backups; exit 0 ;; -n|--no-nas) perform_backup "true"; exit 0 ;; *) echo "Unknown parameter: $1"; usage; exit 1 ;; esac shift done