#!/bin/bash ############################# # CONFIG ############################# # Répertoire contenant tes containers SOURCE_DIR="/home/docker" # Répertoire où stocker les sauvegardes BACKUP_DIR="/home/backup/docker" # Nombre max de backups à conserver (dossiers datés) MAX_BACKUPS=3 # Date interne (pour logs / JSON) DATE_RAW="$(date +'%Y%m%d_%H%M%S')" # Date pour le dossier de backup (format humain demandé) DATE_DIR="$(date +'%Y.%m.%d_%H-%M-%S')" # Dossier de ce backup RUN_DIR="$BACKUP_DIR/$DATE_DIR" # Fichiers TAR dans le dossier de ce backup BACKUP_FILE="$RUN_DIR/docker_backup.tar.gz" CASAOS_FULL_TAR="$RUN_DIR/casaos_full_backup.tar.gz" # Dossier où l'on met les compose / configs CONFIG_DIR="$RUN_DIR" # Dossiers à exclure (chemins relatifs à $SOURCE_DIR pour le tar Docker) EXCLUDE_DIRS=( "docker_var_lib" "1_Backups" "casaos_data/*" "nginx_proxy/data/logs/*" "yt-dl/video/*" ) # LOGS LOG_FILE="/var/log/docker_backup.log" ############################# # COULEURS ############################# NC="\e[0m" GREEN="\e[32m" YELLOW="\e[33m" CYAN="\e[36m" RED="\e[31m" ############################# # 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 echo -e "${YELLOW}pigz n'est pas installé. Voulez-vous l’installer ? (y/n)${NC}" read rep if [ "$rep" = "y" ]; then sudo apt update && sudo apt install -y pigz || { echo -e "${RED}Échec de l’installation de pigz. Abandon.${NC}" exit 1 } else echo -e "${RED}pigz est requis pour ce script. Abandon.${NC}" exit 1 fi fi } rotate_backups() { log_message "Rotation des anciens backups (conserver les $MAX_BACKUPS plus récents)..." # On ne garde que les dossiers datés dans BACKUP_DIR mapfile -t backups_dirs < <(ls -1td "$BACKUP_DIR"/*/ 2>/dev/null || true) if [ "${#backups_dirs[@]}" -gt "$MAX_BACKUPS" ]; then for d in "${backups_dirs[@]:$MAX_BACKUPS}"; do log_message "Suppression ancien backup (dossier) : $d" rm -rf "$d" done fi } get_size() { du -sh "$1" 2>/dev/null | awk '{print $1}' } ############################# # DÉBUT SCRIPT ############################# SECONDS=0 check_pigz mkdir -p "$RUN_DIR" touch "$LOG_FILE" echo -e "${CYAN}=== Backup Docker + Configs (Portainer / CasaOS) ===${NC}" log_message "Début sauvegarde Docker + configs Portainer & CasaOS..." log_message "Dossier de backup : $RUN_DIR" ############################################## # 1) Sauvegarde TAR Docker via pigz + exclusions ############################################## log_message "Création de l’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 des docker-compose # - CasaOS: RUN_DIR/casaos// # - Docker: RUN_DIR/docker// ############################################## 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 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 trouvé : $file" done fi log_message "Extraction des docker-compose Docker (hors casaos_data & dossiers exclus)..." 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 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 trouvé : $file" done ############################################## # 3) Config Portainer (full) ############################################## 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 ############################################## # 4) Config CasaOS (full + tar.gz) ############################################## CASAOS_CONFIG_DIR="$CONFIG_DIR/casaos/full_config" if [ -d "/var/lib/casaos" ]; then log_message "Sauvegarde config CasaOS (full vers $CASAOS_CONFIG_DIR)..." mkdir -p "$CASAOS_CONFIG_DIR" cp -r /var/lib/casaos/* "$CASAOS_CONFIG_DIR/" 2>>"$LOG_FILE" fi # Création du second tar.gz CasaOS FULL (config + volumes casaos_data) if [ -d "/var/lib/casaos" ] || [ -d "$SOURCE_DIR/casaos_data" ]; then 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 de l’archive CASAOS FULL : $CASAOS_FULL_TAR" tar -cf "$CASAOS_FULL_TAR" -I pigz "${CASAOS_TAR_SOURCES[@]}" 2>>"$LOG_FILE" if [ $? -ne 0 ]; then log_message "ERREUR lors de la création du tar CasaOS." echo -e "${RED}Erreur lors de la création de l’archive CasaOS.${NC}" fi fi fi ############################################## # 5) Comptages finaux (sur le backup courant) ############################################## # Compose dans le backup (docker + CasaOS compose seulement) CASAOS_COMPOSE_BACKUP=$(find "$CONFIG_DIR/casaos" -maxdepth 3 -type f -name "docker-compose.y*ml" ! -path "$CASAOS_CONFIG_DIR/*" 2>/dev/null | wc -l) DOCKER_COMPOSE_BACKUP=$(find "$CONFIG_DIR/docker" -type f -name "docker-compose.y*ml" 2>/dev/null | wc -l) # Compose dans le dossier source (uniquement /home/docker//docker-compose.*) 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 "valides" 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) # Rotation des anciens backups (par dossiers) rotate_backups # Nombre total de sets de backup (dossiers datés) TOTAL_BACKUP_SETS=$(ls -1d "$BACKUP_DIR"/*/ 2>/dev/null | wc -l) # Oldest / newest backup set mapfile -t CURRENT_SETS < <(ls -1d "$BACKUP_DIR"/*/ 2>/dev/null | sort) OLDEST_DIR="${CURRENT_SETS[0]}" NEWEST_DIR="${CURRENT_SETS[${#CURRENT_SETS[@]}-1]}" DURATION="$(format_time $SECONDS)" # Tailles des backups courants TAR_SIZE=$(get_size "$BACKUP_FILE") CONFIG_SIZE=$(get_size "$CONFIG_DIR") DOCKER_CONFIG_SIZE=$(get_size "$CONFIG_DIR/docker") # CasaOS full config size (copie dans RUN_DIR/casaos/full_config) if [ -d "$CASAOS_CONFIG_DIR" ]; then CASAOS_CONFIG_SIZE=$(get_size "$CASAOS_CONFIG_DIR") else CASAOS_CONFIG_SIZE="" fi # CasaOS FULL TAR size if [ -f "$CASAOS_FULL_TAR" ]; then CASAOS_FULL_TAR_SIZE=$(get_size "$CASAOS_FULL_TAR") else CASAOS_FULL_TAR_SIZE="" fi # Portainer full config size if [ -n "$PORTAINER_CONFIG_DIR" ] && [ -d "$PORTAINER_CONFIG_DIR" ]; then PORTAINER_CONFIG_SIZE=$(get_size "$PORTAINER_CONFIG_DIR") else PORTAINER_CONFIG_SIZE="" fi ############################################## # 6) RÉSUMÉS ############################################## echo -e "${GREEN}===== BACKUP TERMINÉ =====${NC}" echo -e "${CYAN}Dossier backup courant : ${YELLOW}$RUN_DIR${NC}" ############################################## # LISTE DES DOSSIERS DOCKER PAR TAILLE (ASCII) ############################################## echo -e "\n${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 "┌──────────────────────────┬─────────┬────────────────────────────────────────┐" printf "│ %-24s │ %-7s │ %-38s │\n" "Container" "Taille" "Chemin" echo -e "├──────────────────────────┼─────────┼────────────────────────────────────────┤" 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 "└──────────────────────────┴─────────┴────────────────────────────────────────┘" fi ################################# # DOCKER ################################# echo -e "${CYAN}\n----- DOCKER -----${NC}" echo -e "${CYAN}Containers Docker détectés : ${GREEN}$CONTAINERS_DOCKER${NC}" echo -e "${CYAN}Compose source (Docker) : ${GREEN}$SOURCE_COMPOSE_COUNT${NC}" echo -e "${CYAN}Compose backup (Docker) : ${GREEN}$DOCKER_COMPOSE_BACKUP${NC}" echo -e "${CYAN}Dossier compose Docker : ${YELLOW}$CONFIG_DIR/docker${NC}" echo -e "${CYAN}Taille compose Docker : ${GREEN}$DOCKER_CONFIG_SIZE${NC}" ################################# # CASAOS (si présent) ################################# if [ -d "$CONFIG_DIR/casaos" ] || [ -f "$CASAOS_FULL_TAR" ]; then echo -e "${CYAN}\n----- CASAOS -----${NC}" echo -e "${CYAN}Compose CasaOS backup : ${GREEN}$CASAOS_COMPOSE_BACKUP${NC}" echo -e "${CYAN}Dossier CasaOS backup : ${YELLOW}$CONFIG_DIR/casaos${NC}" [ -n "$CASAOS_CONFIG_SIZE" ] && echo -e "${CYAN}Taille config CasaOS (full) : ${GREEN}$CASAOS_CONFIG_SIZE${NC}" if [ -f "$CASAOS_FULL_TAR" ]; then echo -e "${CYAN}Tar CasaOS FULL : ${YELLOW}$CASAOS_FULL_TAR${NC}" echo -e "${CYAN}Taille tar CasaOS FULL : ${GREEN}$CASAOS_FULL_TAR_SIZE${NC}" fi fi ################################# # PORTAINER (si présent) ################################# if [ -n "$PORTAINER_CONFIG_DIR" ] && [ -d "$PORTAINER_CONFIG_DIR" ]; then echo -e "${CYAN}\n----- PORTAINER -----${NC}" echo -e "${CYAN}Fichiers config Portainer : ${GREEN}$PORTAINER_FILES${NC}" echo -e "${CYAN}Dossier Portainer backup : ${YELLOW}$PORTAINER_CONFIG_DIR${NC}" [ -n "$PORTAINER_CONFIG_SIZE" ] && echo -e "${CYAN}Taille Portainer (full) : ${GREEN}$PORTAINER_CONFIG_SIZE${NC}" fi ################################# # GLOBAL ################################# echo -e "${CYAN}\n----- GLOBAL -----${NC}" echo -e "${CYAN}Nombre de sets de backup : ${GREEN}$TOTAL_BACKUP_SETS${NC}" echo -e "${CYAN}Oldest backup dir : ${YELLOW}${OLDEST_DIR:-N/A}${NC}" echo -e "${CYAN}Newest backup dir : ${YELLOW}${NEWEST_DIR:-N/A}${NC}" echo -e "${CYAN}Taille archive Docker tar.gz : ${GREEN}$TAR_SIZE${NC}" [ -f "$CASAOS_FULL_TAR" ] && echo -e "${CYAN}Taille archive CasaOS tar.gz : ${GREEN}$CASAOS_FULL_TAR_SIZE${NC}" echo -e "${CYAN}Taille dossier backup courant: ${GREEN}$CONFIG_SIZE${NC}" echo -e "${CYAN}Archive Docker courante : ${YELLOW}$BACKUP_FILE${NC}" [ -f "$CASAOS_FULL_TAR" ] && echo -e "${CYAN}Archive CasaOS courante : ${YELLOW}$CASAOS_FULL_TAR${NC}" echo -e "${CYAN}Durée totale : ${GREEN}$DURATION${NC}" ################################# # JSON (optionnel : ./script.sh --json) ################################# if [ "$1" = "--json" ]; then echo "" echo "{" echo " \"date_raw\": \"$DATE_RAW\"," echo " \"date_dir\": \"$DATE_DIR\"," echo " \"run_dir\": \"$RUN_DIR\"," echo " \"docker_backup_file\": \"$BACKUP_FILE\"," echo " \"docker_backup_size\": \"$TAR_SIZE\"," echo " \"casaos_full_backup_file\": \"${CASAOS_FULL_TAR:-}\"," echo " \"casaos_full_backup_size\": \"${CASAOS_FULL_TAR_SIZE:-}\"," echo " \"docker\": {" echo " \"containers\": $CONTAINERS_DOCKER," echo " \"compose_source\": $SOURCE_COMPOSE_COUNT," echo " \"compose_backup\": $DOCKER_COMPOSE_BACKUP" echo " }," echo " \"casaos\": {" echo " \"compose_backup\": $CASAOS_COMPOSE_BACKUP" echo " }," echo " \"portainer\": {" echo " \"files\": $PORTAINER_FILES" echo " }," echo " \"backup_sets\": $TOTAL_BACKUP_SETS," echo " \"oldest_backup_dir\": \"${OLDEST_DIR:-}\"," echo " \"newest_backup_dir\": \"${NEWEST_DIR:-}\"," echo " \"duration\": \"$DURATION\"" echo "}" fi