diff --git a/README.md b/README.md index d949188..b943283 100644 --- a/README.md +++ b/README.md @@ -265,11 +265,41 @@ INSTALLATION MODE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ [N] Normal install - Fresh install or modify existing + [M] Migration - Import existing Docker containers [R] Disaster recovery - Restore from Kopia backup -Select mode (N/R) [N]: +Select mode (N/M/R) [N]: ``` +## Migration Mode + +If you have existing Docker containers from another setup (different directory structure, another server, etc.), migration mode imports them without changing versions. + +### When to Use Migration + +- Moving from `/var/docker` or `/opt/docker` to `~/docker` +- Importing containers from another machine +- Adopting this script's structure for existing setups + +### What Migration Does + +1. **Scans source directory** - Auto-detects Docker directories, finds all compose files +2. **Shows containers found** - Lists each container with size +3. **Select what to migrate** - Whiptail checklist or text menu +4. **Stops containers** (optional) - Ensures clean copy of databases +5. **Copies containers** - Preserves versions, configs, and all data +6. **Starts containers** - In the new location +7. **Offers additional services** - Continue with normal install for more apps + +### Migration vs Restore + +| Feature | Migration | Disaster Recovery | +|---------|-----------|-------------------| +| Source | Existing Docker directory | Kopia backup | +| Versions | Preserved exactly | Preserved exactly | +| After | Install more services | Reconnect backups | +| Use case | Restructuring setup | OS drive failure | + ### 5. Follow Interactive Prompts The script shows current system status and asks: @@ -1046,6 +1076,14 @@ This script is provided as-is for Ubuntu 24.04 Desktop installations. ## Changelog +- **v2.10**: Migration mode for existing Docker setups + - New **Migration mode** - Import existing Docker containers from any directory + - Auto-detects Docker directories and scans for compose files + - Preserves container versions (no unwanted upgrades) + - Whiptail checklist for selecting which containers to migrate + - Option to stop containers for clean database copy + - After migration, offers to install additional services + - Three modes now: Normal install, Migration, Disaster Recovery - **v2.9**: Immich photo library, Watchtower, recovery improvements - **Immich**: Now asks for photo storage location (default: `~/drives/primary/photos`) - **Immich**: External library support for existing photos (read-only, no duplication) diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 8b07c8e..47cfebb 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -513,6 +513,299 @@ run_disaster_recovery() { return 0 } +# Migration function - import existing Docker containers +run_migration() { + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "MIGRATION MODE" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "This will migrate existing Docker containers to this script's structure." + echo "Containers will be copied (not moved) and versions preserved." + echo "" + + # Step 1: Get source Docker directory + echo "Step 1: Locate existing containers" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + # Try to auto-detect Docker directories + DETECTED_DIRS=() + + # Check common locations + for check_dir in /var/docker /opt/docker "$HOME/docker" /home/*/docker; do + if [ -d "$check_dir" ] && [ "$(find "$check_dir" -maxdepth 2 -name 'docker-compose.yml' -o -name 'compose.yml' 2>/dev/null | head -1)" ]; then + DETECTED_DIRS+=("$check_dir") + fi + done + + # Check mounted drives: ~/drives/*, /mnt/*, /media/* + for mount_base in "$HOME/drives" "$HOME_DIR/drives" /mnt /media; do + if [ -d "$mount_base" ]; then + for mount_dir in "$mount_base"/*; do + if [ -d "$mount_dir/docker" ]; then + check_dir="$mount_dir/docker" + if [ "$(find "$check_dir" -maxdepth 2 -name 'docker-compose.yml' -o -name 'compose.yml' 2>/dev/null | head -1)" ]; then + DETECTED_DIRS+=("$check_dir") + fi + fi + done + fi + done + + if [ ${#DETECTED_DIRS[@]} -gt 0 ]; then + echo "Auto-detected Docker directories:" + for i in "${!DETECTED_DIRS[@]}"; do + dir="${DETECTED_DIRS[$i]}" + count=$(find "$dir" -maxdepth 2 \( -name 'docker-compose.yml' -o -name 'compose.yml' \) 2>/dev/null | wc -l) + echo " [$((i+1))] $dir ($count containers)" + done + echo "" + echo "Enter a number to select, or type a custom path:" + else + echo "No Docker directories auto-detected." + echo "" + echo "Common locations:" + echo " • ~/docker" + echo " • ~/drives/primary/docker" + echo " • /var/docker" + echo " • /opt/docker" + echo " • /mnt/data/docker" + echo "" + echo "Enter the full path to your Docker directory:" + fi + echo "" + read -p "Source directory: " SOURCE_DOCKER_DIR + + # Check if user entered a number (to select from detected list) + if [[ "$SOURCE_DOCKER_DIR" =~ ^[0-9]+$ ]] && [ ${#DETECTED_DIRS[@]} -gt 0 ]; then + idx=$((SOURCE_DOCKER_DIR - 1)) + if [ $idx -ge 0 ] && [ $idx -lt ${#DETECTED_DIRS[@]} ]; then + SOURCE_DOCKER_DIR="${DETECTED_DIRS[$idx]}" + echo "Selected: $SOURCE_DOCKER_DIR" + fi + fi + + # Expand ~ if used + SOURCE_DOCKER_DIR="${SOURCE_DOCKER_DIR/#\~/$HOME}" + + if [ ! -d "$SOURCE_DOCKER_DIR" ]; then + echo "❌ Directory not found: $SOURCE_DOCKER_DIR" + return 1 + fi + + # Step 2: Scan for containers + echo "" + echo "Step 2: Scanning for containers" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + CONTAINERS_FOUND=() + CONTAINER_PATHS=() + + while IFS= read -r compose_file; do + if [ -n "$compose_file" ]; then + container_dir=$(dirname "$compose_file") + container_name=$(basename "$container_dir") + CONTAINERS_FOUND+=("$container_name") + CONTAINER_PATHS+=("$container_dir") + fi + done < <(find "$SOURCE_DOCKER_DIR" -maxdepth 2 \( -name 'docker-compose.yml' -o -name 'compose.yml' \) 2>/dev/null) + + if [ ${#CONTAINERS_FOUND[@]} -eq 0 ]; then + echo "❌ No Docker containers found in $SOURCE_DOCKER_DIR" + return 1 + fi + + echo "Found ${#CONTAINERS_FOUND[@]} container(s):" + for i in "${!CONTAINERS_FOUND[@]}"; do + container="${CONTAINERS_FOUND[$i]}" + path="${CONTAINER_PATHS[$i]}" + size=$(du -sh "$path" 2>/dev/null | cut -f1) + echo " [$((i+1))] $container ($size)" + done + echo "" + + # Step 3: Select containers to migrate + echo "Step 3: Select containers to migrate" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + SELECTED_CONTAINERS=() + + if command -v whiptail &> /dev/null && [ ${#CONTAINERS_FOUND[@]} -gt 3 ]; then + # Use whiptail for many containers + CHECKLIST_ARGS=() + for container in "${CONTAINERS_FOUND[@]}"; do + CHECKLIST_ARGS+=("$container" "" "ON") + done + + SELECTED=$(whiptail --title "Select Containers to Migrate" \ + --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ + 20 60 12 \ + "${CHECKLIST_ARGS[@]}" \ + 3>&1 1>&2 2>&3) + + if [ $? -eq 0 ] && [ -n "$SELECTED" ]; then + # Parse whiptail output + for container in $SELECTED; do + # Remove quotes + container="${container//\"/}" + SELECTED_CONTAINERS+=("$container") + done + fi + else + # Text-based selection + echo "Select containers to migrate:" + echo " [A] All containers" + echo " [S] Select individually" + echo " [N] None (cancel)" + echo "" + read -p "Choice (A/S/N) [A]: " MIGRATE_CHOICE + + case "${MIGRATE_CHOICE^^}" in + N) + echo "Migration cancelled." + return 0 + ;; + S) + for container in "${CONTAINERS_FOUND[@]}"; do + read -p " Migrate $container? (y/n) [y]: " MIGRATE_THIS + if [ "$MIGRATE_THIS" != "n" ] && [ "$MIGRATE_THIS" != "N" ]; then + SELECTED_CONTAINERS+=("$container") + fi + done + ;; + *) + SELECTED_CONTAINERS=("${CONTAINERS_FOUND[@]}") + ;; + esac + fi + + if [ ${#SELECTED_CONTAINERS[@]} -eq 0 ]; then + echo "No containers selected." + return 0 + fi + + echo "" + echo "Will migrate ${#SELECTED_CONTAINERS[@]} container(s):" + for container in "${SELECTED_CONTAINERS[@]}"; do + echo " • $container" + done + echo "" + + # Step 4: Stop running containers (optional) + echo "Step 4: Container status" + echo "━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Should running containers be stopped before migration?" + echo " • Stopping ensures clean copy of data/databases" + echo " • Not stopping may result in inconsistent state" + echo "" + read -p "Stop containers before copying? (y/n) [y]: " STOP_CONTAINERS + + if [ "$STOP_CONTAINERS" != "n" ] && [ "$STOP_CONTAINERS" != "N" ]; then + echo "Stopping containers..." + for container in "${SELECTED_CONTAINERS[@]}"; do + for i in "${!CONTAINERS_FOUND[@]}"; do + if [ "${CONTAINERS_FOUND[$i]}" = "$container" ]; then + container_dir="${CONTAINER_PATHS[$i]}" + if [ -f "$container_dir/docker-compose.yml" ] || [ -f "$container_dir/compose.yml" ]; then + echo " Stopping $container..." + (cd "$container_dir" && docker compose down 2>/dev/null) || true + fi + fi + done + done + echo "" + fi + + # Step 5: Copy containers + echo "Step 5: Migrating containers" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + + # Create target directory + mkdir -p "$DOCKER_DIR" + chown "$ACTUAL_USER:$ACTUAL_USER" "$DOCKER_DIR" + + MIGRATED_COUNT=0 + for container in "${SELECTED_CONTAINERS[@]}"; do + for i in "${!CONTAINERS_FOUND[@]}"; do + if [ "${CONTAINERS_FOUND[$i]}" = "$container" ]; then + source_dir="${CONTAINER_PATHS[$i]}" + target_dir="$DOCKER_DIR/$container" + + # Check if already exists + if [ -d "$target_dir" ]; then + echo " ⚠ $container already exists at $target_dir" + read -p " Overwrite? (y/n) [n]: " OVERWRITE + if [ "$OVERWRITE" != "y" ] && [ "$OVERWRITE" != "Y" ]; then + echo " Skipping $container" + continue + fi + rm -rf "$target_dir" + fi + + echo " Copying $container..." + cp -a "$source_dir" "$target_dir" + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$target_dir" + ((MIGRATED_COUNT++)) + echo " ✓ $container migrated" + fi + done + done + + echo "" + echo "✓ Migrated $MIGRATED_COUNT container(s) to $DOCKER_DIR" + echo "" + + # Step 6: Start migrated containers (optional) + echo "Step 6: Start containers" + echo "━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + read -p "Start migrated containers? (y/n) [y]: " START_MIGRATED + + if [ "$START_MIGRATED" != "n" ] && [ "$START_MIGRATED" != "N" ]; then + for container in "${SELECTED_CONTAINERS[@]}"; do + target_dir="$DOCKER_DIR/$container" + if [ -d "$target_dir" ]; then + echo " Starting $container..." + (cd "$target_dir" && docker compose up -d 2>/dev/null) || echo " ⚠ Failed to start $container" + fi + done + echo "" + fi + + # Step 7: Offer additional services + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "MIGRATION COMPLETE" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Containers migrated to: $DOCKER_DIR" + echo "" + echo "Would you like to install additional services not in your migration?" + echo "This will continue with the normal install process where you can" + echo "choose which additional apps to install (Immich, Frigate, etc.)" + echo "" + read -p "Continue to install additional services? (y/n) [y]: " INSTALL_MORE + + if [ "$INSTALL_MORE" = "n" ] || [ "$INSTALL_MORE" = "N" ]; then + echo "" + echo "Migration complete. You can run this script again to install more services." + echo "" + echo "To check running containers:" + echo " docker ps" + echo "" + return 0 + fi + + # Return to continue with normal install + INSTALL_MODE="post-migration" + return 0 +} + # Check for restore mode flag if [ "$RESTORE_MODE" = true ]; then run_disaster_recovery @@ -526,15 +819,29 @@ if [ "$UNATTENDED" != true ]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo " [N] Normal install - Fresh install or modify existing" + echo " [M] Migration - Import existing Docker containers" echo " [R] Disaster recovery - Restore from Kopia backup" echo "" - read -p "Select mode (N/R) [N]: " MODE_SELECT + read -p "Select mode (N/M/R) [N]: " MODE_SELECT case "${MODE_SELECT^^}" in R) run_disaster_recovery exit $? ;; + M) + run_migration + # If migration returns 0 and INSTALL_MODE is post-migration, continue + if [ $? -ne 0 ]; then + exit 1 + fi + if [ "$INSTALL_MODE" != "post-migration" ]; then + exit 0 + fi + echo "" + echo "Continuing with additional service installation..." + echo "" + ;; *) INSTALL_MODE="normal" ;;