#!/bin/bash # ============================================================================= # gitea-backup.sh — Backup complet Gitea (DB + volumes + config) # ============================================================================= # À placer dans /usr/local/bin/ ou ~/scripts/ # Cron recommandé : 0 2 * * * /usr/local/bin/gitea-backup.sh # # Ce script sauvegarde : # 1. La base de données MySQL (dump SQL complet) # 2. Les volumes Docker (data, mysql) # 3. Le fichier docker-compose.yml et la config # # Le tout est horodaté et stocké dans BACKUP_DIR. # Un restore peut se faire sur N'IMPORTE QUEL hôte Docker. # # Paramètres configurables (modifier ci-dessous ou passer en variables d'env) # ============================================================================= set -euo pipefail # ─── Configuration (modifiable) ───────────────────────────────────────────── BACKUP_DIR="${BACKUP_DIR:-/NFS/BACKUP/DOCKER/dev-prod/gitea}" COMPOSE_DIR="${COMPOSE_DIR:-$HOME/dev/git/outils/docker/gitea}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" # Conteneurs DB_CONTAINER="${DB_CONTAINER:-gitea_db}" APP_CONTAINER="${APP_CONTAINER:-gitea_server}" # Base de données DB_NAME="${DB_NAME:-gitea}" DB_USER="${DB_USER:-gitea}" DB_PASSWORD="${DB_PASSWORD:-chab30}" DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-gitea}" # Volumes Docker (chemins HÔTE, pas conteneur) VOLUME_DATA="${VOLUME_DATA:-/DOCKER_CONFIG/gitea/data}" VOLUME_MYSQL="${VOLUME_MYSQL:-/DOCKER_CONFIG/gitea/mysql}" # Rétention RETENTION_DAYS="${RETENTION_DAYS:-30}" # Garder X jours de backups RETENTION_WEEKLY="${RETENTION_WEEKLY:-4}" # Garder X backups hebdomadaires (dimanche) RETENTION_MONTHLY="${RETENTION_MONTHLY:-6}" # Garder X backups mensuels (1er du mois) # Options COMPRESS="${COMPRESS:-true}" # Compression gzip VERIFY_BACKUP="${VERIFY_BACKUP:-true}" # Vérifier l'intégrité après backup STOP_CONTAINERS="${STOP_CONTAINERS:-false}" # Arrêter les conteneurs pendant le backup (plus sûr) ENABLE_NOTIFICATIONS="${ENABLE_NOTIFICATIONS:-false}" # Log LOG_FILE="${LOG_FILE:-/var/log/gitea-backup.log}" # ─── Logging ───────────────────────────────────────────────────────────────── log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } # ─── Initialisation ───────────────────────────────────────────────────────── TIMESTAMP=$(date +%Y%m%d-%H%M%S) DATE_LABEL=$(date +%Y-%m-%d) DAY_OF_WEEK=$(date +%u) # 1=lundi, 7=dimanche DAY_OF_MONTH=$(date +%d) BACKUP_NAME="gitea-backup-${TIMESTAMP}" BACKUP_PATH="${BACKUP_DIR}/${BACKUP_NAME}" RESTORE_SCRIPT="${BACKUP_PATH}/restore.sh" log "════════════════════════════════════════════════════════" log "📦 Début backup Gitea → $BACKUP_PATH" # ─── Pré-vérifications ────────────────────────────────────────────────────── mkdir -p "$BACKUP_DIR" || { log "❌ Impossible de créer $BACKUP_DIR"; exit 1; } cd "$COMPOSE_DIR" || { log "❌ Répertoire $COMPOSE_DIR introuvable"; exit 1; } mkdir -p "$BACKUP_PATH" # Vérifier espace disque (minimum 2 GB) AVAILABLE_KB=$(df "$BACKUP_DIR" | tail -1 | awk '{print $4}') if [[ $AVAILABLE_KB -lt 2097152 ]]; then log "❌ Espace disque insuffisant : $(($AVAILABLE_KB / 1024)) MB disponibles (min 2 GB requis)" exit 1 fi log "💾 Espace disque : $((AVAILABLE_KB / 1024)) MB disponibles" # ─── Optionnel : arrêter les conteneurs pour un backup consistant ─────────── if [[ "$STOP_CONTAINERS" == "true" ]]; then log "⏸️ Arrêt temporaire des conteneurs..." docker compose -f "$COMPOSE_FILE" stop 2>&1 | tee -a "$LOG_FILE" CONTAINERS_WERE_STOPPED=true else CONTAINERS_WERE_STOPPED=false fi # ─── 1. Backup base de données MySQL ──────────────────────────────────────── log "🛢️ Dump base de données MySQL ($DB_NAME)..." # Vérifier que le conteneur DB tourne if docker ps --format '{{.Names}}' | grep -q "^${DB_CONTAINER}$"; then docker exec "$DB_CONTAINER" mysqldump \ -u root -p"$DB_ROOT_PASSWORD" \ --single-transaction \ --routines \ --triggers \ --events \ --hex-blob \ --default-character-set=utf8mb4 \ "$DB_NAME" > "${BACKUP_PATH}/gitea-db.sql" 2>&1 || { log "❌ Échec du dump MySQL" [[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && docker compose -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" exit 1 } DB_SIZE=$(wc -c < "${BACKUP_PATH}/gitea-db.sql") log " ✅ Dump MySQL OK (${DB_SIZE} bytes)" else log "❌ Conteneur $DB_CONTAINER non trouvé (pas en cours d'exécution)" [[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && docker compose -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" exit 1 fi # Compresser le dump si demandé if [[ "$COMPRESS" == "true" ]]; then gzip -f "${BACKUP_PATH}/gitea-db.sql" log " 📦 Compressé : $(ls -lh "${BACKUP_PATH}/gitea-db.sql.gz" | awk '{print $5}')" fi # ─── 2. Backup volumes Docker ─────────────────────────────────────────────── log "📁 Backup volume data ($VOLUME_DATA)..." # On utilise tar depuis l'hôte directement (plus fiable que docker cp pour gros volumes) # Exclusion des fichiers temporaires et logs inutiles tar -czf "${BACKUP_PATH}/gitea-data.tar.gz" \ -C "$(dirname "$VOLUME_DATA")" \ --exclude='*.log' \ --exclude='log/*' \ --exclude='tmp/*' \ --exclude='cache/*' \ "$(basename "$VOLUME_DATA")" 2>&1 || { log "❌ Échec tar du volume data" [[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && docker compose -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" exit 1 } DATA_SIZE=$(ls -lh "${BACKUP_PATH}/gitea-data.tar.gz" | awk '{print $5}') log " ✅ Volume data sauvegardé ($DATA_SIZE)" log "📁 Backup volume MySQL ($VOLUME_MYSQL)..." tar -czf "${BACKUP_PATH}/gitea-mysql.tar.gz" \ -C "$(dirname "$VOLUME_MYSQL")" \ "$(basename "$VOLUME_MYSQL")" 2>&1 || { log "❌ Échec tar du volume MySQL" [[ "$CONTAINERS_WERE_STOPPED" == "true" ]] && docker compose -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" exit 1 } MYSQL_SIZE=$(ls -lh "${BACKUP_PATH}/gitea-mysql.tar.gz" | awk '{print $5}') log " ✅ Volume MySQL sauvegardé ($MYSQL_SIZE)" # ─── 3. Sauvegarde docker-compose + config ────────────────────────────────── log "📋 Sauvegarde docker-compose et configuration..." cp "$COMPOSE_DIR/$COMPOSE_FILE" "${BACKUP_PATH}/docker-compose.yml" # Sauvegarder aussi le app.ini si présent if [[ -f "${VOLUME_DATA}/gitea/conf/app.ini" ]]; then cp "${VOLUME_DATA}/gitea/conf/app.ini" "${BACKUP_PATH}/app.ini" log " ✅ app.ini sauvegardé" fi # ─── Redémarrer les conteneurs si on les avait arrêtés ────────────────────── if [[ "$CONTAINERS_WERE_STOPPED" == "true" ]]; then log "▶️ Redémarrage des conteneurs..." docker compose -f "$COMPOSE_FILE" start 2>&1 | tee -a "$LOG_FILE" # Attendre que Gitea réponde for i in $(seq 1 30); do if curl -s -o /dev/null -w "%{http_code}" "http://localhost:3000" 2>/dev/null | grep -qE "200|302|303"; then log "✅ Gitea répond après ${i}s" break fi sleep 1 done fi # ─── 4. Générer le script de restore ──────────────────────────────────────── log "📝 Génération du script de restore..." cat > "$RESTORE_SCRIPT" << RESTORE_EOF #!/bin/bash # ============================================================================= # RESTORE SCRIPT — Gitea Backup $TIMESTAMP # Généré le $(date) # ============================================================================= # Usage : bash restore.sh [OPTIONS] # # Ce script restaure COMPLÈTEMENT Gitea sur n'importe quel hôte Docker : # 1. Crée les volumes Docker # 2. Restaure la base de données MySQL # 3. Restaure les données Gitea # 4. Lance les conteneurs # # Options : # --dry-run Affiche ce qui serait fait sans exécuter # --force Ne pas demander confirmation # --help Cette aide # ============================================================================= set -euo pipefail # ─── Configuration cible ──────────────────────────────────────────────────── TARGET_COMPOSE_DIR="\${TARGET_COMPOSE_DIR:-\$HOME/dev/git/outils/docker/gitea}" TARGET_VOLUME_DATA="\${TARGET_VOLUME_DATA:-/DOCKER_CONFIG/gitea/data}" TARGET_VOLUME_MYSQL="\${TARGET_VOLUME_MYSQL:-/DOCKER_CONFIG/gitea/mysql}" DB_ROOT_PASSWORD="\${DB_ROOT_PASSWORD:-gitea}" DB_NAME="\${DB_NAME:-gitea}" DB_USER="\${DB_USER:-gitea}" DB_PASSWORD="\${DB_PASSWORD:-chab30}" DRY_RUN=false FORCE=false for arg in "\$@"; do case "\$arg" in --dry-run) DRY_RUN=true ;; --force) FORCE=true ;; --help) head -30 "\$0"; exit 0 ;; esac done SCRIPT_DIR="\$(cd "\$(dirname "\$0")" && pwd)" BACKUP_DIR="\$SCRIPT_DIR" echo "════════════════════════════════════════════════════════" echo "🔄 RESTAURATION GITEA" echo " Source : \$BACKUP_DIR" echo " Date : $TIMESTAMP" echo " Cible : \$TARGET_COMPOSE_DIR" echo "════════════════════════════════════════════════════════" if [[ "\$DRY_RUN" == "true" ]]; then echo "" echo "🔍 DRY RUN — aucune action ne sera effectuée" echo "" echo "Actions qui seraient exécutées :" echo " 1. mkdir -p \$TARGET_VOLUME_DATA \$TARGET_VOLUME_MYSQL" echo " 2. Extraire gitea-mysql.tar.gz → \$TARGET_VOLUME_MYSQL" echo " 3. Extraire gitea-data.tar.gz → \$TARGET_VOLUME_DATA" echo " 4. Copier docker-compose.yml → \$TARGET_COMPOSE_DIR/" echo " 5. docker compose up -d" echo " 6. Attendre MySQL prêt" RESTORE_EOF # Continuer le script restore (le heredoc était trop long pour un seul bloc) cat >> "$RESTORE_SCRIPT" << 'RESTORE_EOF' echo " 7. docker exec gitea_db mysql ... < gitea-db.sql" echo " 8. docker compose restart" echo "" exit 0 fi if [[ "$FORCE" != "true" ]]; then echo "" echo "⚠️ ATTENTION : Ceci va ÉCRASER les données Gitea existantes !" echo " Toute donnée non sauvegardée sera PERDUE." echo "" read -p "Confirmer la restauration ? (taper 'RESTORE' en majuscules) : " CONFIRM if [[ "$CONFIRM" != "RESTORE" ]]; then echo "❌ Restauration annulée." exit 0 fi fi echo "" echo "📦 Étape 1/6 : Création des répertoires..." mkdir -p "$TARGET_VOLUME_DATA" "$TARGET_VOLUME_MYSQL" "$TARGET_COMPOSE_DIR" echo "📦 Étape 2/6 : Restauration volume MySQL..." rm -rf "${TARGET_VOLUME_MYSQL:?}"/* tar -xzf "$BACKUP_DIR/gitea-mysql.tar.gz" -C "$(dirname "$TARGET_VOLUME_MYSQL")" echo "📦 Étape 3/6 : Restauration volume Gitea..." rm -rf "${TARGET_VOLUME_DATA:?}"/* tar -xzf "$BACKUP_DIR/gitea-data.tar.gz" -C "$(dirname "$TARGET_VOLUME_DATA")" echo "📦 Étape 4/6 : Copie docker-compose..." cp "$BACKUP_DIR/docker-compose.yml" "$TARGET_COMPOSE_DIR/" # Restaurer app.ini si présent if [[ -f "$BACKUP_DIR/app.ini" ]]; then mkdir -p "${TARGET_VOLUME_DATA}/gitea/conf" cp "$BACKUP_DIR/app.ini" "${TARGET_VOLUME_DATA}/gitea/conf/app.ini" echo " ✅ app.ini restauré" fi echo "📦 Étape 5/6 : Démarrage des conteneurs..." cd "$TARGET_COMPOSE_DIR" docker compose up -d echo " ⏳ Attente démarrage MySQL (30s)..." sleep 10 for i in $(seq 1 20); do if docker exec gitea_db mysqladmin ping -u root -p"$DB_ROOT_PASSWORD" --silent 2>/dev/null; then echo " ✅ MySQL prêt après $((10 + i))s" break fi sleep 1 done echo "📦 Étape 6/6 : Restauration base de données..." DB_DUMP="$BACKUP_DIR/gitea-db.sql" if [[ -f "${DB_DUMP}.gz" ]]; then gunzip -c "${DB_DUMP}.gz" | docker exec -i gitea_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" else docker exec -i gitea_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" < "$DB_DUMP" fi echo "" echo "🔄 Redémarrage final des conteneurs..." docker compose restart echo "" echo "════════════════════════════════════════════════════════" echo "✅ RESTAURATION TERMINÉE !" echo "" echo " Gitea : http://localhost:3000" echo " Vérifiez les logs : docker compose logs -f" echo "════════════════════════════════════════════════════════" RESTORE_EOF chmod +x "$RESTORE_SCRIPT" log " ✅ Script restore créé : restore.sh" # ─── 5. Vérification intégrité ────────────────────────────────────────────── if [[ "$VERIFY_BACKUP" == "true" ]]; then log "🔍 Vérification intégrité des archives..." ERRORS=0 for ARCHIVE in "$BACKUP_PATH"/*.tar.gz; do if ! tar -tzf "$ARCHIVE" > /dev/null 2>&1; then log " ❌ Archive corrompue : $ARCHIVE" ERRORS=$((ERRORS + 1)) fi done if [[ "$COMPRESS" == "true" ]]; then if ! gunzip -t "${BACKUP_PATH}/gitea-db.sql.gz" 2>/dev/null; then log " ❌ Dump SQL corrompu" ERRORS=$((ERRORS + 1)) fi fi if [[ $ERRORS -gt 0 ]]; then log "❌ $ERRORS erreur(s) d'intégrité détectée(s)" exit 1 fi log " ✅ Toutes les archives sont valides" fi # ─── 6. Métadonnées du backup ─────────────────────────────────────────────── cat > "${BACKUP_PATH}/backup-info.txt" << INFO Date : $(date) Hôte : $(hostname) Gitea version : $(docker exec "$APP_CONTAINER" gitea --version 2>/dev/null || echo "inconnue") DB type : MySQL 8 DB name : $DB_NAME Compression : $COMPRESS Containers stopped : $STOP_CONTAINERS Taille DB : $(ls -lh "${BACKUP_PATH}"/gitea-db.sql* | awk '{print $5}') Taille data : $DATA_SIZE Taille mysql : $MYSQL_SIZE INFO # ─── 7. Nettoyage des vieux backups ───────────────────────────────────────── log "🧹 Nettoyage backups (rétention: ${RETENTION_DAYS}j, ${RETENTION_WEEKLY} hebdo, ${RETENTION_MONTHLY} mensuels)..." # Supprimer les backups plus vieux que RETENTION_DAYS (sauf dimanche et 1er du mois) find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" -mtime +"$RETENTION_DAYS" | while read -r backup; do BASENAME=$(basename "$backup") # Garder les backups du dimanche et du 1er du mois (gérés séparément) log " 🗑️ Suppression : $BASENAME" rm -rf "$backup" done # Nettoyage hebdomadaire : garder les N derniers dimanches if [[ "$DAY_OF_WEEK" == "7" ]]; then find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" | sort -r | tail -n +$((RETENTION_WEEKLY + 1)) | while read -r backup; do # Ne supprimer que si c'est un backup du dimanche # (on ne touche pas aux backups mensuels du 1er) [[ "$(basename "$backup")" =~ 01- ]] && continue log " 🗑️ Suppression hebdo : $(basename "$backup")" rm -rf "$backup" done fi # Nettoyage mensuel : garder les N derniers 1er du mois if [[ "$DAY_OF_MONTH" == "01" ]]; then find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*01-*" | sort -r | tail -n +$((RETENTION_MONTHLY + 1)) | while read -r backup; do log " 🗑️ Suppression mensuel : $(basename "$backup")" rm -rf "$backup" done fi # ─── 8. Rapport final ─────────────────────────────────────────────────────── TOTAL_SIZE=$(du -sh "$BACKUP_PATH" | cut -f1) BACKUP_COUNT=$(find "$BACKUP_DIR" -maxdepth 1 -type d -name "gitea-backup-*" | wc -l) log "✅ Backup terminé !" log "📊 Total : $TOTAL_SIZE" log "📋 Backups conservés : $BACKUP_COUNT" log "📂 Destination : $BACKUP_PATH" log "═══ Fin ───────────────────────────────────────────" echo "" echo "📂 $BACKUP_PATH" echo " ├── gitea-db.sql.gz ← Dump MySQL" echo " ├── gitea-data.tar.gz ← Volume /data" echo " ├── gitea-mysql.tar.gz ← Volume MySQL" echo " ├── docker-compose.yml ← Stack Docker" echo " ├── restore.sh ← Script de restore" echo " └── backup-info.txt ← Métadonnées" exit 0