#!/bin/bash # ============================================= # backup-myapps.sh — Run on MAIN SERVER # Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo # Usage: ./backup-myapps.sh # ============================================= set -euo pipefail BACKUP_DATE=$(date +%Y%m%d_%H%M%S) BACKUP_NAME="myapps-backup-${BACKUP_DATE}" BACKUP_DIR="/root/backups/${BACKUP_NAME}" BACKUP_ARCHIVE="/root/backups/${BACKUP_NAME}.tar.gz" # SSH config to reach the VM (backup destination) VM_USER="root" VM_HOST="localhost" VM_PORT="2223" VM_KEY="/root/.ssh/contabo-key" VM_DEST="/backups/main-server/" # Log file for backup status (used by boot-check script) BACKUP_LOG_FILE="/root/backups/backup-status.log" MAX_BACKUPS=10 # ── Write status to log ────────────────────────────────────────────────────── log_status() { local status="$1" # SUCCESS or FAILED local name="$2" local msg="${3:-}" echo "$(date '+%Y-%m-%d %H:%M:%S') | ${status} | ${name} | ${msg}" >> "$BACKUP_LOG_FILE" } echo "=========================================" echo "📦 Starting Backup: $BACKUP_NAME" echo " Apps: Frappe, Nextcloud, Mautic, n8n, Odoo" echo "=========================================" mkdir -p "$BACKUP_DIR" mkdir -p "/root/backups" cd "$BACKUP_DIR" # -------------------------------------------------- # 1. Docker container list (filtered to your apps) # -------------------------------------------------- echo "" echo "📋 [1/7] Saving Docker container list..." docker ps -a --filter "name=frappe" \ --filter "name=nextcloud" \ --filter "name=mautic" \ --filter "name=n8n" \ --filter "name=odoo" \ --format "table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}" \ > docker-containers.txt 2>/dev/null || true echo " ✅ Done" # -------------------------------------------------- # 2. docker-compose files # -------------------------------------------------- echo "" echo "📄 [2/7] Saving docker-compose files..." mkdir -p compose-files for app in frappe-setup odoo-clean nextcloud-setup mautic-setup n8n-setup; do if [ -d ~/$app ]; then cp -r ~/$app compose-files/ && echo " ✅ $app" || echo " ⚠️ $app copy failed" else echo " ⏭️ $app not found — skipping" fi done # -------------------------------------------------- # 3. Docker volumes # -------------------------------------------------- echo "" echo "💾 [3/7] Backing up Docker volumes..." mkdir -p volumes VOLUMES=( "frappe-setup_frappe-sites" "frappe-setup_mariadb-data" "nextcloud-setup_nextcloud-data" "nextcloud-setup_nextcloud-db-data" "mautic-setup_mautic-data" "mautic-setup_mautic-db-data" "n8n-setup_n8n-data" "n8n-setup_n8n-db-data" "odoo-clean_db-data" "odoo-clean_odoo-etc" ) for volume in "${VOLUMES[@]}"; do if ! docker volume inspect "$volume" &>/dev/null; then echo " ⏭️ $volume — not found, skipping" continue fi echo -n " 📁 $volume ... " docker run --rm \ -v "${volume}:/source:ro" \ -v "$(pwd)/volumes:/backup" \ alpine \ tar czf "/backup/${volume}.tar.gz" -C /source . \ && echo "✅" || echo "⚠️ FAILED" done # -------------------------------------------------- # 4. Container inspect configs # -------------------------------------------------- echo "" echo "🔧 [4/7] Saving container inspect configs..." mkdir -p container-configs COUNT=0 while IFS= read -r cid; do name=$(docker inspect --format='{{.Name}}' "$cid" | sed 's/\///') docker inspect "$cid" > "container-configs/${name}.json" 2>/dev/null && COUNT=$((COUNT+1)) done < <(docker ps -a --filter "name=frappe" \ --filter "name=nextcloud" \ --filter "name=mautic" \ --filter "name=n8n" \ --filter "name=odoo" \ --format "{{.ID}}") echo " ✅ Saved $COUNT container configs" # -------------------------------------------------- # 5. Extract important app config files # -------------------------------------------------- echo "" echo "⚙️ [5/7] Extracting app config files..." mkdir -p configs docker run --rm \ -v nextcloud-setup_nextcloud-data:/source:ro \ alpine cat /source/config/config.php > configs/nextcloud-config.php 2>/dev/null \ && echo " ✅ Nextcloud config.php" \ || echo " ⏭️ Nextcloud config not found" docker exec frappe-erpnext \ cat /home/frappe/frappe-bench/sites/erpnext.navitrends.ovh/site_config.json \ > configs/frappe-site_config.json 2>/dev/null \ && echo " ✅ Frappe site_config.json" \ || echo " ⏭️ Frappe config not found" # -------------------------------------------------- # 6. Backup metadata + checksum # -------------------------------------------------- echo "" echo "📝 [6/7] Writing backup metadata..." VOLUME_COUNT=$(ls volumes/*.tar.gz 2>/dev/null | wc -l) cat > backup-info.txt << EOF Backup Name: $BACKUP_NAME Backup Date: $(date) Hostname: $(hostname) Apps: Frappe, Nextcloud, Mautic, n8n, Odoo Volumes: $VOLUME_COUNT volume(s) backed up Docker info: $(docker --version) Volumes included: $(ls volumes/*.tar.gz 2>/dev/null | xargs -I{} basename {} || echo "none") EOF # Write individual volume checksums for integrity verification later echo "" >> backup-info.txt echo "Volume SHA256 checksums:" >> backup-info.txt for f in volumes/*.tar.gz; do [ -f "$f" ] && sha256sum "$f" | awk '{print $1 " " $2}' >> backup-info.txt || true done echo " ✅ Done" # -------------------------------------------------- # 7. Compress the backup # -------------------------------------------------- echo "" echo "🗜️ [7/7] Compressing backup..." cd /root/backups tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/" COMPRESSED_SIZE=$(du -h "${BACKUP_NAME}.tar.gz" | cut -f1) echo " ✅ Compressed size: $COMPRESSED_SIZE → $BACKUP_ARCHIVE" # Write a top-level SHA256 for the final archive (used by health-check) sha256sum "${BACKUP_NAME}.tar.gz" > "${BACKUP_NAME}.tar.gz.sha256" echo " ✅ Checksum written: ${BACKUP_NAME}.tar.gz.sha256" # Remove staging directory now that archive is created rm -rf "$BACKUP_DIR" # -------------------------------------------------- # 8. Retention — keep only the latest MAX_BACKUPS # -------------------------------------------------- echo "" echo "🧹 [Retention] Keeping latest ${MAX_BACKUPS} backups..." ARCHIVE_LIST=$(ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null || true) ARCHIVE_COUNT=$(echo "$ARCHIVE_LIST" | grep -c '.tar.gz' || true) if [ "$ARCHIVE_COUNT" -gt "$MAX_BACKUPS" ]; then TO_DELETE=$(echo "$ARCHIVE_LIST" | tail -n +$((MAX_BACKUPS + 1))) while IFS= read -r old_file; do [ -z "$old_file" ] && continue rm -f "$old_file" rm -f "${old_file}.sha256" echo " 🗑️ Deleted old backup: $(basename $old_file)" done <<< "$TO_DELETE" else echo " ✅ ${ARCHIVE_COUNT}/${MAX_BACKUPS} backups — nothing to prune" fi # -------------------------------------------------- # 9. Send to VM over SSH # -------------------------------------------------- echo "" echo "📤 Sending backup to VM (${VM_HOST}:${VM_PORT})..." scp -i "$VM_KEY" \ -P "$VM_PORT" \ -o StrictHostKeyChecking=no \ -o ConnectTimeout=15 \ "${BACKUP_NAME}.tar.gz" \ "${VM_USER}@${VM_HOST}:${VM_DEST}" if [ $? -eq 0 ]; then echo " ✅ Backup sent to VM successfully!" # Also send the checksum file scp -i "$VM_KEY" -P "$VM_PORT" -o StrictHostKeyChecking=no \ "${BACKUP_NAME}.tar.gz.sha256" "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null || true else echo " ❌ Transfer failed. The compressed backup is still at:" echo " $BACKUP_ARCHIVE" echo " 💡 Retry manually:" echo " scp -i $VM_KEY -P $VM_PORT $BACKUP_ARCHIVE ${VM_USER}@${VM_HOST}:${VM_DEST}" fi # -------------------------------------------------- # 10. Write final status to log # -------------------------------------------------- log_status "SUCCESS" "$BACKUP_NAME" "size=${COMPRESSED_SIZE}" echo "" echo "=========================================" echo "✅ BACKUP COMPLETE" echo " Name: $BACKUP_NAME" echo " Local: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" echo " Remote: ${VM_HOST}:${VM_DEST}${BACKUP_NAME}.tar.gz" echo "=========================================" # ── Chiffrement AES-256 ────────────────────────────────────────────────────── encrypt_backup() { echo "🔐 Chiffrement AES-256..." openssl enc -aes-256-cbc -pbkdf2 -pass pass:Navitrends2024! \ -in "$BACKUP_ARCHIVE" \ -out "${BACKUP_ARCHIVE}.enc" rm -f "$BACKUP_ARCHIVE" echo "✅ Archive chiffrée : ${BACKUP_ARCHIVE}.enc" } # ── Notification email échec ───────────────────────────────────────────────── notify_failure() { echo "📧 Envoi notification échec..." echo "Backup FAILED: $BACKUP_NAME" | \ mail -s "[Navitrends] BACKUP FAILED - $(date)" arijabidi577@gmail.com }