#!/bin/bash # ============================================================================= # restore.sh — Restauration d'application Docker depuis un backup # ============================================================================= # Usage : # bash restore.sh → Restauration interactive # bash restore.sh --force → Sans confirmation # bash restore.sh --dry-run → Vérification sans exécuter # # Exemples : # bash restore.sh /NFS/BACKUP/DOCKER/\$HOSTNAME/\${DOCKER_APP_NAME}/\${DOCKER_APP_NAME}-backup-20260626-020000 # bash restore.sh /NFS/BACKUP/DOCKER/\$HOSTNAME/\${DOCKER_APP_NAME}/\${DOCKER_APP_NAME}-backup-20260626-020000 --dry-run # ============================================================================= set -euo pipefail # ─── Charger la configuration ──────────────────────────────────────────────── # Ordre : 1) config globale (template) → 2) config spécifique à l'app (surcharge) SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" GLOBAL_CONFIG="${SCRIPT_DIR}/docker-scripts.conf" if [[ -f "$GLOBAL_CONFIG" ]]; then # shellcheck source=/dev/null source "$GLOBAL_CONFIG" fi # ─── Nom de l'application (défaut si pas dans la config) ───────────────────── DOCKER_APP_NAME="${DOCKER_APP_NAME:-gitea}" # ─── Configuration locale (après chargement global, permet surcharge env) ──── COMPOSE_DIR="${COMPOSE_DIR:-/srv/docker/$DOCKER_APP_NAME}" COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}" # Charger la config spécifique à l'application (surcharge la config globale) APP_CONFIG="${COMPOSE_DIR}/docker-scripts.conf" if [[ -f "$APP_CONFIG" ]] && [[ "$APP_CONFIG" != "$GLOBAL_CONFIG" ]]; then # shellcheck source=/dev/null source "$APP_CONFIG" fi # ─── Couleurs ─────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m' ok() { echo -e " ${GREEN}✅${NC} $1"; } warn() { echo -e " ${YELLOW}⚠️${NC} $1"; } err() { echo -e " ${RED}❌${NC} $1"; } info() { echo -e " ${CYAN}📋${NC} $1"; } # ─── Usage ────────────────────────────────────────────────────────────────── usage() { cat << EOF Usage: $0 [OPTIONS] Arguments : chemin_backup Dossier du backup à restaurer (ex: ${DOCKER_APP_NAME}-backup-YYYYMMDD-HHMMSS) Options : --force Ne pas demander confirmation --dry-run Affiche ce qui serait fait sans exécuter --help Cette aide Variables d'environnement : TARGET_COMPOSE_DIR Dossier docker-compose cible (défaut: /srv/docker/\${DOCKER_APP_NAME}) TARGET_VOLUME_DATA Volume data cible (défaut: /srv/docker/\${DOCKER_APP_NAME}/data) TARGET_VOLUME_MYSQL Volume MySQL cible (défaut: /srv/docker/\${DOCKER_APP_NAME}/mysql) DB_ROOT_PASSWORD Mot de passe root MySQL (défaut: \${DOCKER_APP_NAME}) DB_NAME Nom base de données (défaut: \${DOCKER_APP_NAME}) EOF exit 0 } # ─── Vérifications ────────────────────────────────────────────────────────── check_prerequisites() { local missing=0 if ! command -v docker &>/dev/null; then err "Docker n'est pas installé" missing=1 fi if ! command -v tar &>/dev/null; then err "tar n'est pas installé" missing=1 fi if docker compose version &>/dev/null; then DOCKER_COMPOSE="docker compose" elif command -v docker-compose &>/dev/null; then DOCKER_COMPOSE="docker-compose" else err "Ni 'docker compose' ni 'docker-compose' n'est disponible" missing=1 fi return $missing } # ─── Lister les backups disponibles ───────────────────────────────────────── list_backups() { local dir="${1:-.}" echo "" echo "Backups disponibles :" echo "─────────────────────" local count=0 for d in "$dir"/${DOCKER_APP_NAME}-backup-*; do if [[ -d "$d" ]]; then count=$((count + 1)) local info_file="$d/backup-info.txt" local date_info="" if [[ -f "$info_file" ]]; then date_info=$(grep "^Date" "$info_file" 2>/dev/null | sed 's/Date[[:space:]]*:[[:space:]]*//' || echo "inconnue") fi echo " $count. $(basename "$d") ($date_info)" fi done if [[ $count -eq 0 ]]; then echo " Aucun backup trouvé dans $dir" return 1 fi echo "" echo "Utilisez : $0 " return 0 } # ═════════════════════════════════════════════════════════════════════════════ # MAIN # ═════════════════════════════════════════════════════════════════════════════ FORCE=false DRY_RUN=false BACKUP_PATH="" # Détection docker compose (par défaut, sera surchargé après check) DOCKER_COMPOSE="docker compose" for arg in "$@"; do case "$arg" in --force) FORCE=true ;; --dry-run) DRY_RUN=true ;; --help|-h) usage ;; --*) echo "Option inconnue : $arg"; usage ;; *) if [[ -z "$BACKUP_PATH" ]]; then BACKUP_PATH="$arg" fi ;; esac done # Si aucun backup spécifié, lister les backups et suggérer if [[ -z "$BACKUP_PATH" ]]; then # Chercher dans les emplacements courants FOUND=false for search_dir in "/NFS/BACKUP/DOCKER/$HOSTNAME/$DOCKER_APP_NAME" "$HOME/backups/$DOCKER_APP_NAME" "."; do if [[ -d "$search_dir" ]]; then list_backups "$search_dir" && FOUND=true && break fi done if [[ "$FOUND" != "true" ]]; then echo "" echo "Usage: $0 [OPTIONS]" echo " $0 --help pour plus d'informations" fi exit 1 fi # Résoudre le chemin absolu BACKUP_PATH="$(cd "$(dirname "$BACKUP_PATH")" 2>/dev/null && pwd)/$(basename "$BACKUP_PATH")" || BACKUP_PATH="$(realpath "$BACKUP_PATH" 2>/dev/null || echo "$BACKUP_PATH")" # Vérifier que le dossier de backup existe et est valide if [[ ! -d "$BACKUP_PATH" ]]; then err "Dossier de backup introuvable : $BACKUP_PATH" echo "" echo "Recherche de backups dans les emplacements standards..." for search_dir in "/NFS/BACKUP/DOCKER/$HOSTNAME/$DOCKER_APP_NAME" "$HOME/backups/$DOCKER_APP_NAME" "."; do if [[ -d "$search_dir" ]]; then list_backups "$search_dir" break fi done exit 1 fi # Vérifier les fichiers requis MISSING_FILES=() for required_file in "${DOCKER_APP_NAME}-mysql.tar.gz" "${DOCKER_APP_NAME}-data.tar.gz" "docker-compose.yml"; do if [[ ! -f "$BACKUP_PATH/$required_file" ]]; then MISSING_FILES+=("$required_file") fi done # Vérifier le dump SQL (compressé ou non) if [[ ! -f "$BACKUP_PATH/${DOCKER_APP_NAME}-db.sql.gz" ]] && [[ ! -f "$BACKUP_PATH/${DOCKER_APP_NAME}-db.sql" ]]; then MISSING_FILES+=("${DOCKER_APP_NAME}-db.sql ou ${DOCKER_APP_NAME}-db.sql.gz") fi if [[ ${#MISSING_FILES[@]} -gt 0 ]]; then err "Fichiers requis manquants dans le backup :" for f in "${MISSING_FILES[@]}"; do err " - $f" done echo "" warn "Ce dossier ne semble pas être un backup valide pour ${DOCKER_APP_NAME}." exit 1 fi echo "" echo "════════════════════════════════════════════════════════" echo "🔄 RESTAURATION" echo "════════════════════════════════════════════════════════" echo "" # ─── Configuration cible ──────────────────────────────────────────────────── TARGET_COMPOSE_DIR="${TARGET_COMPOSE_DIR:-/srv/docker/$DOCKER_APP_NAME}" TARGET_VOLUME_DATA="${TARGET_VOLUME_DATA:-/srv/docker/$DOCKER_APP_NAME/data}" TARGET_VOLUME_MYSQL="${TARGET_VOLUME_MYSQL:-/srv/docker/$DOCKER_APP_NAME/mysql}" DB_ROOT_PASSWORD="${DB_ROOT_PASSWORD:-$DOCKER_APP_NAME}" DB_NAME="${DB_NAME:-$DOCKER_APP_NAME}" DB_USER="${DB_USER:-$DOCKER_APP_NAME}" # ─── Afficher les infos du backup ──────────────────────────────────────────── BACKUP_NAME=$(basename "$BACKUP_PATH") BACKUP_DATE=${BACKUP_NAME#${DOCKER_APP_NAME}-backup-} if [[ -f "$BACKUP_PATH/backup-info.txt" ]]; then info "Informations du backup :" while IFS= read -r line; do echo " $line" done < "$BACKUP_PATH/backup-info.txt" echo "" else info "Backup : $BACKUP_NAME" echo "" fi info "Configuration cible :" echo " Compose dir : $TARGET_COMPOSE_DIR" echo " Volume data : $TARGET_VOLUME_DATA" echo " Volume MySQL: $TARGET_VOLUME_MYSQL" echo " DB name : $DB_NAME" echo "" # ─── Vérifier les prérequis ───────────────────────────────────────────────── check_prerequisites || { echo "" err "Prérequis manquants — impossible de continuer" exit 1 } # ─── Dry-run ──────────────────────────────────────────────────────────────── if [[ "$DRY_RUN" == "true" ]]; then echo "🔍 DRY RUN — aucune action ne sera effectuée" echo "" echo "Actions qui seraient exécutées :" echo "" echo " 1. Arrêter les conteneurs ${DOCKER_APP_NAME} existants" echo " cd $TARGET_COMPOSE_DIR && $DOCKER_COMPOSE down" echo "" echo " 2. Créer les répertoires cibles" echo " mkdir -p $TARGET_VOLUME_DATA $TARGET_VOLUME_MYSQL" echo "" echo " 3. Restaurer le volume MySQL" echo " rm -rf ${TARGET_VOLUME_MYSQL}/*" echo " tar -xzf $BACKUP_PATH/${DOCKER_APP_NAME}-mysql.tar.gz -C $(dirname "$TARGET_VOLUME_MYSQL")" echo "" echo " 4. Restaurer le volume ${DOCKER_APP_NAME}" echo " rm -rf ${TARGET_VOLUME_DATA}/*" echo " tar -xzf $BACKUP_PATH/${DOCKER_APP_NAME}-data.tar.gz -C $(dirname "$TARGET_VOLUME_DATA")" echo "" echo " 5. Copier docker-compose.yml" echo " cp $BACKUP_PATH/docker-compose.yml $TARGET_COMPOSE_DIR/" echo "" if [[ -f "$BACKUP_PATH/app.ini" ]]; then echo " 6. Restaurer app.ini" echo " mkdir -p ${TARGET_VOLUME_DATA}/${DOCKER_APP_NAME}/conf" echo " cp $BACKUP_PATH/app.ini ${TARGET_VOLUME_DATA}/${DOCKER_APP_NAME}/conf/" echo "" fi echo " 7. Démarrer les conteneurs" echo " cd $TARGET_COMPOSE_DIR && $DOCKER_COMPOSE up -d" echo "" echo " 8. Restaurer la base de données" echo " docker exec -i ${DOCKER_APP_NAME}_db mysql -u root -p$DB_ROOT_PASSWORD $DB_NAME < dump" echo "" echo " 9. Redémarrer" echo " $DOCKER_COMPOSE restart" echo "" exit 0 fi # ─── Confirmation ─────────────────────────────────────────────────────────── if [[ "$FORCE" != "true" ]]; then echo "⚠️ ATTENTION : Cette opération va ÉCRASER l'installation ${DOCKER_APP_NAME} existante !" echo " Toute donnée non sauvegardée sera PERDUE." echo "" echo " Les conteneurs seront arrêtés et les volumes remplacés." echo "" read -p "Taper 'RESTORE' (en majuscules) pour confirmer : " CONFIRM if [[ "$CONFIRM" != "RESTORE" ]]; then echo "❌ Restauration annulée." exit 0 fi echo "" fi # ═════════════════════════════════════════════════════════════════════════════ # RESTAURATION # ═════════════════════════════════════════════════════════════════════════════ TIMING_START=$(date +%s) # ─── Étape 1 : Arrêter les conteneurs existants ───────────────────────────── echo "📦 Étape 1/7 : Arrêt des conteneurs existants..." if [[ -f "$TARGET_COMPOSE_DIR/docker-compose.yml" ]] || [[ -f "$TARGET_COMPOSE_DIR/$COMPOSE_FILE" ]]; then cd "$TARGET_COMPOSE_DIR" 2>/dev/null && $DOCKER_COMPOSE down 2>&1 || true fi ok "Conteneurs arrêtés" # ─── Étape 2 : Créer les répertoires ──────────────────────────────────────── echo "📦 Étape 2/7 : Création des répertoires cibles..." mkdir -p "$TARGET_VOLUME_DATA" "$TARGET_VOLUME_MYSQL" "$TARGET_COMPOSE_DIR" ok "Répertoires créés" # ─── Étape 3 : Restaurer volume MySQL ─────────────────────────────────────── echo "📦 Étape 3/7 : Restauration volume MySQL..." rm -rf "${TARGET_VOLUME_MYSQL:?}"/* if ! tar -xzf "$BACKUP_PATH/${DOCKER_APP_NAME}-mysql.tar.gz" -C "$(dirname "$TARGET_VOLUME_MYSQL")"; then err "Échec de l'extraction du volume MySQL" exit 1 fi ok "Volume MySQL restauré" # ─── Étape 4 : Restaurer volume ${DOCKER_APP_NAME} ─────────────────────────── echo "📦 Étape 4/7 : Restauration volume ${DOCKER_APP_NAME}..." rm -rf "${TARGET_VOLUME_DATA:?}"/* if ! tar -xzf "$BACKUP_PATH/${DOCKER_APP_NAME}-data.tar.gz" -C "$(dirname "$TARGET_VOLUME_DATA")"; then err "Échec de l'extraction du volume ${DOCKER_APP_NAME}" exit 1 fi ok "Volume ${DOCKER_APP_NAME} restauré" # ─── Étape 5 : Copier docker-compose ──────────────────────────────────────── echo "📦 Étape 5/7 : Copie docker-compose et configuration..." cp "$BACKUP_PATH/docker-compose.yml" "$TARGET_COMPOSE_DIR/" ok "docker-compose.yml copié" if [[ -f "$BACKUP_PATH/app.ini" ]]; then mkdir -p "${TARGET_VOLUME_DATA}/${DOCKER_APP_NAME}/conf" cp "$BACKUP_PATH/app.ini" "${TARGET_VOLUME_DATA}/${DOCKER_APP_NAME}/conf/app.ini" ok "app.ini restauré" fi # ─── Étape 6 : Démarrer les conteneurs ────────────────────────────────────── echo "📦 Étape 6/7 : Démarrage des conteneurs..." cd "$TARGET_COMPOSE_DIR" if ! $DOCKER_COMPOSE up -d; then err "Échec du démarrage des conteneurs" warn "Vérifiez les logs : $DOCKER_COMPOSE logs" exit 1 fi ok "Conteneurs démarrés" # Attendre que MySQL soit prêt echo " ⏳ Attente démarrage MySQL..." MYSQL_READY=false sleep 10 for i in $(seq 1 30); do if docker exec ${DOCKER_APP_NAME}_db mysqladmin ping -u root -p"$DB_ROOT_PASSWORD" --silent 2>/dev/null; then echo " ✅ MySQL prêt après $((10 + i))s" MYSQL_READY=true break fi sleep 1 done if [[ "$MYSQL_READY" != "true" ]]; then warn "MySQL n'a pas répondu — on tente la restauration DB quand même..." fi # ─── Étape 7 : Restaurer la base de données ───────────────────────────────── echo "📦 Étape 7/7 : Restauration base de données..." # Créer la base de données si elle n'existe pas docker exec ${DOCKER_APP_NAME}_db mysql -u root -p"$DB_ROOT_PASSWORD" -e "CREATE DATABASE IF NOT EXISTS \`$DB_NAME\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" 2>/dev/null || true # Restaurer le dump if [[ -f "$BACKUP_PATH/${DOCKER_APP_NAME}-db.sql.gz" ]]; then gunzip -c "$BACKUP_PATH/${DOCKER_APP_NAME}-db.sql.gz" | docker exec -i ${DOCKER_APP_NAME}_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" 2>&1 || { err "Échec de la restauration du dump SQL (compressé)" exit 1 } elif [[ -f "$BACKUP_PATH/${DOCKER_APP_NAME}-db.sql" ]]; then docker exec -i ${DOCKER_APP_NAME}_db mysql -u root -p"$DB_ROOT_PASSWORD" "$DB_NAME" < "$BACKUP_PATH/${DOCKER_APP_NAME}-db.sql" 2>&1 || { err "Échec de la restauration du dump SQL" exit 1 } fi ok "Base de données restaurée" # ─── Redémarrage final ────────────────────────────────────────────────────── echo "" echo "🔄 Redémarrage final des conteneurs..." $DOCKER_COMPOSE restart 2>&1 || true # ─── Vérification santé ───────────────────────────────────────────────────── echo "" echo "🏥 Vérification que ${DOCKER_APP_NAME} répond..." for i in $(seq 1 30); do STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${HEALTH_CHECK_URL:-http://localhost:3000}" 2>/dev/null || echo "000") if [[ "$STATUS" == "200" || "$STATUS" == "302" || "$STATUS" == "303" ]]; then ok "${DOCKER_APP_NAME} répond (HTTP $STATUS) après ${i}s" break fi if [[ $i -eq 30 ]]; then warn "${DOCKER_APP_NAME} ne répond pas encore — vérifiez les logs" fi sleep 1 done # ─── Résumé ───────────────────────────────────────────────────────────────── TIMING_END=$(date +%s) DURATION=$((TIMING_END - TIMING_START)) echo "" echo "════════════════════════════════════════════════════════" echo "✅ RESTAURATION TERMINÉE !" echo "" echo " Durée : ${DURATION}s" echo " Backup : $BACKUP_NAME" echo " Cible : $TARGET_COMPOSE_DIR" echo " ${DOCKER_APP_NAME} : ${HEALTH_CHECK_URL:-http://localhost:3000}" echo "" echo " Logs : $DOCKER_COMPOSE logs -f" echo " Status : $DOCKER_COMPOSE ps" echo "════════════════════════════════════════════════════════" exit 0