Files
docker-stack-template/restore.sh
T
bruno 6f761fe8ed Rendre docker-backup.sh entierement generique via DOCKER_APP_NAME
Suppression de tous les hardcodes 'gitea' dans docker-backup.sh :
noms de backup, fichiers DB/volumes, patterns de nettoyage, script
restore genere, metadonnees, logs et notifications.

Autres corrections :
- docker-auto-update.sh : VERSION_ENDPOINT configurable au lieu
  du endpoint Gitea /api/v1/version en dur
- restore.sh : HEALTH_CHECK_URL au lieu de localhost:3000 en dur
- docker-scripts.conf : ajout VERSION_ENDPOINT
- Suppression de backup-docker.sh (script legacy non generique)
- README : synchronisation des noms de fichiers reels
- Correction config file source (gitea-scripts.conf -> docker-scripts.conf)
2026-06-26 14:13:54 -04:00

426 lines
18 KiB
Bash

#!/bin/bash
# =============================================================================
# restore.sh — Restauration d'application Docker depuis un backup
# =============================================================================
# Usage :
# bash restore.sh <chemin_backup> → Restauration interactive
# bash restore.sh <chemin_backup> --force → Sans confirmation
# bash restore.sh <chemin_backup> --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 commune ─────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONFIG_FILE="${CONFIG_FILE:-$SCRIPT_DIR/docker-scripts.conf}"
if [[ -f "$CONFIG_FILE" ]]; then
# shellcheck source=/dev/null
source "$CONFIG_FILE"
fi
DOCKER_APP_NAME="${DOCKER_APP_NAME:-gitea}"
# ─── 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 <chemin_backup> [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: ~/dev/git/outils/docker/\${DOCKER_APP_NAME})
TARGET_VOLUME_DATA Volume data cible (défaut: /DOCKER_CONFIG/\${DOCKER_APP_NAME}/data)
TARGET_VOLUME_MYSQL Volume MySQL cible (défaut: /DOCKER_CONFIG/\${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 <chemin_backup>"
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 <chemin_backup> [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:-$HOME/dev/git/outils/docker/$DOCKER_APP_NAME}"
TARGET_VOLUME_DATA="${TARGET_VOLUME_DATA:-/DOCKER_CONFIG/$DOCKER_APP_NAME/data}"
TARGET_VOLUME_MYSQL="${TARGET_VOLUME_MYSQL:-/DOCKER_CONFIG/$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