diff --git a/README.md b/README.md index 9b37c3f..9987945 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,7 @@ Install containerized applications to `~/docker/{appname}/`: - **wg-easy** - WireGuard VPN with web UI - **Traccar** - GPS tracking server - **Portainer** - Docker management UI +- **MeshCentral Server** - Self-hosted remote management server - **FindMyDevice** - Self-hosted Android device tracking - **Frigate-Notify** - Push notifications for Frigate AI events @@ -632,6 +633,7 @@ docker compose up -d | Traccar | 8082 | http://localhost:8082 | | Portainer | 9443 | https://localhost:9443 | | FindMyDevice | 8084 | http://localhost:8084 | +| MeshCentral Server | 4430 | https://localhost:4430 | ### Container Backup & Restore (Kopia) @@ -1034,6 +1036,11 @@ This script is provided as-is for Ubuntu 24.04 Desktop installations. ## Changelog +- **v2.8**: MeshCentral Server and improved recovery + - Added MeshCentral Server (self-hosted remote management, web-based RDP/terminal) + - Recovery mode now installs core utilities first (openssh-server, git, curl, etc.) + - Added whiptail checklist for service selection during restore (Ubuntu-server style) + - Recovery supports restoring some/none/all services instead of all-or-nothing - **v2.7**: Disaster recovery mode - **One-click restore** from Kopia backup after system failure - New `--restore` flag for disaster recovery mode diff --git a/ubuntu-post-install.sh b/ubuntu-post-install.sh index 453aaa1..ca36919 100644 --- a/ubuntu-post-install.sh +++ b/ubuntu-post-install.sh @@ -112,15 +112,41 @@ run_disaster_recovery() { echo "DISASTER RECOVERY MODE" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" - echo "This will restore your Docker apps and data from a Kopia backup." + echo "This will restore your system and Docker apps 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" + # Step 1: Install core utilities + echo "Step 1: Installing core utilities" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "" + echo "Installing essential packages..." + apt-get update + apt-get install -y \ + net-tools \ + openssh-server \ + git \ + curl \ + wget \ + htop \ + ncdu \ + tree \ + zip \ + unzip \ + whiptail \ + 2>/dev/null || echo " ⚠ Some packages may have failed" + + # Start SSH + systemctl enable ssh 2>/dev/null || true + systemctl start ssh 2>/dev/null || true + echo "✓ Core utilities installed" + echo "" + + # Step 2: Find/mount backup drive + echo "Step 2: Locate backup drive" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Available block devices:" @@ -172,8 +198,8 @@ run_disaster_recovery() { echo "✓ Found Kopia repository at: $KOPIA_REPO" echo "" - # Step 2: Get Kopia password - echo "Step 2: Kopia password" + # Step 3: Get Kopia password + echo "Step 3: Kopia password" echo "━━━━━━━━━━━━━━━━━━━━━━" echo "" @@ -192,9 +218,9 @@ run_disaster_recovery() { echo "" fi - # Step 3: Install Docker if needed + # Step 4: Install Docker if needed echo "" - echo "Step 3: Ensure Docker is installed" + echo "Step 4: Ensure Docker is installed" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if ! command -v docker &> /dev/null; then echo "Installing Docker..." @@ -213,8 +239,8 @@ run_disaster_recovery() { fi echo "" - # Step 4: List snapshots and let user choose - echo "Step 4: Select snapshot to restore" + # Step 5: List snapshots and let user choose + echo "Step 5: Select snapshot to restore" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Connecting to repository and listing snapshots..." @@ -241,9 +267,9 @@ run_disaster_recovery() { echo "Using latest snapshot: $SNAPSHOT_ID" fi - # Step 5: Restore to temp location + # Step 6: Restore to temp location echo "" - echo "Step 5: Restoring from backup" + echo "Step 6: Restoring from backup" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" @@ -265,43 +291,91 @@ run_disaster_recovery() { echo "✓ Snapshot restored to temp location" echo "" - # Step 6: Detect and install services - echo "Step 6: Installing services from backup" - echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # Step 7: Detect and select services to restore + echo "Step 7: Select services to restore" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" mkdir -p "$DOCKER_DIR" # Find all docker-compose.yml files in restored backup + declare -A SERVICE_DIRS 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") + SERVICE_DIRS["$SERVICE_NAME"]="$SERVICE_DIR" 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 "Found ${#SERVICES_FOUND[@]} services in backup." echo "" - read -p "Restore all services? (y/n): " RESTORE_ALL + # Check if whiptail is available for nice UI + if command -v whiptail &> /dev/null; then + # Build whiptail checklist arguments + CHECKLIST_ARGS=() + for svc in "${SERVICES_FOUND[@]}"; do + CHECKLIST_ARGS+=("$svc" "" "ON") + done - 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" + # Show checklist - returns selected items + SELECTED=$(whiptail --title "Select Services to Restore" \ + --checklist "Use SPACE to select/deselect, ENTER to confirm:" \ + 20 60 12 \ + "${CHECKLIST_ARGS[@]}" \ + 3>&1 1>&2 2>&3) - echo "Restoring $SERVICE_NAME..." + # Parse selected services (whiptail returns quoted strings) + SELECTED_SERVICES=() + for item in $SELECTED; do + # Remove quotes + svc=$(echo "$item" | tr -d '"') + SELECTED_SERVICES+=("$svc") + done + else + # Fallback: text-based selection + echo "Select services to restore:" + echo " [A] All services" + echo " [N] None (skip restore)" + echo " [S] Select individually" + echo "" + read -p "Choice (A/N/S) [A]: " RESTORE_CHOICE + + case "${RESTORE_CHOICE^^}" in + N) + SELECTED_SERVICES=() + ;; + S) + SELECTED_SERVICES=() + for svc in "${SERVICES_FOUND[@]}"; do + read -p " Restore $svc? (y/n) [y]: " RESTORE_SVC + if [ "$RESTORE_SVC" != "n" ] && [ "$RESTORE_SVC" != "N" ]; then + SELECTED_SERVICES+=("$svc") + fi + done + ;; + *) + SELECTED_SERVICES=("${SERVICES_FOUND[@]}") + ;; + esac + fi + + # Restore selected services + echo "" + if [ ${#SELECTED_SERVICES[@]} -eq 0 ]; then + echo "No services selected for restore." + else + echo "Restoring ${#SELECTED_SERVICES[@]} services..." + echo "" + for svc in "${SELECTED_SERVICES[@]}"; do + SERVICE_DIR="${SERVICE_DIRS[$svc]}" + TARGET_DIR="$DOCKER_DIR/$svc" + + echo " Restoring $svc..." # Copy entire service directory cp -r "$SERVICE_DIR" "$TARGET_DIR" 2>/dev/null || true @@ -309,27 +383,62 @@ run_disaster_recovery() { # Fix ownership chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TARGET_DIR" 2>/dev/null || true - echo " ✓ $SERVICE_NAME restored to $TARGET_DIR" + echo " ✓ $svc restored to $TARGET_DIR" done fi fi - # Step 7: Start services + # Step 8: Start services echo "" - echo "Step 7: Starting services" + echo "Step 8: Starting services" echo "━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" - read -p "Start all restored services now? (y/n): " START_ALL + # Count restored services + RESTORED_COUNT=0 + for dir in "$DOCKER_DIR"/*/; do + if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/compose.yml" ]; then + ((RESTORED_COUNT++)) + fi + done - 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 + if [ $RESTORED_COUNT -eq 0 ]; then + echo "No services to start." + else + echo "Start restored services?" + echo " [A] All services" + echo " [S] Select which to start" + echo " [N] None (start manually later)" + echo "" + read -p "Choice (A/S/N) [A]: " START_CHOICE + + case "${START_CHOICE^^}" in + N) + echo "Services not started. Start manually with:" + echo " cd ~/docker/{service} && docker compose up -d" + ;; + S) + for dir in "$DOCKER_DIR"/*/; do + if [ -f "$dir/docker-compose.yml" ] || [ -f "$dir/compose.yml" ]; then + SERVICE_NAME=$(basename "$dir") + read -p " Start $SERVICE_NAME? (y/n) [y]: " START_SVC + if [ "$START_SVC" != "n" ] && [ "$START_SVC" != "N" ]; then + echo " Starting $SERVICE_NAME..." + (cd "$dir" && docker compose up -d 2>/dev/null) || echo " ⚠ Failed to start $SERVICE_NAME" + fi + fi + done + ;; + *) + 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 + ;; + esac fi # Cleanup @@ -2832,6 +2941,79 @@ PORTAINER_COMPOSE fi fi + # ---- MESHCENTRAL SERVER ---- + echo "" + echo "┌─────────────────────────────────────────────────────────────────┐" + echo "│ MESHCENTRAL SERVER - Self-hosted remote management │" + echo "│ Full MeshCentral server (not just agent). Manage all devices. │" + echo "│ Port: 4430 (https), 4433 (agent) │" + echo "└─────────────────────────────────────────────────────────────────┘" + prompt_yn "Install MeshCentral Server? (y/n):" "n" INSTALL_MESHCENTRAL_SERVER + + if [ "$INSTALL_MESHCENTRAL_SERVER" = "y" ] || [ "$INSTALL_MESHCENTRAL_SERVER" = "Y" ]; then + echo "Installing MeshCentral Server..." + MC_DIR="$DOCKER_DIR/meshcentral" + + if [ "$DRY_RUN" = true ]; then + echo "[DRY-RUN] Would create $MC_DIR" + else + mkdir -p "$MC_DIR" 2>/dev/null || true + cd "$MC_DIR" 2>/dev/null || cd "$DOCKER_DIR" + + cat > docker-compose.yml << 'MC_COMPOSE' +name: meshcentral + +services: + meshcentral: + image: ghcr.io/ylianst/meshcentral:latest + container_name: meshcentral + hostname: meshcentral + restart: unless-stopped + environment: + - NODE_ENV=production + - HOSTNAME=${MC_HOSTNAME:-localhost} + - REVERSE_PROXY=${MC_REVERSE_PROXY:-false} + - REVERSE_PROXY_TLS_PORT=${MC_TLS_PORT:-443} + - IFRAME=false + - ALLOW_NEW_ACCOUNTS=true + - WEBRTC=true + volumes: + - ./data:/opt/meshcentral/meshcentral-data + - ./files:/opt/meshcentral/meshcentral-files + - ./backups:/opt/meshcentral/meshcentral-backups + ports: + - "4430:443" + - "4433:4433" +MC_COMPOSE + echo "✓ Installed docker-compose.yml" + + # Ask for hostname + echo "" + prompt_text "MeshCentral hostname (domain or IP) [localhost]:" "localhost" MC_HOSTNAME 2>/dev/null || MC_HOSTNAME="localhost" + + cat > .env << MC_ENV +MC_HOSTNAME=$MC_HOSTNAME +MC_REVERSE_PROXY=false +MC_TLS_PORT=443 +MC_ENV + + mkdir -p data files backups 2>/dev/null || true + chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR" 2>/dev/null || true + + echo "" + echo "✓ MeshCentral Server installed at $MC_DIR" + echo " Start: cd $MC_DIR && docker compose up -d" + echo " Access: https://localhost:4430" + echo "" + echo " First visit: Create admin account" + echo " Then: Add devices → Download agent for each OS" + echo "" + echo " ⚠️ For remote access, set MC_HOSTNAME in .env to your domain/IP" + echo " Docs: https://meshcentral.com/docs/" + echo "" + fi + fi + # ---- FINDMYDEVICE (FMD) ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐"