#!/bin/bash # Backup complet Docker + (CasaOS si présent) + Portainer # + export docker-compose + rapport HTML + index # + last/ + rotation + publication WEB (HTML only) ############################# # CONFIG ############################# SOURCE_DIR="/home/docker" # Base des backups datés BACKUP_DIR="/home/backup/docker/compose" MAX_BACKUPS=3 DATE_RAW="$(date +'%Y%m%d_%H%M%S')" DATE_DIR="$(date +'%Y.%m.%d_%H-%M-%S')" # Dossier daté du run courant RUN_DIR="$BACKUP_DIR/$DATE_DIR" # Dossier "dernier backup" LAST_DIR="$BACKUP_DIR/last" # Archives dans le dossier daté BACKUP_FILE="$RUN_DIR/docker_backup.tar.gz" CASAOS_FULL_TAR="$RUN_DIR/casaos_full_backup.tar.gz" # Dossier de configs / compose dans le run CONFIG_DIR="$RUN_DIR" # Logs LOG_FILE="/var/log/docker_backup.log" # Rapports HTML HTML_REPORT="$RUN_DIR/report.html" HTML_REPORT_LAST="$LAST_DIR/report.html" HTML_INDEX_LAST="$LAST_DIR/index.html" # Publication WEB (HTML uniquement) WEB_PUBLIC_DIR="/home/www-adm1n/docker_compose" # Dossiers à exclure (chemins relatifs à $SOURCE_DIR) EXCLUDE_DIRS=( "docker_var_lib" "1_Backups" "casaos_data/*" "nginx_proxy/data/logs/*" "yt-dl/video/*" ) ############################# # MODE TTY / CRON ############################# # Si pas de terminal (cron), on désactive les couleurs if [ -t 1 ]; then USE_COLORS=1 else USE_COLORS=0 fi ############################# # COULEURS ############################# if [ "$USE_COLORS" = "1" ]; then NC="\e[0m" GREEN="\e[32m" YELLOW="\e[33m" CYAN="\e[36m" RED="\e[31m" else NC="" GREEN="" YELLOW="" CYAN="" RED="" fi ############################# # DÉTECTION CASAOS ############################# CASAOS_PRESENT=0 if [ -d "/var/lib/casaos" ] || [ -d "$SOURCE_DIR/casaos_data" ] || command -v casaos >/dev/null 2>&1; then CASAOS_PRESENT=1 fi ############################# # FONCTIONS ############################# log_message() { local msg="$1" local now now="$(date +'%Y-%m-%d %H:%M:%S')" echo "$now - $msg" >> "$LOG_FILE" echo -e "$now - $msg" } format_time() { local t=$1 ((h=t/3600)) ((m=(t%3600)/60)) ((s=t%60)) printf "%02d:%02d:%02d\n" $h $m $s } check_pigz() { if ! command -v pigz >/dev/null 2>&1; then log_message "pigz n'est pas installé." if [ "$USE_COLORS" = "1" ]; then echo -e "${YELLOW}pigz n'est pas installé. Voulez-vous l’installer ? (y/n)${NC}" read -r rep if [ "$rep" = "y" ]; then sudo apt update && sudo apt install -y pigz || { echo -e "${RED}Échec installation pigz. Abandon.${NC}" exit 1 } else echo -e "${RED}pigz requis. Abandon.${NC}" exit 1 fi else echo "ERREUR: pigz requis (mode cron non-interactif)." exit 1 fi fi } rotate_backups() { log_message "Rotation backups (max $MAX_BACKUPS)..." # Liste uniquement les dossiers datés, ignore last/ mapfile -t backups_dirs < <( find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d \ ! -name "last" \ -printf "%T@ %p\n" 2>/dev/null \ | sort -nr \ | awk '{print $2}' ) if [ "${#backups_dirs[@]}" -gt "$MAX_BACKUPS" ]; then for d in "${backups_dirs[@]:$MAX_BACKUPS}"; do log_message "Suppression ancien backup : $d" rm -rf "$d" done fi } get_size() { du -sh "$1" 2>/dev/null | awk '{print $1}' } html_escape() { sed -e 's/&/\&/g' \ -e 's//\>/g' } html_line() { # $1=label $2=value $3=emoji $4=class echo "
$3$1$2
" >> "$HTML_REPORT" } html_container_table() { local dirs dirs=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \ ! -path "$SOURCE_DIR/casaos_data" \ ! -path "$SOURCE_DIR/docker_var_lib" \ ! -path "$SOURCE_DIR/portainer" \ 2>/dev/null) if [ -z "$dirs" ]; then echo "
Aucun dossier Docker trouvé.
" >> "$HTML_REPORT" return fi echo "
" >> "$HTML_REPORT" echo "" >> "$HTML_REPORT" echo "" >> "$HTML_REPORT" # tri par taille desc du -sh $dirs 2>/dev/null | sort -hr | while read -r size path; do name="$(basename "$path")" esc_path="$(echo "$path" | html_escape)" echo "" >> "$HTML_REPORT" done echo "
📦 Container📏 Taille📁 Chemin
$name$size$esc_path
" >> "$HTML_REPORT" } generate_index_html() { local base_dir="$1" local output_file="$2" cat > "$output_file" < Index Backups Docker

📦 Index des Backups Docker

Dossier backups: $base_dir
ℹ️ Les backups datés ne sont pas exposés sur le web (sécurité). Seul le rapport LAST est accessible.

EOF # Liste dossiers datés (ignore last) find "$base_dir" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null \ | sort -r \ | while read -r d; do name="$(basename "$d")" tarfile="$d/docker_backup.tar.gz" report="$d/report.html" mtime="$(date -r "$d" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || echo 'N/A')" if [ -f "$tarfile" ]; then tarsize="$(du -sh "$tarfile" 2>/dev/null | awk '{print $1}')" else tarsize="N/A" fi if [ -f "$report" ]; then status="OK" badge="badge-ok" else status="missing report" badge="badge-warn" fi cat >> "$output_file" <
📄 $name
$status
🕒 $mtime — 🗜️ docker_backup.tar.gz : $tarsize
EOF done cat >> "$output_file" < EOF } ############################# # DÉBUT SCRIPT ############################# SECONDS=0 check_pigz mkdir -p "$RUN_DIR" mkdir -p "$BACKUP_DIR" touch "$LOG_FILE" echo -e "${CYAN}=== Backup Docker + Configs (Portainer / CasaOS) ===${NC}" log_message "Début sauvegarde..." log_message "Dossier backup (run) : $RUN_DIR" log_message "Dossier backup (last): $LAST_DIR" log_message "CasaOS détecté : $CASAOS_PRESENT" ############################################## # 1) Archive Docker ############################################## log_message "Création archive Docker : $BACKUP_FILE" EXCLUDE_ARGS=( "--exclude=*.log" "--exclude=.composer" "--exclude=.aptitude" "--exclude=.cache" "--exclude=.cmake" "--exclude=.yarn" "--exclude=.w3m" "--exclude=.pip" "--exclude=.pm2" "--exclude=.pm" "--exclude=.bundle" "--exclude=.gem" "--exclude=.cpan" "--exclude=.cpanm" "--exclude=.git" "--exclude=.local" "--exclude=.npm" "--exclude=.nvm" "--exclude=.rvm" "--exclude=node_modules" "--exclude=lost+found" ) for d in "${EXCLUDE_DIRS[@]}"; do EXCLUDE_ARGS+=( "--exclude=$d" ) done tar -cf "$BACKUP_FILE" -I pigz \ --directory="$SOURCE_DIR" \ "${EXCLUDE_ARGS[@]}" \ . 2>> "$LOG_FILE" if [ $? -ne 0 ]; then log_message "ERREUR lors de la création du tar Docker." echo -e "${RED}Erreur lors de la création de l’archive Docker.${NC}" exit 1 fi ############################################## # 2) Extraction docker-compose CasaOS (si présent) ############################################## if [ "$CASAOS_PRESENT" = "1" ]; then log_message "Extraction des docker-compose CasaOS..." if [ -d "$SOURCE_DIR/casaos_data" ]; then find "$SOURCE_DIR/casaos_data" -maxdepth 3 -type f -name "docker-compose.y*ml" | while read -r file; do container_name=$(basename "$(dirname "$file")") dest_dir="$CONFIG_DIR/casaos/$container_name" mkdir -p "$dest_dir" cp "$file" "$dest_dir/" log_message " [CasaOS] compose : $file" done fi else log_message "CasaOS non détecté → ignoré." fi ############################################## # 3) Extraction docker-compose Docker (hors CasaOS) ############################################## log_message "Extraction des docker-compose Docker..." find "$SOURCE_DIR" -mindepth 2 -maxdepth 2 -type f -name "docker-compose.y*ml" \ ! -path "$SOURCE_DIR/casaos_data/*" \ ! -path "$SOURCE_DIR/docker_var_lib/*" \ 2>/dev/null | while read -r file; do container_name=$(basename "$(dirname "$file")") dest_dir="$CONFIG_DIR/Compose/$container_name" mkdir -p "$dest_dir" cp "$file" "$dest_dir/" log_message " [Docker] compose : $file" done ############################################## # 4) Backup config Portainer (si volume présent) ############################################## PORTAINER_CONFIG_DIR="$CONFIG_DIR/portainer" PORTAINER_FILES=0 if docker volume inspect portainer_data >/dev/null 2>&1; then log_message "Sauvegarde config Portainer..." mkdir -p "$PORTAINER_CONFIG_DIR" cp -r /var/lib/docker/volumes/portainer_data/_data/* "$PORTAINER_CONFIG_DIR/" 2>>"$LOG_FILE" PORTAINER_FILES=$(find "$PORTAINER_CONFIG_DIR" -type f 2>/dev/null | wc -l) else PORTAINER_CONFIG_DIR="" fi ############################################## # 5) Backup CasaOS (config + FULL TAR) si présent ############################################## CASAOS_CONFIG_DIR="$CONFIG_DIR/casaos/full_config" if [ "$CASAOS_PRESENT" = "1" ]; then if [ -d "/var/lib/casaos" ]; then log_message "Sauvegarde config CasaOS full..." mkdir -p "$CASAOS_CONFIG_DIR" cp -r /var/lib/casaos/* "$CASAOS_CONFIG_DIR/" 2>>"$LOG_FILE" fi CASAOS_TAR_SOURCES=() [ -d "/var/lib/casaos" ] && CASAOS_TAR_SOURCES+=( "/var/lib/casaos" ) [ -d "$SOURCE_DIR/casaos_data" ] && CASAOS_TAR_SOURCES+=( "$SOURCE_DIR/casaos_data" ) if [ "${#CASAOS_TAR_SOURCES[@]}" -gt 0 ]; then log_message "Création archive CasaOS FULL : $CASAOS_FULL_TAR" tar -cf "$CASAOS_FULL_TAR" -I pigz "${CASAOS_TAR_SOURCES[@]}" 2>>"$LOG_FILE" fi fi ############################################## # 6) Stats + rotation ############################################## CASAOS_COMPOSE_BACKUP=0 if [ "$CASAOS_PRESENT" = "1" ]; then CASAOS_COMPOSE_BACKUP=$(find "$CONFIG_DIR/casaos" -maxdepth 3 -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l) fi DOCKER_COMPOSE_BACKUP=$(find "$CONFIG_DIR/Compose" -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l) SOURCE_COMPOSE_COUNT=$(find "$SOURCE_DIR" -mindepth 2 -maxdepth 2 -type f -name "docker-compose.y*ml" \ ! -path "$SOURCE_DIR/casaos_data/*" \ ! -path "$SOURCE_DIR/docker_var_lib/*" \ 2>/dev/null | wc -l) CONTAINERS_DOCKER=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \ ! -path "$SOURCE_DIR/casaos_data" \ ! -path "$SOURCE_DIR/docker_var_lib" \ ! -path "$SOURCE_DIR/portainer" 2>/dev/null | wc -l) rotate_backups TOTAL_BACKUP_SETS=$(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null | wc -l) mapfile -t CURRENT_SETS < <(find "$BACKUP_DIR" -mindepth 1 -maxdepth 1 -type d ! -name "last" 2>/dev/null | sort) OLDEST_DIR="${CURRENT_SETS[0]}" NEWEST_DIR="${CURRENT_SETS[${#CURRENT_SETS[@]}-1]}" DURATION="$(format_time $SECONDS)" TAR_SIZE="$(get_size "$BACKUP_FILE")" CASAOS_FULL_TAR_SIZE="" [ -f "$CASAOS_FULL_TAR" ] && CASAOS_FULL_TAR_SIZE="$(get_size "$CASAOS_FULL_TAR")" PORTAINER_CONFIG_SIZE="" [ -n "$PORTAINER_CONFIG_DIR" ] && [ -d "$PORTAINER_CONFIG_DIR" ] && PORTAINER_CONFIG_SIZE="$(get_size "$PORTAINER_CONFIG_DIR")" ############################################## # 7) Génération rapport HTML (run daté) ############################################## cat > "$HTML_REPORT" < Backup Docker Report - $DATE_DIR

📦 Backup Docker / CasaOS / Portainer

📅 $DATE_DIR — 🕒 $(date +'%Y-%m-%d %H:%M:%S')

🔧 Global

EOF # lignes global html_line "Dossier backup courant" "$(echo "$RUN_DIR" | html_escape)" "📁" "info" html_line "Durée totale" "$DURATION" "⏱️" "info" html_line "Nombre de sets de backup" "$TOTAL_BACKUP_SETS" "🗂️" "info" echo "

🐳 Docker

" >> "$HTML_REPORT" html_line "Containers détectés" "$CONTAINERS_DOCKER" "🧱" "ok" html_line "Compose source" "$SOURCE_COMPOSE_COUNT" "📄" "info" html_line "Compose backup" "$DOCKER_COMPOSE_BACKUP" "📦" "info" html_line "Archive Docker (.tar.gz)" "$(echo "$BACKUP_FILE ($TAR_SIZE)" | html_escape)" "🗜️" "ok" echo "

🏠 CasaOS

" >> "$HTML_REPORT" if [ "$CASAOS_PRESENT" = "1" ]; then html_line "CasaOS détecté" "OUI" "✅" "ok" html_line "Compose CasaOS backup" "$CASAOS_COMPOSE_BACKUP" "📄" "info" if [ -f "$CASAOS_FULL_TAR" ]; then html_line "Archive CasaOS FULL (.tar.gz)" "$(echo "$CASAOS_FULL_TAR ($CASAOS_FULL_TAR_SIZE)" | html_escape)" "🗜️" "ok" else html_line "Archive CasaOS FULL (.tar.gz)" "Non générée" "⚠️" "warn" fi else html_line "CasaOS détecté" "NON (ignoré)" "🚫" "warn" fi echo "

🧭 Portainer

" >> "$HTML_REPORT" html_line "Fichiers config Portainer" "$PORTAINER_FILES" "📁" "info" [ -n "$PORTAINER_CONFIG_SIZE" ] && html_line "Taille config Portainer" "$PORTAINER_CONFIG_SIZE" "📏" "info" echo "

📊 Containers / Tailles (dossiers dans $SOURCE_DIR)

" >> "$HTML_REPORT" html_container_table cat >> "$HTML_REPORT" < ✅ Rapport généré automatiquement par le script de backup.
⚠️ Ne partage pas ce rapport publiquement : il révèle des chemins et des infos de structure.
EOF log_message "Rapport HTML généré : $HTML_REPORT" ################## ############################################## # 8) MAJ last/ (SANS copier les archives .gz) ############################################## log_message "Mise à jour du dossier LAST : $LAST_DIR" rm -rf "$LAST_DIR" mkdir -p "$LAST_DIR" # HTML cp -f "$HTML_REPORT" "$LAST_DIR/report.html" # Compose / configs (pas d'archives) [ -d "$CONFIG_DIR/Compose" ] && cp -a "$CONFIG_DIR/Compose" "$LAST_DIR/" [ -d "$CONFIG_DIR/portainer" ] && cp -a "$CONFIG_DIR/portainer" "$LAST_DIR/" [ "$CASAOS_PRESENT" = "1" ] && [ -d "$CONFIG_DIR/casaos" ] && cp -a "$CONFIG_DIR/casaos" "$LAST_DIR/" # Index HTML last/ generate_index_html "$BACKUP_DIR" "$HTML_INDEX_LAST" log_message "LAST mis à jour ✅" log_message "Dernier rapport HTML : $HTML_REPORT_LAST" log_message "Index HTML last : $HTML_INDEX_LAST" ############################################## # 9) Publication WEB (HTML UNIQUEMENT) ############################################## log_message "Publication web (HTML only) vers : $WEB_PUBLIC_DIR" rm -rf "$WEB_PUBLIC_DIR" mkdir -p "$WEB_PUBLIC_DIR" cp -f "$LAST_DIR/report.html" "$WEB_PUBLIC_DIR/report.html" cp -f "$LAST_DIR/index.html" "$WEB_PUBLIC_DIR/" 2>/dev/null || true log_message "Publication web OK ✅" ############################################## # 10) Rapport CONSOLE ############################################## echo -e "${GREEN}===== BACKUP TERMINÉ ✅ =====${NC}" ############################################## # LISTE DES DOSSIERS DOCKER PAR TAILLE (ASCII) ############################################## echo "" echo -e "${CYAN}----- LISTE/TAILLE DES RÉPERTOIRES DOCKER -----${NC}" DOCKER_DIRS=$(find "$SOURCE_DIR" -mindepth 1 -maxdepth 1 -type d \ ! -path "$SOURCE_DIR/casaos_data" \ ! -path "$SOURCE_DIR/docker_var_lib" \ ! -path "$SOURCE_DIR/portainer" \ 2>/dev/null) if [ -z "$DOCKER_DIRS" ]; then echo -e "${YELLOW}(Aucun dossier Docker trouvé)${NC}" else echo -e "${CYAN}┌──────────────────────────┬─────────┬────────────────────────────────────────┐${NC}" printf "${CYAN}│ %-24s │ %-7s │ %-38s │${NC}\n" "Container" "Taille" "Chemin" echo -e "${CYAN}├──────────────────────────┼─────────┼────────────────────────────────────────┤${NC}" # du + tri par taille desc du -sh $DOCKER_DIRS 2>/dev/null | sort -hr | while read -r size path; do name=$(basename "$path") printf "│ %-24s │ %-7s │ %-38s │\n" "$name" "$size" "$path" done echo -e "${CYAN}└──────────────────────────┴─────────┴────────────────────────────────────────┘${NC}" fi ############################################## echo -e "${CYAN}Dossier backup courant : ${YELLOW}$RUN_DIR${NC}" echo "" echo -e "${CYAN}----- DOCKER -----${NC}" echo "Containers Docker détectés : $CONTAINERS_DOCKER" echo "Compose source (Docker) : $SOURCE_COMPOSE_COUNT" echo "Compose backup (Docker) : $DOCKER_COMPOSE_BACKUP" echo "Archive Docker (run) : $BACKUP_FILE ($TAR_SIZE)" echo "" echo -e "${CYAN}----- CASAOS -----${NC}" if [ "$CASAOS_PRESENT" = "1" ]; then echo "CasaOS détecté : OUI" echo "Compose CasaOS backup : $CASAOS_COMPOSE_BACKUP" [ -f "$CASAOS_FULL_TAR" ] && echo "Archive CasaOS FULL (run) : $CASAOS_FULL_TAR ($CASAOS_FULL_TAR_SIZE)" else echo "CasaOS détecté : NON (ignoré)" fi echo "" echo -e "${CYAN}----- PORTAINER -----${NC}" echo "Fichiers config Portainer : $PORTAINER_FILES" [ -n "$PORTAINER_CONFIG_SIZE" ] && echo "Taille Portainer (run) : $PORTAINER_CONFIG_SIZE" echo "" echo -e "${CYAN}----- GLOBAL -----${NC}" echo "Sets de backup : $TOTAL_BACKUP_SETS" echo "Oldest backup : ${OLDEST_DIR:-N/A}" echo "Newest backup : ${NEWEST_DIR:-N/A}" echo "Durée totale : $DURATION" echo "" echo "HTML (run) : $HTML_REPORT" echo "HTML (last) : $HTML_REPORT_LAST" echo "WEB index : $WEB_PUBLIC_DIR/index.html" echo "WEB report : $WEB_PUBLIC_DIR/report.html" ############################################## # JSON optionnel ############################################## if [ "$1" = "--json" ]; then echo "" echo "{" echo " \"date_raw\": \"$DATE_RAW\"," echo " \"date_dir\": \"$DATE_DIR\"," echo " \"run_dir\": \"$RUN_DIR\"," echo " \"last_dir\": \"$LAST_DIR\"," echo " \"docker_backup_file\": \"$BACKUP_FILE\"," echo " \"docker_backup_size\": \"$TAR_SIZE\"," echo " \"casaos_present\": $CASAOS_PRESENT," echo " \"casaos_full_backup_file\": \"${CASAOS_FULL_TAR:-}\"," echo " \"casaos_full_backup_size\": \"${CASAOS_FULL_TAR_SIZE:-}\"," echo " \"portainer_files\": $PORTAINER_FILES," echo " \"backup_sets\": $TOTAL_BACKUP_SETS," echo " \"duration\": \"$DURATION\"," echo " \"html_report_run\": \"$HTML_REPORT\"," echo " \"html_report_last\": \"$HTML_REPORT_LAST\"," echo " \"web_index\": \"$WEB_PUBLIC_DIR/index.html\"," echo " \"web_report\": \"$WEB_PUBLIC_DIR/report.html\"" echo "}" fi