#!/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' \
-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 "| 📦 Container | 📏 Taille | 📁 Chemin |
" >> "$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 "| $name | $size | $esc_path |
" >> "$HTML_REPORT"
done
echo "
" >> "$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" <
🕒 $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" <
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