#!/bin/bash # ============================================================= # PLEX32 — Config Backup Script # Creates tar archives of Plex and Tautulli configs # Copies to NAS08 via NFS as single files # Keeps last 3 backups, deletes older ones # Sends HTML email report in KingDezigns style # Location: /usr/scripts/plex32/plex32_backup.sh # Schedule: Every 3 days at 2:00 AM (cron) # ============================================================= # --- Config --- HOSTNAME="plex32" PLEX_SRC="/opt/plex/config" TAUTULLI_SRC="/opt/tautulli/config" NFS_DEST="/mnt/plex/config" TAUTULLI_LOC="backups/tautulli" PLEX_LOC="backups/plex" TMP_DIR="/tmp/plex32-backup" LOG_DIR="/var/log/plex32-backup" LOG_FILE="$LOG_DIR/backup-$(date +%Y%m%d).log" RETENTION_COUNT=3 COMPOSE_DIR="/home/rufusking/docker/plex" DATESTAMP=$(date '+%Y-%m-%d') TIMESTAMP=$(date '+%B %d, %Y at %I:%M %p') PLEX_ARCHIVE="plex-config-${DATESTAMP}.tar.gz" TAUTULLI_ARCHIVE="tautulli-config-${DATESTAMP}.tar.gz" SMTP_SERVER="smtppro.zoho.com" SMTP_PORT=465 SMTP_USER="rufus.king@kingdezigns.com" SMTP_PASS="$(cat /etc/plex32-smtp-pass 2>/dev/null)" EMAIL_TO="rufus.king@kingdezigns.com" EMAIL_FROM="rufus.king@kingdezigns.com" # --- Setup --- mkdir -p "$LOG_DIR" "$TMP_DIR" START_TIME=$(date +%s) exec > >(tee -a "$LOG_FILE") 2>&1 echo "[$(date '+%Y-%m-%d %H:%M:%S')] PLEX32 backup started" # --- Check NFS mount --- if ! mountpoint -q "$NFS_DEST"; then echo "[ERROR] NFS mount not available: $NFS_DEST" exit 1 fi # --- Stop containers --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Stopping Plex and Tautulli containers..." cd "$COMPOSE_DIR" && docker compose down STOP_STATUS=$? if [ $STOP_STATUS -ne 0 ]; then echo "[ERROR] Failed to stop containers cleanly" fi # --- Tar Plex config --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Plex config archive..." PLEX_START=$(date +%s) tar -czf "$TMP_DIR/$PLEX_ARCHIVE" \ --exclude="config/Library/Application Support/Plex Media Server/Cache" \ --exclude="config/Library/Application Support/Plex Media Server/Logs" \ --exclude="config/Library/Application Support/Plex Media Server/Crash Reports" \ -C /opt/plex config 2>&1 PLEX_TAR_STATUS=$? PLEX_END=$(date +%s) PLEX_DURATION=$((PLEX_END - PLEX_START)) if [ $PLEX_TAR_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive created in ${PLEX_DURATION}s" else echo "[ERROR] Plex tar failed with status $PLEX_TAR_STATUS" fi # --- Tar Tautulli config --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Creating Tautulli config archive..." TAUTULLI_START=$(date +%s) tar -czf "$TMP_DIR/$TAUTULLI_ARCHIVE" \ -C /opt/tautulli config 2>&1 TAUTULLI_TAR_STATUS=$? TAUTULLI_END=$(date +%s) TAUTULLI_DURATION=$((TAUTULLI_END - TAUTULLI_START)) if [ $TAUTULLI_TAR_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive created in ${TAUTULLI_DURATION}s" else echo "[ERROR] Tautulli tar failed with status $TAUTULLI_TAR_STATUS" fi # --- Restart containers --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Restarting containers..." cd "$COMPOSE_DIR" && docker compose up -d START_STATUS=$? if [ $START_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully" else echo "[ERROR] Failed to restart containers on first attempt — retrying in 15 seconds..." sleep 15 cd "$COMPOSE_DIR" && docker compose up -d START_STATUS=$? if [ $START_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Containers restarted successfully on retry" else echo "[ERROR] Containers failed to restart after retry — manual intervention required" fi fi # --- Verify containers are actually running --- sleep 5 PLEX_RUNNING=$(docker ps --filter "name=plex" --filter "status=running" --format "{{.Names}}" | grep -c "^plex$") TAUTULLI_RUNNING=$(docker ps --filter "name=tautulli" --filter "status=running" --format "{{.Names}}" | grep -c "^tautulli$") if [ "$PLEX_RUNNING" -eq 1 ]; then PLEX_CONTAINER_STATUS="✅ RUNNING" PLEX_CONTAINER_COLOR="#166534" PLEX_CONTAINER_BG="#f0fdf4" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex container verified running" else PLEX_CONTAINER_STATUS="❌ NOT RUNNING — MANUAL INTERVENTION REQUIRED" PLEX_CONTAINER_COLOR="#b91c1c" PLEX_CONTAINER_BG="#fff1f2" echo "[ERROR] Plex container is NOT running after backup" fi if [ "$TAUTULLI_RUNNING" -eq 1 ]; then TAUTULLI_CONTAINER_STATUS="✅ RUNNING" TAUTULLI_CONTAINER_COLOR="#166534" TAUTULLI_CONTAINER_BG="#f0fdf4" echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli container verified running" else TAUTULLI_CONTAINER_STATUS="❌ NOT RUNNING — MANUAL INTERVENTION REQUIRED" TAUTULLI_CONTAINER_COLOR="#b91c1c" TAUTULLI_CONTAINER_BG="#fff1f2" echo "[ERROR] Tautulli container is NOT running after backup" fi # --- Copy Plex archive to NAS08 --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Plex archive to NAS08..." COPY_PLEX_START=$(date +%s) cp "$TMP_DIR/$PLEX_ARCHIVE" "$NFS_DEST/$PLEX_LOC/" PLEX_COPY_STATUS=$? COPY_PLEX_END=$(date +%s) PLEX_COPY_DURATION=$((COPY_PLEX_END - COPY_PLEX_START)) if [ $PLEX_COPY_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Plex archive copied in ${PLEX_COPY_DURATION}s" else echo "[ERROR] Plex archive copy failed" fi # --- Copy Tautulli archive to NAS08 --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Copying Tautulli archive to NAS08..." COPY_TAUTULLI_START=$(date +%s) cp "$TMP_DIR/$TAUTULLI_ARCHIVE" "$NFS_DEST/$TAUTULLI_LOC/" TAUTULLI_COPY_STATUS=$? COPY_TAUTULLI_END=$(date +%s) TAUTULLI_COPY_DURATION=$((COPY_TAUTULLI_END - COPY_TAUTULLI_START)) if [ $TAUTULLI_COPY_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Tautulli archive copied in ${TAUTULLI_COPY_DURATION}s" else echo "[ERROR] Tautulli archive copy failed" fi # --- Rotate old backups — keep last 3 --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Rotating old backups..." ls -t "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f ls -t "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | tail -n +$((RETENTION_COUNT + 1)) | xargs rm -f # --- Cleanup temp files --- rm -f "$TMP_DIR/$PLEX_ARCHIVE" "$TMP_DIR/$TAUTULLI_ARCHIVE" # --- Sizes and storage --- PLEX_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$PLEX_ARCHIVE" 2>/dev/null | awk '{print $5}') TAUTULLI_ARCHIVE_SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$TAUTULLI_ARCHIVE" 2>/dev/null | awk '{print $5}') PLEX_BACKUP_COUNT=$(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | wc -l) TAUTULLI_BACKUP_COUNT=$(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | wc -l) # Get accurate NAS08 disk stats via SSH (avoids NFS dataset quota confusion) NAS08_DISK_INFO=$(ssh -o StrictHostKeyChecking=no -o ConnectTimeout=5 rufusking@192.168.150.35 \ "df -h /export/kingdezigns-all | awk 'NR==2{print \$3\" used / \"\$2\" total / \"\$4\" free\"}'" 2>/dev/null) if [ -z "$NAS08_DISK_INFO" ]; then # Fallback to NFS mount stats if SSH fails NAS08_DISK_INFO=$(df -h "$NFS_DEST" 2>/dev/null | awk 'NR==2{print $3" used / "$2" total / "$4" free"}') fi # List of current backups on NAS08 PLEX_BACKUP_LIST=$(ls -lh "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | \ awk '{print $NF" ("$5")"}' | xargs -I{} basename {} 2>/dev/null | \ while read -r line; do echo "$line"; done) TAUTULLI_BACKUP_LIST=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | \ awk '{print $NF" ("$5")"}' | xargs -I{} basename {} 2>/dev/null | \ while read -r line; do echo "$line"; done) # Build backup list HTML rows PLEX_LIST_ROWS="" while IFS= read -r f; do SIZE=$(ls -lh "$NFS_DEST/$PLEX_LOC/$f" 2>/dev/null | awk '{print $5}') PLEX_LIST_ROWS="${PLEX_LIST_ROWS}$f$SIZE" done < <(ls "$NFS_DEST/$PLEX_LOC"/plex-config-*.tar.gz 2>/dev/null | xargs -I{} basename {}) TAUTULLI_LIST_ROWS="" while IFS= read -r f; do SIZE=$(ls -lh "$NFS_DEST/$TAUTULLI_LOC/$f" 2>/dev/null | awk '{print $5}') TAUTULLI_LIST_ROWS="${TAUTULLI_LIST_ROWS}$f$SIZE" done < <(ls "$NFS_DEST/$TAUTULLI_LOC"/tautulli-config-*.tar.gz 2>/dev/null | xargs -I{} basename {}) # --- Total duration --- END_TIME=$(date +%s) TOTAL_DURATION=$((END_TIME - START_TIME)) TOTAL_MIN=$((TOTAL_DURATION / 60)) TOTAL_SEC=$((TOTAL_DURATION % 60)) # --- Overall status --- PLEX_STATUS=$((PLEX_TAR_STATUS + PLEX_COPY_STATUS)) TAUTULLI_STATUS=$((TAUTULLI_TAR_STATUS + TAUTULLI_COPY_STATUS)) CONTAINER_STATUS=$((PLEX_RUNNING == 1 && TAUTULLI_RUNNING == 1 ? 0 : 1)) if [ $PLEX_STATUS -eq 0 ] && [ $TAUTULLI_STATUS -eq 0 ] && [ $CONTAINER_STATUS -eq 0 ]; then OVERALL_STATUS="SUCCESS" STATUS_COLOR="#1a7f4b" BANNER_ICON="✅" BANNER_MSG="Backup completed successfully — Plex and Tautulli configs archived to NAS08" BADGE_COLOR="#1a7f4b" elif [ $PLEX_STATUS -ne 0 ] && [ $TAUTULLI_STATUS -ne 0 ]; then OVERALL_STATUS="FAILED" STATUS_COLOR="#b91c1c" BANNER_ICON="🔴" BANNER_MSG="Backup failed — both archive operations encountered errors" BADGE_COLOR="#b91c1c" else OVERALL_STATUS="PARTIAL" STATUS_COLOR="#b45309" BANNER_ICON="⚠️" BANNER_MSG="Backup partially completed — review individual results below" BADGE_COLOR="#b45309" fi # --- Status labels --- if [ $PLEX_STATUS -eq 0 ]; then PLEX_LABEL="✅ SUCCESS" PLEX_ROW_BG="#f0fdf4" PLEX_TEXT_COLOR="#166534" else PLEX_LABEL="❌ FAILED" PLEX_ROW_BG="#fff1f2" PLEX_TEXT_COLOR="#b91c1c" fi if [ $TAUTULLI_STATUS -eq 0 ]; then TAUTULLI_LABEL="✅ SUCCESS" TAUTULLI_ROW_BG="#f0fdf4" TAUTULLI_TEXT_COLOR="#166534" else TAUTULLI_LABEL="❌ FAILED" TAUTULLI_ROW_BG="#fff1f2" TAUTULLI_TEXT_COLOR="#b91c1c" fi # --- Send email --- echo "[$(date '+%Y-%m-%d %H:%M:%S')] Sending email report..." python3 << PYEOF import smtplib, ssl from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText smtp_server = "$SMTP_SERVER" smtp_port = $SMTP_PORT smtp_user = "$SMTP_USER" smtp_pass = "$SMTP_PASS" email_to = "$EMAIL_TO" email_from = "$EMAIL_FROM" subject = "PLEX32 Backup — $OVERALL_STATUS — $DATESTAMP" html = """
Config Backup
🎬 plex32
$TIMESTAMP  |  VLAN 50 — Lab/Servers
$OVERALL_STATUS
$BANNER_ICON  $BANNER_MSG
Duration
${TOTAL_MIN}m ${TOTAL_SEC}s
Plex Archive
$PLEX_ARCHIVE_SIZE
Tautulli Archive
$TAUTULLI_ARCHIVE_SIZE
Plex Copies
$PLEX_BACKUP_COUNT / $RETENTION_COUNT
📋 Backup Results
Component Archive Duration Status
🎬 Plex Config $PLEX_ARCHIVE ${PLEX_DURATION}s tar + ${PLEX_COPY_DURATION}s copy $PLEX_LABEL
📊 Tautulli Config $TAUTULLI_ARCHIVE ${TAUTULLI_DURATION}s tar + ${TAUTULLI_COPY_DURATION}s copy $TAUTULLI_LABEL
🐳 Container Status After Backup
🎬 Plex $PLEX_CONTAINER_STATUS
📊 Tautulli $TAUTULLI_CONTAINER_STATUS
🗄️ Archives on NAS08
$PLEX_LIST_ROWS $TAUTULLI_LIST_ROWS
Plex Backups (max $RETENTION_COUNT kept)
Tautulli Backups (max $RETENTION_COUNT kept)
Backup Destination NAS08 (192.168.150.35) — $NAS08_DISK_INFO
💡 How to Restore Plex Config
Machine: plex32 (192.168.150.45)
Step 1 — Stop Plex and Tautulli
cd ~/docker/plex && docker compose down
Step 2 — Copy archive from NAS08 to PLEX32
cp /mnt/plex/config/$PLEX_LOC/$PLEX_ARCHIVE /tmp/
Step 3 — Clear current Plex config
sudo rm -rf /opt/plex/config/Library
Step 4 — Extract the backup
sudo tar -xzf /tmp/$PLEX_ARCHIVE -C /opt/plex
sudo chown -R rufusking:ncshare /opt/plex/config
Step 5 — Restart containers
cd ~/docker/plex && docker compose up -d
Step 6 — Verify both containers are running
docker ps
Both plex and tautulli should show status Up. Then open http://192.168.150.45:32400/web and confirm your library and watch history are intact.
Step 7 — Cleanup temp file
rm /tmp/$PLEX_ARCHIVE
Config Backup  |  plex32  |  KingDezigns Infrastructure
""" msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = email_from msg["To"] = email_to msg.attach(MIMEText(html, "html")) ctx = ssl.create_default_context() with smtplib.SMTP_SSL(smtp_server, smtp_port, context=ctx, timeout=15) as s: s.login(smtp_user, smtp_pass) s.sendmail(email_from, [email_to], msg.as_string()) print("Email sent successfully") PYEOF EMAIL_STATUS=$? if [ $EMAIL_STATUS -eq 0 ]; then echo "[$(date '+%Y-%m-%d %H:%M:%S')] Email sent successfully" else echo "[ERROR] Email send failed" fi # --- Cleanup old logs --- find "$LOG_DIR" -name "backup-*.log" -mtime +90 -delete 2>/dev/null echo "[$(date '+%Y-%m-%d %H:%M:%S')] Backup complete — Status: $OVERALL_STATUS | Duration: ${TOTAL_MIN}m ${TOTAL_SEC}s"