From 5b17022856b29531331a991ffc676f6f41925e64 Mon Sep 17 00:00:00 2001 From: Peter Wood Date: Tue, 18 Nov 2025 20:36:02 -0500 Subject: [PATCH] feat: Enhance backup-gitea.sh for NAS support and Runner integration; add restore-gitea.sh script --- backup-gitea.sh | 376 +++++++++++++++++++++-------------------------- restore-gitea.sh | 124 ++++++++++++++++ 2 files changed, 291 insertions(+), 209 deletions(-) mode change 100644 => 100755 backup-gitea.sh create mode 100755 restore-gitea.sh diff --git a/backup-gitea.sh b/backup-gitea.sh old mode 100644 new mode 100755 index 51192da..3cb0766 --- a/backup-gitea.sh +++ b/backup-gitea.sh @@ -1,8 +1,7 @@ #!/bin/bash -# backup-gitea.sh - Backup Gitea data and PostgreSQL database -# Author: Shell Repository -# Description: Comprehensive backup solution for Gitea with PostgreSQL database +# backup-gitea.sh - Backup Gitea, Postgres, and Runner +# Enhanced for NAS support and Runner integration set -e @@ -13,255 +12,214 @@ RED='\033[0;31m' BLUE='\033[0;34m' NC='\033[0m' # No Color -# Configuration +# ========================================== +# 1. CONFIGURATION +# ========================================== SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +COMPOSE_DIR="/home/acedanger/docker/gitea" + BACKUP_DIR="/home/acedanger/backups/gitea" -COMPOSE_DIR="/home/acedanger/docker/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 logs directory exists +# Ensure directories exist mkdir -p "$(dirname "$LOG_FILE")" +mkdir -p "$BACKUP_DIR" -# Logging function +# 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() { - echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE" + # 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 and PostgreSQL database" - echo "" + echo "Backup Gitea data, Runner, and PostgreSQL database" echo "Options:" echo " -h, --help Show this help message" - echo " -d, --dry-run Show what would be backed up without doing it" - echo " -f, --force Force backup even if one was recently created" - echo " -r, --restore FILE Restore from specified backup directory" - echo " -l, --list List available backups" - echo " -c, --cleanup Clean up old backups (keeps last 7 days)" - echo " --keep-days DAYS Number of days to keep backups (default: 7)" + echo " -l, --list List available local backups" + echo " -n, --no-nas Skip copying to NAS (Local only)" echo "" - echo "Examples:" - echo " $0 # Regular backup" - echo " $0 --dry-run # See what would be backed up" - echo " $0 --list # List available backups" - echo " $0 --restore /path/to/backup # Restore from backup" } # Check dependencies check_dependencies() { - local missing_deps=() - - command -v docker >/dev/null 2>&1 || missing_deps+=("docker") - command -v docker-compose >/dev/null 2>&1 || missing_deps+=("docker-compose") - - if [ ${#missing_deps[@]} -ne 0 ]; then - echo -e "${RED}Error: Missing required dependencies: ${missing_deps[*]}${NC}" - echo "Please install the missing dependencies and try again." + if ! command -v docker &> /dev/null; then + log "${RED}Error: docker is not installed.${NC}" exit 1 fi - - # Check if docker-compose file exists + + # Verify the compose file exists where we expect it if [ ! -f "$COMPOSE_FILE" ]; then - echo -e "${RED}Error: Docker compose file not found at $COMPOSE_FILE${NC}" + 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 - - # Check if we can access Docker - if ! docker info >/dev/null 2>&1; then - echo -e "${RED}Error: Cannot access Docker. Check if Docker is running and you have permissions.${NC}" - exit 1 - fi -} - -# Check if Gitea services are running -check_gitea_services() { - cd "$COMPOSE_DIR" - - if ! docker-compose ps | grep -q "Up"; then - echo -e "${YELLOW}Warning: Gitea services don't appear to be running${NC}" - echo "Some backup operations may fail if services are not running." - read -p "Continue anyway? (y/N): " -n 1 -r - echo - if [[ ! $REPLY =~ ^[Yy]$ ]]; then - echo "Backup cancelled" - exit 1 - fi - fi } # List available backups list_backups() { echo -e "${BLUE}=== Available Gitea Backups ===${NC}" - - if [ ! -d "$BACKUP_DIR" ]; then - echo -e "${YELLOW}No backup directory found at $BACKUP_DIR${NC}" - return 0 - fi - - local count=0 - - # Find backup directories - for backup_path in "$BACKUP_DIR"/gitea_backup_*; do - if [ -d "$backup_path" ]; then - local backup_name - backup_name=$(basename "$backup_path") - local backup_date - backup_date=$(echo "$backup_name" | sed 's/gitea_backup_//' | sed 's/_/ /') - local size - size=$(du -sh "$backup_path" 2>/dev/null | cut -f1) - local info_file="$backup_path/backup_info.txt" - - echo -e "${GREEN}📦 $backup_name${NC}" - echo " Date: $backup_date" - echo " Size: $size" - echo " Path: $backup_path" - - if [ -f "$info_file" ]; then - local gitea_version - gitea_version=$(grep "Gitea Version:" "$info_file" 2>/dev/null | cut -d: -f2- | xargs) - if [ -n "$gitea_version" ]; then - echo " Version: $gitea_version" - fi - fi - - echo "" - count=$((count + 1)) - fi - done - - if [ $count -eq 0 ]; then - echo -e "${YELLOW}No backups found in $BACKUP_DIR${NC}" - echo "Run a backup first to create one." - else - echo -e "${BLUE}Total backups found: $count${NC}" - fi + ls -lh "$BACKUP_DIR"/*.tar.gz 2>/dev/null || echo "No backups found." } -# Change to compose directory -cd "$COMPOSE_DIR" +# ========================================== +# 2. BACKUP LOGIC +# ========================================== +perform_backup() { + local SKIP_NAS=$1 + + log "Starting backup process..." -# Create timestamped backup directory -BACKUP_PATH="$BACKUP_DIR/gitea_backup_$DATE" -mkdir -p "$BACKUP_PATH" + # 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; } -# Backup PostgreSQL database -echo "Backing up PostgreSQL database..." -docker-compose exec -T db pg_dump -U ${POSTGRES_USER:-gitea} ${POSTGRES_DB:-gitea} > "$BACKUP_PATH/database.sql" + # 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 -# Backup Gitea data volume -echo "Backing up Gitea data volume..." -docker run --rm \ - -v gitea_gitea:/data:ro \ - -v "$BACKUP_PATH":/backup \ - alpine:latest \ - tar czf /backup/gitea_data.tar.gz -C /data . + # Create a temporary staging directory for this specific backup + TEMP_BACKUP_PATH="$BACKUP_DIR/temp_$DATE" + mkdir -p "$TEMP_BACKUP_PATH" -# Backup PostgreSQL data volume (optional, as we have the SQL dump) -echo "Backing up PostgreSQL data volume..." -docker run --rm \ - -v gitea_postgres:/data:ro \ - -v "$BACKUP_PATH":/backup \ - alpine:latest \ - tar czf /backup/postgres_data.tar.gz -C /data . + # 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 -# Copy docker-compose configuration -echo "Backing up configuration files..." -cp "$COMPOSE_FILE" "$BACKUP_PATH/" -if [ -f ".env" ]; then - cp ".env" "$BACKUP_PATH/" -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 . -# Create a restore script -cat > "$BACKUP_PATH/restore.sh" << 'EOF' + # 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 for Gitea backup +# 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 -set -e +docker compose down -RESTORE_DIR="$(dirname "$0")" -COMPOSE_DIR="/home/acedanger/docker/gitea" +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 "WARNING: This will stop Gitea and replace all data!" -read -p "Are you sure you want to continue? (yes/no): " confirm +echo "Restoring Gitea Files..." +docker run --rm --volumes-from gitea -v $(pwd):/backup alpine tar xzf /backup/gitea_data.tar.gz -C /data -if [ "$confirm" != "yes" ]; then - echo "Restore cancelled" - exit 1 +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 -cd "$COMPOSE_DIR" - -# Stop services -echo "Stopping Gitea services..." -docker-compose down - -# Remove existing volumes -echo "Removing existing volumes..." -docker volume rm gitea_gitea gitea_postgres || true - -# Recreate volumes -echo "Creating volumes..." -docker volume create gitea_gitea -docker volume create gitea_postgres - -# Restore Gitea data -echo "Restoring Gitea data..." -docker run --rm \ - -v gitea_gitea:/data \ - -v "$RESTORE_DIR":/backup:ro \ - alpine:latest \ - tar xzf /backup/gitea_data.tar.gz -C /data - -# Start database for restore -echo "Starting database for restore..." -docker-compose up -d db - -# Wait for database to be ready -echo "Waiting for database to be ready..." -sleep 10 - -# Restore database -echo "Restoring database..." -docker-compose exec -T db psql -U ${POSTGRES_USER:-gitea} -d ${POSTGRES_DB:-gitea} < "$RESTORE_DIR/database.sql" - -# Start all services -echo "Starting all services..." -docker-compose up -d - -echo "Restore completed!" -EOF - -chmod +x "$BACKUP_PATH/restore.sh" - -# Create info file -cat > "$BACKUP_PATH/backup_info.txt" << EOF -Gitea Backup Information -======================== -Backup Date: $(date) -Backup Location: $BACKUP_PATH -Gitea Version: $(docker-compose exec -T server gitea --version | head -1) -PostgreSQL Version: $(docker-compose exec -T db postgres --version) - -Files included: -- database.sql: PostgreSQL database dump -- gitea_data.tar.gz: Gitea data volume -- postgres_data.tar.gz: PostgreSQL data volume -- docker-compose.yml: Docker compose configuration -- .env: Environment variables (if exists) -- restore.sh: Restore script - -To restore this backup, run: -cd $BACKUP_PATH -./restore.sh -EOF - -# Cleanup old backups (keep last 7 days) -echo "Cleaning up old backups..." -find "$BACKUP_DIR" -type d -name "gitea_backup_*" -mtime +7 -exec rm -rf {} + 2>/dev/null || true - -echo "Backup completed successfully!" -echo "Backup saved to: $BACKUP_PATH" -echo "Backup size: $(du -sh "$BACKUP_PATH" | cut -f1)" \ No newline at end of file +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 \ No newline at end of file diff --git a/restore-gitea.sh b/restore-gitea.sh new file mode 100755 index 0000000..ce32c88 --- /dev/null +++ b/restore-gitea.sh @@ -0,0 +1,124 @@ +#!/bin/bash + +# restore-gitea.sh +# Usage: ./restore-gitea.sh + +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 + +# Check Arguments +if [ "$#" -ne 2 ]; then + echo -e "${RED}Usage: $0 ${NC}" + echo "Example: $0 ./backups/gitea_backup.tar.gz ~/docker/gitea_restore" + exit 1 +fi + +BACKUP_FILE=$(realpath "$1") +DEST_DIR="$2" + +# 1. Validation +if [ ! -f "$BACKUP_FILE" ]; then + echo -e "${RED}Error: Backup file not found at $BACKUP_FILE${NC}" + exit 1 +fi + +if [ -d "$DEST_DIR" ]; then + echo -e "${YELLOW}Warning: Destination directory '$DEST_DIR' already exists.${NC}" + echo -e "${RED}This process will overwrite files and STOP containers in that directory.${NC}" + read -p "Are you sure you want to continue? (y/N): " -n 1 -r + echo + if [[ ! $REPLY =~ ^[Yy]$ ]]; then + echo "Restore cancelled." + exit 1 + fi +else + echo -e "${BLUE}Creating destination directory: $DEST_DIR${NC}" + mkdir -p "$DEST_DIR" +fi + +# Switch to destination directory +cd "$DEST_DIR" || exit 1 + +# 2. Extract Backup Archive +echo -e "${BLUE}Step 1/6: Extracting backup archive...${NC}" +tar -xzf "$BACKUP_FILE" +echo "Extraction complete." + +# Load environment variables from the extracted .env (if it exists) +if [ -f ".env" ]; then + echo "Loading .env configuration..." + export $(grep -v '^#' .env | xargs) +fi + +# 3. Stop Existing Services & Clean Volumes +echo -e "${BLUE}Step 2/6: Preparing Docker environment...${NC}" +# We stop containers and remove volumes to ensure a clean restore state +docker compose down -v 2>/dev/null || true +echo "Environment cleaned." + +# 4. Restore Volume Data (Files) +echo -e "${BLUE}Step 3/6: Restoring Gitea Data Volume...${NC}" +# We must create the containers (no-start) first so the volume exists +docker compose create gitea + +# Helper container to extract data into the volume +docker run --rm \ + --volumes-from gitea \ + -v "$DEST_DIR":/backup \ + alpine tar xzf /backup/gitea_data.tar.gz -C /data + +echo "Gitea data restored." + +# Restore Runner Data (if present) +if [ -f "runner_data.tar.gz" ]; then + echo -e "${BLUE}Step 4/6: Restoring Runner Data Volume...${NC}" + docker compose create runner 2>/dev/null || true + if docker compose ps -a | grep -q "runner"; then + docker run --rm \ + --volumes-from gitea-runner \ + -v "$DEST_DIR":/backup \ + alpine tar xzf /backup/runner_data.tar.gz -C /data + echo "Runner data restored." + else + echo -e "${YELLOW}Runner service not defined in compose file. Skipping.${NC}" + fi +else + echo "No runner backup found. Skipping." +fi + +# 5. Restore Database +echo -e "${BLUE}Step 5/6: Restoring Database...${NC}" +# Start only the DB container +docker compose up -d db + +# Wait for Postgres to be ready +echo "Waiting for Database to initialize (15s)..." +sleep 15 + +if [ -f "database.sql" ]; then + echo "Importing SQL dump..." + cat database.sql | docker compose exec -T db psql -U "${POSTGRES_USER:-gitea}" -d "${POSTGRES_DB:-gitea}" + echo "Database import successful." +else + echo -e "${RED}Error: database.sql not found in backup!${NC}" + exit 1 +fi + +# 6. Start All Services +echo -e "${BLUE}Step 6/6: Starting Gitea...${NC}" +docker compose up -d + +# Cleanup extracted files (Optional - comment out if you want to inspect them) +# echo "Cleaning up temporary extraction files..." +# rm database.sql gitea_data.tar.gz runner_data.tar.gz + +echo -e "${GREEN}=======================================${NC}" +echo -e "${GREEN}✅ Restore Complete!${NC}" +echo -e "${GREEN}Gitea is running at: $DEST_DIR${NC}" +echo -e "${GREEN}=======================================${NC}" \ No newline at end of file