#!/bin/bash # ============================================= # backup-myapps.sh — Run on MAIN SERVER # Backs up: Frappe, Nextcloud, Mautic, n8n, Odoo # Storage tiers: # 1. Local → /root/backups/ (CRITICAL — always runs) # 2. VM → SSH tunnel → /backups/main-server/ # 3. Cloud → Cloudflare R2 (S3-compatible, optional) # # NOTE: R2 and VM failures do NOT stop the backup. # Local backup always completes first. # ============================================= set -uo 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" VM_USER="root" VM_HOST="178.18.243.51" VM_PORT="22" VM_KEY="/root/.ssh/id_rsa" VM_DEST="/backups/cloudproject/" # ── Load R2 credentials ─────────────────────────────────────────────────────── CREDENTIALS_FILE="/root/.r2-credentials" if [ -f "$CREDENTIALS_FILE" ]; then set +u source "$CREDENTIALS_FILE" set -u fi # Also try config.py as fallback if [ -z "${AWS_ACCESS_KEY_ID:-}" ]; then CONFIG_PY="/root/management-platform/config.py" if [ -f "$CONFIG_PY" ]; then _KEY=$(grep -oP '(?<=R2_ACCESS_KEY_ID",\s{5}")[^"]+' "$CONFIG_PY" 2>/dev/null || true) _SEC=$(grep -oP '(?<=R2_SECRET_ACCESS_KEY", ")[^"]+' "$CONFIG_PY" 2>/dev/null || true) _BKT=$(grep -oP '(?<=R2_BUCKET_NAME",\s{7}")[^"]+' "$CONFIG_PY" 2>/dev/null || true) [ -n "$_KEY" ] && export AWS_ACCESS_KEY_ID="$_KEY" [ -n "$_SEC" ] && export AWS_SECRET_ACCESS_KEY="$_SEC" [ -n "$_BKT" ] && export R2_BUCKET_NAME="$_BKT" fi fi R2_ACCOUNT_ID="${R2_ACCOUNT_ID:-35e00c230cc8066252a2d9890b69aea2}" R2_BUCKET="${R2_BUCKET_NAME:-navitrends-backups}" R2_ENDPOINT="https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" BACKUP_LOG_FILE="/root/backups/backup-status.log" MAX_BACKUPS=10 MAX_R2_BACKUPS=5 TIER1_STATUS="pending" TIER2_STATUS="pending" TIER3_STATUS="pending" log_status() { local status="$1" name="$2" 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 " Tiers: Local → VM → ☁ Cloudflare R2" echo "=========================================" mkdir -p "$BACKUP_DIR" "/root/backups" cd "$BACKUP_DIR" # -------------------------------------------------- # 1. Docker container list # -------------------------------------------------- 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 (continuing)" 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" 2>/dev/null | sed 's/\///') if [ -n "$name" ]; then docker inspect "$cid" > "container-configs/${name}.json" 2>/dev/null \ && COUNT=$((COUNT+1)) || true fi done < <(docker ps -a \ --filter "name=frappe" --filter "name=nextcloud" \ --filter "name=mautic" --filter "name=n8n" --filter "name=odoo" \ --format "{{.ID}}" 2>/dev/null || true) echo " ✅ Saved $COUNT container configs" # -------------------------------------------------- # 5. 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 (skipping)" 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 (skipping)" # -------------------------------------------------- # 6. 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 2>/dev/null || echo 'N/A') Storage Tiers: - Local: /root/backups/ - VM: ${VM_HOST}:${VM_PORT} → ${VM_DEST} - Cloud: Cloudflare R2 → s3://${R2_BUCKET}/backups/ Volumes included: $(ls volumes/*.tar.gz 2>/dev/null | xargs -I{} basename {} 2>/dev/null || echo "none") EOF 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 # -------------------------------------------------- echo "" echo "🗜️ [7/7] Compressing backup..." cd /root/backups tar -czf "${BACKUP_NAME}.tar.gz" "${BACKUP_NAME}/" 2>/dev/null if [ $? -ne 0 ]; then echo " ❌ CRITICAL: Compression failed!" log_status "FAILED" "$BACKUP_NAME" "compression_failed" exit 1 fi COMPRESSED_SIZE=$(du -h "${BACKUP_NAME}.tar.gz" | cut -f1) echo " ✅ Compressed size: $COMPRESSED_SIZE → $BACKUP_ARCHIVE" sha256sum "${BACKUP_NAME}.tar.gz" > "${BACKUP_NAME}.tar.gz.sha256" echo " ✅ Checksum written" rm -rf "$BACKUP_DIR" TIER1_STATUS="ok:${COMPRESSED_SIZE}" echo "" echo "✅ [TIER 1] Local backup complete: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" # -------------------------------------------------- # 8. Retention — keep only MAX_BACKUPS locally # -------------------------------------------------- echo "" echo "🧹 [Retention] Keeping latest ${MAX_BACKUPS} local backups..." ARCHIVE_LIST=$(ls -t /root/backups/myapps-backup-*.tar.gz 2>/dev/null || true) ARCHIVE_COUNT=$(echo "$ARCHIVE_LIST" | grep -c '.tar.gz' 2>/dev/null || echo "0") 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" "${old_file}.sha256" echo " 🗑️ Deleted: $(basename $old_file)" done <<< "$TO_DELETE" else echo " ✅ ${ARCHIVE_COUNT}/${MAX_BACKUPS} backups — nothing to prune" fi # -------------------------------------------------- # 9. VM transfer [TIER 2] — non-fatal # -------------------------------------------------- echo "" echo "📤 [TIER 2] Sending backup to VM (${VM_HOST}:${VM_PORT})..." scp -i "$VM_KEY" -P "$VM_PORT" \ -o StrictHostKeyChecking=no -o ConnectTimeout=30 \ "${BACKUP_NAME}.tar.gz" \ "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null VM_SCP_RC=$? if [ $VM_SCP_RC -eq 0 ]; then echo " ✅ Backup sent to VM successfully!" scp -i "$VM_KEY" -P "$VM_PORT" \ -o StrictHostKeyChecking=no -o ConnectTimeout=15 \ "${BACKUP_NAME}.tar.gz.sha256" \ "${VM_USER}@${VM_HOST}:${VM_DEST}" 2>/dev/null || true TIER2_STATUS="ok" else echo " ⚠️ VM transfer failed (rc=$VM_SCP_RC) — local backup is safe, continuing..." TIER2_STATUS="failed:rc=${VM_SCP_RC}" fi # -------------------------------------------------- # 10. Cloudflare R2 [TIER 3] — non-fatal # -------------------------------------------------- echo "" echo "☁️ [TIER 3] Uploading to Cloudflare R2..." if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then echo " ⚠️ R2 credentials not found — skipping cloud upload" echo " 💡 Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in /root/.r2-credentials" TIER3_STATUS="skipped:no_credentials" else echo " Credentials: found (key=${AWS_ACCESS_KEY_ID:0:8}...)" echo " Bucket: ${R2_BUCKET}" echo " Endpoint: ${R2_ENDPOINT}" PYTHON_BIN="/root/management-platform/venv/bin/python3" if [ ! -f "$PYTHON_BIN" ]; then PYTHON_BIN="python3" fi R2_SCRIPT="/tmp/r2_upload_$$.py" cat > "$R2_SCRIPT" << PYEOF import sys, os, boto3 from datetime import datetime, timezone account_id = "${R2_ACCOUNT_ID}" bucket = "${R2_BUCKET}" endpoint = "https://${R2_ACCOUNT_ID}.r2.cloudflarestorage.com" access_key = "${AWS_ACCESS_KEY_ID}" secret_key = "${AWS_SECRET_ACCESS_KEY}" archive = "/root/backups/${BACKUP_NAME}.tar.gz" sha_file = archive + '.sha256' key = "backups/${BACKUP_NAME}.tar.gz" max_r2 = ${MAX_R2_BACKUPS} try: client = boto3.client( 's3', endpoint_url=endpoint, aws_access_key_id=access_key, aws_secret_access_key=secret_key, region_name='auto' ) # Ensure bucket exists try: client.head_bucket(Bucket=bucket) except Exception: client.create_bucket(Bucket=bucket) print(f' Created bucket: {bucket}') # Upload archive size_mb = os.path.getsize(archive) / 1024 / 1024 timestamp = datetime.now(timezone.utc).isoformat() print(f' Uploading {size_mb:.1f} MB to r2://{bucket}/{key}') client.upload_file( archive, bucket, key, ExtraArgs={"Metadata": { "uploaded-by": "backup-script", "uploaded-at": timestamp, "original-file": archive.split("/")[-1], }} ) print(' ✅ R2 archive uploaded successfully') # Upload sha256 companion if os.path.exists(sha_file): client.upload_file(sha_file, bucket, key + '.sha256') print(' ✅ R2 SHA256 checksum uploaded') # ── R2 Retention: keep only max_r2 latest backups ────────── print(f' 🧹 Enforcing R2 retention (max {max_r2})...') resp = client.list_objects_v2(Bucket=bucket, Prefix='backups/') objects = sorted( [o for o in resp.get('Contents', []) if not o['Key'].endswith('.sha256')], key=lambda x: x['LastModified'], reverse=True # newest first ) to_delete = objects[max_r2:] if not to_delete: print(f' ✅ {len(objects)}/{max_r2} R2 backups — nothing to prune') for obj in to_delete: old_key = obj['Key'] client.delete_object(Bucket=bucket, Key=old_key) try: client.delete_object(Bucket=bucket, Key=old_key + '.sha256') except Exception: pass print(f' 🗑️ Deleted from R2: {old_key.replace("backups/", "")}') sys.exit(0) except Exception as e: print(f' ERROR: {e}') sys.exit(2) PYEOF "$PYTHON_BIN" "$R2_SCRIPT" R2_RC=$? rm -f "$R2_SCRIPT" if [ $R2_RC -eq 0 ]; then echo " ✅ R2 upload + retention complete" TIER3_STATUS="ok" else echo " ⚠️ R2 upload failed (rc=$R2_RC) — local backup is safe at: $BACKUP_ARCHIVE" TIER3_STATUS="failed:rc=${R2_RC}" fi fi # -------------------------------------------------- # 11. Final summary + log # -------------------------------------------------- echo "" echo "=========================================" echo "✅ BACKUP COMPLETE" echo "" echo " 📦 Name: $BACKUP_NAME" echo " 💾 Local: $BACKUP_ARCHIVE ($COMPRESSED_SIZE)" echo " 🖥️ VM: ${TIER2_STATUS}" echo " ☁️ R2: ${TIER3_STATUS}" echo "=========================================" log_status "SUCCESS" "$BACKUP_NAME" \ "size=${COMPRESSED_SIZE} | tier1=${TIER1_STATUS} | tier2=${TIER2_STATUS} | tier3=${TIER3_STATUS}" # ── Notification on failure (optional) ─────────────────────────────────────── notify_failure() { echo "Backup FAILED: $BACKUP_NAME" | \ mail -s "[Navitrends] BACKUP FAILED - $(date)" ameniboukottaya@gmail.com }