From c9d3276d362aa419529cc8531c2f3e75a890318d Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 1 Jan 2026 22:24:44 +0000 Subject: [PATCH] Add disaster recovery mode for one-click restore from backup New features: - --restore flag for disaster recovery mode - Interactive mode selector at script start (N=Normal, R=Recovery) - Full disaster recovery flow: 1. Show available drives, auto-mount if device path given 2. Auto-detect Kopia repository 3. Try to find password in backed-up .env, or prompt 4. Install Docker if needed 5. List available snapshots, let user choose or use 'latest' 6. Restore snapshot to temp location 7. Detect all docker-compose.yml files = services to restore 8. Copy services to ~/docker/ 9. Optionally start all containers 10. Cleanup temp files Removed old buried import section that only showed manual instructions. Documentation: - Added Disaster Recovery section to README - Added --restore to command-line options - Documented what gets restored and requirements - Added v2.7 changelog entry --- README.md | 66 +++++++- ubuntu-post-install.sh | 343 +++++++++++++++++++++++++++++++++++------ 2 files changed, 365 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 002c550..d3bbf7f 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ Automated setup script for Ubuntu 24.04 Desktop that installs essential tools, c - **Modular** - Every component is optional with y/n prompts - **Dry-run mode** - Preview what would be installed without making changes - **Unattended mode** - Run with defaults for automated/scripted installs +- **Disaster recovery** - One-click restore from Kopia backup after system failure - **Logging** - All output logged to `/var/log/post-install.log` - **Local backup** - rsync to 1-4 drives with customizable names - **Cloud backup** - Encrypted backup to Google Drive, OneDrive, or 40+ providers @@ -191,6 +192,9 @@ sudo ./post-install.sh --dry-run # Automated install with defaults (no prompts) sudo ./post-install.sh --unattended +# Disaster recovery - restore from Kopia backup +sudo ./post-install.sh --restore + # Show help sudo ./post-install.sh --help ``` @@ -201,7 +205,59 @@ sudo ./post-install.sh --help - Install Docker - Install fail2ban (since password auth is enabled) - Enable UFW firewall -- Skip Samba, NetBird, RustDesk, Backup system + +## Disaster Recovery + +If your OS drive fails, you can restore everything from a Kopia backup. + +### One-Click Recovery + +```bash +# 1. Install fresh Ubuntu 24.04 +# 2. Connect your backup drive +# 3. Download and run the script + +wget https://raw.githubusercontent.com/outis1one/post-ubuntu-install/main/ubuntu-post-install.sh +chmod +x ubuntu-post-install.sh +sudo ./ubuntu-post-install.sh --restore +``` + +### What the Recovery Does + +1. **Finds your backup drive** - Shows available drives, auto-detects Kopia repo +2. **Installs Docker** - If not already installed +3. **Lists snapshots** - Shows all available backups, lets you choose +4. **Restores services** - Detects which apps were backed up, restores them all +5. **Starts containers** - Optionally starts all services immediately + +### What Gets Restored + +Everything in `~/docker/` that Kopia backed up: +- **App configs** - All settings, users, preferences +- **Databases** - Immich, Mealie, Traccar, etc. +- **Media metadata** - Emby/Jellyfin watch history, thumbnails +- **Minecraft worlds** - Saves, mods, permissions +- **Frigate** - Camera configs, detection settings +- **All other container data** + +### Recovery Requirements + +- Fresh Ubuntu 24.04 installation +- Backup drive with Kopia repository +- Kopia password (stored in `~/docker/kopia/.env` on backup, or remembered) + +### Interactive Mode + +When you run the script without `--restore`, you'll be asked: +``` +INSTALLATION MODE +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + [N] Normal install - Fresh install or modify existing + [R] Disaster recovery - Restore from Kopia backup + +Select mode (N/R) [N]: +``` ### 5. Follow Interactive Prompts @@ -980,6 +1036,14 @@ This script is provided as-is for Ubuntu 24.04 Desktop installations. ## Changelog +- **v2.7**: Disaster recovery mode + - **One-click restore** from Kopia backup after system failure + - New `--restore` flag for disaster recovery mode + - Interactive mode selector at script start: Normal install or Disaster recovery + - Auto-detects Kopia repository on backup drives + - Auto-detects and restores all backed-up Docker services + - Installs Docker if needed, starts all containers after restore + - Recovery flow: Mount drive → Find repo → Enter password → Select snapshot → Restore → Start - **v2.6**: FindMyDevice, Frigate-Notify, and resilient install - Added FindMyDevice server (self-hosted Android tracking) - Added Frigate-Notify (push alerts for Frigate AI detections) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 45f98e0..453aaa1 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -22,16 +22,20 @@ show_help() { echo " --unattended Run with default options (no prompts)" echo " Defaults: skip key generation, no SSH imports," echo " install Docker, skip VPNs/Remote Desktop/Backup" + echo " --restore Disaster recovery mode - restore from backup" echo " --help Show this help message" echo "" echo "Examples:" echo " sudo ./post-install.sh # Interactive mode" echo " sudo ./post-install.sh --dry-run # Preview installations" echo " sudo ./post-install.sh --unattended # Automated install" + echo " sudo ./post-install.sh --restore # Disaster recovery" echo "" exit 0 } +RESTORE_MODE=false + for arg in "$@"; do case $arg in --dry-run) @@ -40,6 +44,9 @@ for arg in "$@"; do --unattended) UNATTENDED=true ;; + --restore) + RESTORE_MODE=true + ;; --help|-h) show_help ;; @@ -86,11 +93,304 @@ fi # Get the actual user (not root) ACTUAL_USER="${SUDO_USER:-$USER}" ACTUAL_HOME=$(getent passwd "$ACTUAL_USER" | cut -d: -f6) +DOCKER_DIR="$ACTUAL_HOME/docker" echo "Note: Script will continue even if individual packages fail to install" echo "Log file: $LOG_FILE" echo "" +# ============================================================================ +# INSTALLATION MODE SELECTOR +# ============================================================================ + +INSTALL_MODE="normal" + +# Disaster recovery function +run_disaster_recovery() { + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "DISASTER RECOVERY MODE" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "This will restore your Docker apps and data from a Kopia backup." + echo "" + echo "Requirements:" + echo " • Backup drive connected (with kopia-repo folder)" + echo " • Kopia password (saved in .env or you remember it)" + echo "" + + # Step 1: Find/mount backup drive + echo "Step 1: Locate backup drive" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Available block devices:" + lsblk -o NAME,SIZE,FSTYPE,MOUNTPOINT,LABEL 2>/dev/null || lsblk + echo "" + + # Check if drives are already mounted + if [ -d "$ACTUAL_HOME/drives" ]; then + echo "Existing mount points in ~/drives/:" + ls -la "$ACTUAL_HOME/drives/" 2>/dev/null || echo " (none)" + echo "" + fi + + read -p "Enter path to backup drive or Kopia repo (e.g., /dev/sdb1 or ~/drives/backup1): " BACKUP_PATH + + # If it's a device, mount it + if [[ "$BACKUP_PATH" == /dev/* ]]; then + MOUNT_POINT="$ACTUAL_HOME/drives/restore-backup" + mkdir -p "$MOUNT_POINT" 2>/dev/null + echo "Mounting $BACKUP_PATH to $MOUNT_POINT..." + mount "$BACKUP_PATH" "$MOUNT_POINT" || { echo "Failed to mount. Check device path."; return 1; } + BACKUP_PATH="$MOUNT_POINT" + fi + + # Expand ~ to home directory + BACKUP_PATH="${BACKUP_PATH/#\~/$ACTUAL_HOME}" + + # Look for kopia-repo + KOPIA_REPO="" + if [ -d "$BACKUP_PATH/kopia-repo" ]; then + KOPIA_REPO="$BACKUP_PATH/kopia-repo" + elif [ -d "$BACKUP_PATH/kopia.repository" ] || [ -f "$BACKUP_PATH/kopia.repository.f" ]; then + KOPIA_REPO="$BACKUP_PATH" + else + echo "" + echo "Looking for Kopia repository..." + FOUND_REPO=$(find "$BACKUP_PATH" -maxdepth 3 -name "kopia.repository*" -type f 2>/dev/null | head -1) + if [ -n "$FOUND_REPO" ]; then + KOPIA_REPO=$(dirname "$FOUND_REPO") + fi + fi + + if [ -z "$KOPIA_REPO" ] || [ ! -d "$KOPIA_REPO" ]; then + echo "❌ Could not find Kopia repository at $BACKUP_PATH" + echo " Look for a folder containing 'kopia.repository' files" + return 1 + fi + + echo "✓ Found Kopia repository at: $KOPIA_REPO" + echo "" + + # Step 2: Get Kopia password + echo "Step 2: Kopia password" + echo "━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + # Try to find password in backed-up .env + if [ -f "$BACKUP_PATH/kopia/.env" ]; then + KOPIA_PASS=$(grep "KOPIA_PASSWORD" "$BACKUP_PATH/kopia/.env" 2>/dev/null | cut -d= -f2) + if [ -n "$KOPIA_PASS" ]; then + echo "Found password in backup. Use this? (y/n)" + read -p "> " USE_FOUND_PASS + [ "$USE_FOUND_PASS" != "y" ] && KOPIA_PASS="" + fi + fi + + if [ -z "$KOPIA_PASS" ]; then + read -s -p "Enter Kopia repository password: " KOPIA_PASS + echo "" + fi + + # Step 3: Install Docker if needed + echo "" + echo "Step 3: Ensure Docker is installed" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + if ! command -v docker &> /dev/null; then + echo "Installing Docker..." + apt-get update + apt-get install -y ca-certificates curl gnupg + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg + chmod a+r /etc/apt/keyrings/docker.gpg + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null + apt-get update + apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin + usermod -aG docker "$ACTUAL_USER" + echo "✓ Docker installed" + else + echo "✓ Docker already installed" + fi + echo "" + + # Step 4: List snapshots and let user choose + echo "Step 4: Select snapshot to restore" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Connecting to repository and listing snapshots..." + echo "" + + # Use docker to run kopia + SNAPSHOT_LIST=$(docker run --rm \ + -v "$KOPIA_REPO:/repository" \ + -e KOPIA_PASSWORD="$KOPIA_PASS" \ + kopia/kopia:latest \ + snapshot list --all 2>&1) + + if echo "$SNAPSHOT_LIST" | grep -q "invalid password"; then + echo "❌ Invalid password" + return 1 + fi + + echo "$SNAPSHOT_LIST" + echo "" + read -p "Enter snapshot ID to restore (or 'latest' for most recent): " SNAPSHOT_ID + + if [ "$SNAPSHOT_ID" = "latest" ]; then + SNAPSHOT_ID=$(echo "$SNAPSHOT_LIST" | grep -oE "^[a-f0-9]+" | tail -1) + echo "Using latest snapshot: $SNAPSHOT_ID" + fi + + # Step 5: Restore to temp location + echo "" + echo "Step 5: Restoring from backup" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + RESTORE_TEMP="$ACTUAL_HOME/docker-restore-temp" + mkdir -p "$RESTORE_TEMP" + + echo "Restoring to $RESTORE_TEMP..." + docker run --rm \ + -v "$KOPIA_REPO:/repository" \ + -v "$RESTORE_TEMP:/restore" \ + -e KOPIA_PASSWORD="$KOPIA_PASS" \ + kopia/kopia:latest \ + restore "$SNAPSHOT_ID" /restore + + if [ $? -ne 0 ]; then + echo "❌ Restore failed" + return 1 + fi + echo "✓ Snapshot restored to temp location" + echo "" + + # Step 6: Detect and install services + echo "Step 6: Installing services from backup" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + mkdir -p "$DOCKER_DIR" + + # Find all docker-compose.yml files in restored backup + SERVICES_FOUND=() + for compose_file in $(find "$RESTORE_TEMP" -name "docker-compose.yml" -o -name "compose.yml" 2>/dev/null); do + SERVICE_DIR=$(dirname "$compose_file") + SERVICE_NAME=$(basename "$SERVICE_DIR") + SERVICES_FOUND+=("$SERVICE_NAME") + done + + if [ ${#SERVICES_FOUND[@]} -eq 0 ]; then + echo "No Docker services found in backup" + # Check if backup is the docker folder itself + if [ -f "$RESTORE_TEMP/docker-compose.yml" ]; then + echo "Found docker-compose.yml at root - checking structure..." + fi + else + echo "Found ${#SERVICES_FOUND[@]} services in backup:" + for svc in "${SERVICES_FOUND[@]}"; do + echo " • $svc" + done + echo "" + + read -p "Restore all services? (y/n): " RESTORE_ALL + + if [ "$RESTORE_ALL" = "y" ] || [ "$RESTORE_ALL" = "Y" ]; then + for compose_file in $(find "$RESTORE_TEMP" -name "docker-compose.yml" -o -name "compose.yml" 2>/dev/null); do + SERVICE_DIR=$(dirname "$compose_file") + SERVICE_NAME=$(basename "$SERVICE_DIR") + TARGET_DIR="$DOCKER_DIR/$SERVICE_NAME" + + echo "Restoring $SERVICE_NAME..." + + # Copy entire service directory + cp -r "$SERVICE_DIR" "$TARGET_DIR" 2>/dev/null || true + + # Fix ownership + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TARGET_DIR" 2>/dev/null || true + + echo " ✓ $SERVICE_NAME restored to $TARGET_DIR" + done + fi + fi + + # Step 7: Start services + echo "" + echo "Step 7: Starting services" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + read -p "Start all restored services now? (y/n): " START_ALL + + if [ "$START_ALL" = "y" ] || [ "$START_ALL" = "Y" ]; then + for dir in "$DOCKER_DIR"/*/; do + if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/compose.yml" ]; then + SERVICE_NAME=$(basename "$dir") + echo "Starting $SERVICE_NAME..." + (cd "$dir" && docker compose up -d 2>/dev/null) || echo " ⚠ Failed to start $SERVICE_NAME" + fi + done + fi + + # Cleanup + echo "" + read -p "Remove temporary restore files? (y/n): " CLEANUP + if [ "$CLEANUP" = "y" ]; then + rm -rf "$RESTORE_TEMP" + echo "✓ Cleaned up temp files" + fi + + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "DISASTER RECOVERY COMPLETE" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Services restored to: $DOCKER_DIR" + echo "" + echo "To check running containers:" + echo " docker ps" + echo "" + echo "To view logs:" + echo " docker compose logs -f" + echo "" + echo "⚠️ Some services may need manual configuration:" + echo " • Frigate: Edit config/config.yml with camera URLs" + echo " • Caddy: Update Caddyfile with your domain" + echo " • ddclient: Edit config/ddclient.conf with DNS credentials" + echo "" + + return 0 +} + +# Check for restore mode flag +if [ "$RESTORE_MODE" = true ]; then + run_disaster_recovery + exit $? +fi + +# Interactive mode selector (if not unattended) +if [ "$UNATTENDED" != true ]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "INSTALLATION MODE" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo " [N] Normal install - Fresh install or modify existing" + echo " [R] Disaster recovery - Restore from Kopia backup" + echo "" + read -p "Select mode (N/R) [N]: " MODE_SELECT + + case "${MODE_SELECT^^}" in + R) + run_disaster_recovery + exit $? + ;; + *) + INSTALL_MODE="normal" + ;; + esac + echo "" +fi + # ============================================================================ # SOFTWARE DETECTION FUNCTIONS # ============================================================================ @@ -2856,49 +3156,6 @@ RESTORE_SCRIPT fi fi - # ============================================================================ - # IMPORT/RESTORE CONTAINERS - # ============================================================================ - echo "" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "IMPORT CONTAINERS FROM BACKUP" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "" - echo "If you have a previous Kopia backup, you can import/restore your" - echo "Docker containers from a backup drive." - echo "" - prompt_yn "Import containers from a Kopia backup? (y/n):" "n" IMPORT_CONTAINERS - - if [ "$IMPORT_CONTAINERS" = "y" ] || [ "$IMPORT_CONTAINERS" = "Y" ]; then - if [ "$DRY_RUN" = true ]; then - echo "[DRY-RUN] Would prompt for backup location and restore containers" - else - echo "" - prompt_text "Path to Kopia repository [e.g., /home/user/drives/backup1/kopia-repo]:" "" IMPORT_REPO - - if [ -d "$IMPORT_REPO" ]; then - echo "" - echo "Repository found. To restore:" - echo "" - echo "1. Start Kopia with this repository:" - echo " docker run -it --rm -v $IMPORT_REPO:/repository -v $DOCKER_DIR:/restore kopia/kopia repository connect filesystem --path=/repository" - echo "" - echo "2. List available snapshots:" - echo " docker run -it --rm -v $IMPORT_REPO:/repository kopia/kopia snapshot list" - echo "" - echo "3. Restore a snapshot:" - echo " docker run -it --rm -v $IMPORT_REPO:/repository -v $DOCKER_DIR:/restore kopia/kopia restore /restore" - echo "" - echo "4. Start your containers:" - echo " for dir in ~/docker/*/; do (cd \"\$dir\" && docker compose up -d); done" - echo "" - else - echo "Repository not found at $IMPORT_REPO" - echo "Make sure your backup drive is mounted and try again." - fi - fi - fi - echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Docker applications configured in: $DOCKER_DIR"