#!/bin/bash # Ubuntu 24.04 Post-Installation Script # Run with: sudo bash post-install.sh # This script is rerunnable - it detects existing installations # ============================================================================ # COMMAND-LINE ARGUMENT PARSING # ============================================================================ DRY_RUN=false UNATTENDED=false LOG_FILE="/var/log/post-install.log" show_help() { echo "Ubuntu 24.04 Post-Installation Script" echo "" echo "Usage: sudo ./post-install.sh [OPTIONS]" echo "" echo "Options:" echo " --dry-run Preview what would be installed without making changes" 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) DRY_RUN=true ;; --unattended) UNATTENDED=true ;; --restore) RESTORE_MODE=true ;; --help|-h) show_help ;; *) echo "Unknown option: $arg" echo "Use --help for usage information" exit 1 ;; esac done # ============================================================================ # LOGGING SETUP # ============================================================================ # Create log file and tee all output exec > >(tee -a "$LOG_FILE") 2>&1 echo "" echo "=== Post-Install Log Started: $(date) ===" >> "$LOG_FILE" echo "=== Ubuntu 24.04 Post-Installation Script ===" echo "" if [ "$DRY_RUN" = true ]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DRY RUN MODE - No changes will be made" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" fi if [ "$UNATTENDED" = true ]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "UNATTENDED MODE - Using default options" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" fi # Check if running as root if [ "$EUID" -ne 0 ]; then echo "Please run as root (use sudo)" exit 1 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 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: 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" # Install Kopia for backup management echo "Installing Kopia..." if ! command -v kopia &> /dev/null; then curl -s https://kopia.io/signing-key | gpg --dearmor -o /usr/share/keyrings/kopia-keyring.gpg 2>/dev/null echo "deb [signed-by=/usr/share/keyrings/kopia-keyring.gpg] https://packages.kopia.io/apt/ stable main" | tee /etc/apt/sources.list.d/kopia.list apt-get update apt-get install -y kopia 2>/dev/null || echo " ⚠ Kopia install failed" fi # 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:" 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 3: Get Kopia password echo "Step 3: 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 4: Install Docker if needed echo "" echo "Step 4: 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 5: List snapshots and let user choose echo "Step 5: 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 6: Restore to temp location echo "" echo "Step 6: 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 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" else echo "Found ${#SERVICES_FOUND[@]} services in backup." echo "" # 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 # 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) # 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 # Fix ownership chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TARGET_DIR" 2>/dev/null || true echo " ✓ $svc restored to $TARGET_DIR" done fi fi # Step 8: Start services echo "" echo "Step 8: Starting services" echo "━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" # 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 [ $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 echo "" read -p "Remove temporary restore files? (y/n): " CLEANUP if [ "$CLEANUP" = "y" ]; then rm -rf "$RESTORE_TEMP" echo "✓ Cleaned up temp files" fi # Step 9: Reconnect Kopia for future backups echo "" echo "Step 9: Setting up future backups" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Reconnecting Kopia to backup repository for future backups..." if command -v kopia &> /dev/null && [ -n "$KOPIA_REPO" ]; then # Reconnect to the repository if kopia repository connect filesystem --path="$KOPIA_REPO" --password="$KOPIA_PASSWORD" 2>/dev/null; then echo "✓ Kopia reconnected to repository" echo " Repository: $KOPIA_REPO" # Verify backup scripts exist if [ -f "$DOCKER_DIR/kopia/backup-containers.sh" ]; then chmod +x "$DOCKER_DIR/kopia/backup-containers.sh" 2>/dev/null || true echo "✓ Backup script ready: $DOCKER_DIR/kopia/backup-containers.sh" else echo " ⚠ Backup script not found - run normal install to set up" fi else echo " ⚠ Could not reconnect to repository" echo " Run manually: kopia repository connect filesystem --path=\"$KOPIA_REPO\"" fi else echo " ⚠ Kopia not available or repository not found" echo " Run the normal install script to set up backups" fi echo "" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DISASTER RECOVERY COMPLETE" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Services restored to: $DOCKER_DIR" echo "" echo "Backups: $(command -v kopia &>/dev/null && kopia repository status &>/dev/null && echo "✓ Connected" || echo "⚠ Not connected")" echo " Run backups: $DOCKER_DIR/kopia/backup-containers.sh" 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 # ============================================================================ is_docker_installed() { command -v docker &> /dev/null && systemctl is-active --quiet docker 2>/dev/null } is_samba_installed() { command -v smbd &> /dev/null && systemctl is-active --quiet smbd 2>/dev/null } is_netbird_installed() { command -v netbird &> /dev/null } is_rustdesk_installed() { command -v rustdesk &> /dev/null || dpkg -l rustdesk &> /dev/null } is_rclone_installed() { command -v rclone &> /dev/null } is_rsync_installed() { command -v rsync &> /dev/null } is_ufw_installed() { command -v ufw &> /dev/null } is_wireguard_installed() { command -v wg &> /dev/null } is_tailscale_installed() { command -v tailscale &> /dev/null } is_teamviewer_installed() { command -v teamviewer &> /dev/null || dpkg -l teamviewer &> /dev/null 2>&1 } is_meshcentral_installed() { # MeshCentral agent is typically installed as meshagent command -v meshagent &> /dev/null || [ -f /usr/local/mesh_services/meshagent/meshagent ] } # ============================================================================ # DRY-RUN AND UNATTENDED HELPER FUNCTIONS # ============================================================================ # Run a command, but skip it in dry-run mode run_cmd() { if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would execute: $*" return 0 else "$@" fi } # Prompt for yes/no, with unattended default # Usage: prompt_yn "Question?" "default" VARNAME # default can be "y" or "n" prompt_yn() { local question="$1" local default="$2" local varname="$3" if [ "$UNATTENDED" = true ]; then eval "$varname='$default'" echo "$question [auto: $default]" return fi read -p "$question " response eval "$varname='$response'" } # Prompt for text input, with unattended default # Usage: prompt_text "Question?" "default" VARNAME prompt_text() { local question="$1" local default="$2" local varname="$3" if [ "$UNATTENDED" = true ]; then eval "$varname='$default'" echo "$question [auto: $default]" return fi read -p "$question " response eval "$varname='$response'" } # ============================================================================ # SHOW CURRENT INSTALLATION STATUS # ============================================================================ echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "CURRENT SYSTEM STATUS" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" if is_docker_installed; then echo " ✓ Docker: Installed ($(docker --version 2>/dev/null | cut -d' ' -f3 | tr -d ','))" else echo " ○ Docker: Not installed" fi if is_samba_installed; then echo " ✓ Samba: Installed and running" else echo " ○ Samba: Not installed" fi if is_netbird_installed; then echo " ✓ NetBird: Installed" else echo " ○ NetBird: Not installed" fi if is_rustdesk_installed; then echo " ✓ RustDesk: Installed" else echo " ○ RustDesk: Not installed" fi if is_teamviewer_installed; then echo " ✓ TeamViewer: Installed" else echo " ○ TeamViewer: Not installed" fi if is_meshcentral_installed; then echo " ✓ MeshCentral Agent: Installed" else echo " ○ MeshCentral Agent: Not installed" fi if is_wireguard_installed; then echo " ✓ WireGuard: Installed" else echo " ○ WireGuard: Not installed" fi if is_tailscale_installed; then echo " ✓ Tailscale: Installed" else echo " ○ Tailscale: Not installed" fi if is_rclone_installed; then echo " ✓ rclone: Installed" else echo " ○ rclone: Not installed" fi if is_rsync_installed; then echo " ✓ rsync: Installed" else echo " ○ rsync: Not installed" fi if is_ufw_installed; then if ufw status 2>/dev/null | grep -q "Status: active"; then echo " ✓ UFW Firewall: Enabled" else echo " ○ UFW Firewall: Installed but disabled" fi else echo " ○ UFW Firewall: Not installed" fi echo "" echo "This script can reinstall/reconfigure any component." echo "" # Update package list echo "Updating package lists..." run_cmd apt update # Install basic utilities echo "" echo "Installing basic utilities..." echo " - net-tools: Network configuration tools (ifconfig, netstat, etc.)" echo " - ncdu: Disk usage analyzer with ncurses interface" echo " - git: Version control system" echo " - curl: Command-line tool for transferring data with URLs" echo " - wget: Network downloader" echo " - htop: Interactive process viewer" echo " - tree: Display directory structure in tree format" echo " - zip/unzip: Archive compression utilities" echo " - rclone: Rsync for cloud storage and local drives (backup tool)" echo "" run_cmd apt install -y \ net-tools \ ncdu \ git \ curl \ wget \ htop \ tree \ zip \ unzip \ rclone || echo "Warning: Some utilities failed to install, continuing..." # Install OpenSSH Server echo "" echo "Installing OpenSSH Server..." echo " - openssh-server: SSH server for remote access" echo "" apt install -y openssh-server || echo "Warning: OpenSSH server installation failed, continuing..." # Start and enable SSH service systemctl start ssh || echo "Warning: Failed to start SSH" systemctl enable ssh || echo "Warning: Failed to enable SSH" # Generate SSH key for this computer echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "SSH KEY GENERATION" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" prompt_yn "Generate a new SSH key for this computer? (y/n):" "n" GENERATE_KEY if [ "$GENERATE_KEY" = "y" ] || [ "$GENERATE_KEY" = "Y" ]; then echo "" read -p "Enter a label/comment for the key (e.g., email or hostname) [default: $ACTUAL_USER@$(hostname)]: " KEY_COMMENT if [ -z "$KEY_COMMENT" ]; then KEY_COMMENT="$ACTUAL_USER@$(hostname)" fi # Check if key already exists if [ -f "$ACTUAL_HOME/.ssh/id_rsa" ]; then echo "" echo "⚠️ WARNING: SSH key already exists at $ACTUAL_HOME/.ssh/id_rsa" read -p "Overwrite existing key? This cannot be undone! (y/n): " OVERWRITE_KEY if [ "$OVERWRITE_KEY" != "y" ] && [ "$OVERWRITE_KEY" != "Y" ]; then echo "Skipping key generation." GENERATE_KEY="n" fi fi if [ "$GENERATE_KEY" = "y" ] || [ "$GENERATE_KEY" = "Y" ]; then echo "" echo "Generating 4096-bit RSA key pair..." echo "This may take a moment..." # Generate key as the actual user, not root sudo -u "$ACTUAL_USER" ssh-keygen -t rsa -b 4096 -C "$KEY_COMMENT" -f "$ACTUAL_HOME/.ssh/id_rsa" -N "" if [ $? -eq 0 ]; then echo "" echo "✓ SSH key generated successfully!" echo "" echo "Private key: $ACTUAL_HOME/.ssh/id_rsa (keep this secret!)" echo "Public key: $ACTUAL_HOME/.ssh/id_rsa.pub" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Your PUBLIC key (safe to share):" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" cat "$ACTUAL_HOME/.ssh/id_rsa.pub" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "To use this key:" echo " • Add to GitHub: Settings → SSH and GPG keys → New SSH key" echo " • Add to servers: Copy above key to remote ~/.ssh/authorized_keys" echo " • View anytime: cat ~/.ssh/id_rsa.pub" echo "" else echo "✗ Failed to generate SSH key" fi fi else echo "Skipping SSH key generation." fi # Import SSH keys from GitHub/Launchpad echo "" prompt_text "Import SSH keys from GitHub? (enter username or leave blank to skip):" "" GITHUB_USER prompt_text "Import SSH keys from Launchpad? (enter username or leave blank to skip):" "" LAUNCHPAD_USER KEYS_IMPORTED=false # Create .ssh directory if it doesn't exist mkdir -p "$ACTUAL_HOME/.ssh" touch "$ACTUAL_HOME/.ssh/authorized_keys" chmod 700 "$ACTUAL_HOME/.ssh" chmod 600 "$ACTUAL_HOME/.ssh/authorized_keys" if [ -n "$GITHUB_USER" ]; then echo "Importing SSH keys from GitHub user: $GITHUB_USER" if curl -fsSL "https://github.com/$GITHUB_USER.keys" >> "$ACTUAL_HOME/.ssh/authorized_keys" 2>/dev/null; then echo "✓ GitHub keys imported successfully" KEYS_IMPORTED=true else echo "✗ Failed to import GitHub keys" fi fi if [ -n "$LAUNCHPAD_USER" ]; then echo "Importing SSH keys from Launchpad user: $LAUNCHPAD_USER" if curl -fsSL "https://launchpad.net/~$LAUNCHPAD_USER/+sshkeys" >> "$ACTUAL_HOME/.ssh/authorized_keys" 2>/dev/null; then echo "✓ Launchpad keys imported successfully" KEYS_IMPORTED=true else echo "✗ Failed to import Launchpad keys" fi fi # Fix ownership chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ACTUAL_HOME/.ssh" # Disable password authentication if keys were imported if [ "$KEYS_IMPORTED" = true ]; then echo "" echo "SSH keys imported. Disabling password authentication..." # Backup sshd_config cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup # Disable password authentication sed -i 's/^#*PasswordAuthentication yes/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/^#*PasswordAuthentication no/PasswordAuthentication no/' /etc/ssh/sshd_config # Ensure these settings are also set grep -q "^PasswordAuthentication" /etc/ssh/sshd_config || echo "PasswordAuthentication no" >> /etc/ssh/sshd_config # Restart SSH service to apply changes systemctl restart ssh echo "✓ Password authentication disabled. SSH key authentication required." echo "✓ Backup saved to /etc/ssh/sshd_config.backup" else echo "" echo "No SSH keys imported. Password authentication remains enabled." # Offer fail2ban since password auth is still enabled echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "FAIL2BAN (Recommended with password SSH)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Since password authentication is enabled, fail2ban is recommended" echo "to protect against brute-force SSH attacks." echo "" prompt_yn "Install and enable fail2ban? (y/n):" "y" INSTALL_FAIL2BAN if [ "$INSTALL_FAIL2BAN" = "y" ] || [ "$INSTALL_FAIL2BAN" = "Y" ]; then echo "" echo "Installing fail2ban..." run_cmd apt install -y fail2ban if [ "$DRY_RUN" != true ]; then # Create local config to protect SSH cat > /etc/fail2ban/jail.local << 'FAIL2BAN_CONFIG' [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 5 bantime = 3600 findtime = 600 FAIL2BAN_CONFIG run_cmd systemctl enable fail2ban run_cmd systemctl restart fail2ban echo "✓ fail2ban installed and configured" echo " - 5 failed attempts = 1 hour ban" echo " - View banned IPs: sudo fail2ban-client status sshd" fi else echo "Skipping fail2ban installation." fi fi INSTALL_FAIL2BAN="${INSTALL_FAIL2BAN:-n}" # Docker Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DOCKER INSTALLATION" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" if is_docker_installed; then echo "Docker is already installed: $(docker --version 2>/dev/null)" prompt_yn "Reinstall Docker? (y/n):" "n" INSTALL_DOCKER else prompt_yn "Install Docker? (y/n):" "y" INSTALL_DOCKER fi if [ "$INSTALL_DOCKER" = "y" ] || [ "$INSTALL_DOCKER" = "Y" ]; then echo "" echo "Installing Docker prerequisites..." echo " - ca-certificates: SSL/TLS certificates for secure connections" echo " - gnupg: GNU Privacy Guard for package verification" echo " - lsb-release: Provides Ubuntu version information" echo "" run_cmd apt install -y \ ca-certificates \ gnupg \ lsb-release || echo "Warning: Some prerequisites failed to install, continuing..." echo "" echo "Installing Docker..." echo " - Docker Engine: Container runtime platform" echo " - Docker Compose: Multi-container application orchestration" echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would remove old Docker packages" echo "[DRY-RUN] Would add Docker GPG key and repository" echo "[DRY-RUN] Would install docker-ce, docker-ce-cli, containerd.io, plugins" echo "[DRY-RUN] Would start and enable Docker service" echo "[DRY-RUN] Would add $SUDO_USER to docker group" else # Remove old Docker packages if they exist apt remove -y docker docker-engine docker.io containerd runc 2>/dev/null || true # Add Docker's official GPG key install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc # Add Docker repository echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ tee /etc/apt/sources.list.d/docker.list > /dev/null # Update package list with Docker repo apt update # Install Docker Engine, CLI, containerd, and Docker Compose plugin apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin || echo "Warning: Docker installation failed, continuing..." # Start and enable Docker service systemctl start docker || echo "Warning: Failed to start Docker" systemctl enable docker || echo "Warning: Failed to enable Docker" # Add current user to docker group (if not root) if [ -n "$SUDO_USER" ]; then usermod -aG docker "$SUDO_USER" echo "User $SUDO_USER added to docker group" fi # Verify Docker installation echo "" echo "Verifying Docker installation..." docker --version || echo "Warning: Docker verification failed" docker compose version || echo "Warning: Docker Compose verification failed" fi else echo "Skipping Docker installation." fi # Samba File Sharing (Optional) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "SAMBA FILE SHARING (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Samba allows you to share folders over the network to Windows, Mac, and Linux." echo "The script will share your primary drive at ~/drives/primary" echo "" if is_samba_installed; then echo "Samba is already installed and running." if grep -q "\[Primary\]" /etc/samba/smb.conf 2>/dev/null; then echo " Share 'Primary' is configured at: $ACTUAL_HOME/drives/primary" fi echo "" prompt_yn "Reconfigure Samba? (y/n):" "n" INSTALL_SAMBA else prompt_yn "Install and configure Samba file sharing? (y/n):" "n" INSTALL_SAMBA fi if [ "$INSTALL_SAMBA" = "y" ] || [ "$INSTALL_SAMBA" = "Y" ]; then echo "" echo "Installing Samba file sharing..." echo " - Samba: SMB/CIFS file server for network file sharing" echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would install samba" echo "[DRY-RUN] Would configure [Primary] share at $ACTUAL_HOME/drives/primary" echo "[DRY-RUN] Would prompt for Samba password" echo "[DRY-RUN] Would enable and start smbd/nmbd services" else run_cmd apt install -y samba || echo "Warning: Samba installation failed, continuing..." # Configure Samba share for primary drive if command -v smbd &> /dev/null; then echo "" echo "Configuring Samba share for primary drive..." # Backup existing config cp /etc/samba/smb.conf /etc/samba/smb.conf.backup-$(date +%Y%m%d-%H%M%S) # Add Primary share configuration if ! grep -q "\[Primary\]" /etc/samba/smb.conf; then cat >> /etc/samba/smb.conf << SAMBA_CONFIG # Primary drive share - added by post-install script [Primary] comment = Primary Drive path = $ACTUAL_HOME/drives/primary browseable = yes read only = no writable = yes valid users = $ACTUAL_USER create mask = 0775 directory mask = 0775 SAMBA_CONFIG echo "✓ Added [Primary] share to Samba configuration" else echo "Samba [Primary] share already configured, skipping..." fi # Add Samba user (skip in unattended mode - user must set password manually) if [ "$UNATTENDED" != true ]; then echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "SAMBA PASSWORD SETUP" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Set a password for Samba file sharing access." echo "Tip: Using the same password as your system login is convenient." echo "" smbpasswd -a "$ACTUAL_USER" else echo "Skipping Samba password setup (unattended mode)" echo "Set password later with: sudo smbpasswd -a $ACTUAL_USER" fi # Enable and restart Samba services systemctl enable smbd nmbd || echo "Warning: Failed to enable Samba services" systemctl restart smbd nmbd || echo "Warning: Failed to restart Samba services" echo "" echo "✓ Samba configured successfully" echo " Share name: Primary" echo " Path: $ACTUAL_HOME/drives/primary" echo " Access: \\\\$(hostname)\\Primary (Windows) or smb://$(hostname)/Primary (Mac/Linux)" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "ADDING MORE SAMBA SHARES" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "To add another share, edit the Samba config file:" echo "" echo " sudo nano /etc/samba/smb.conf" echo "" echo "Add a new section at the end:" echo "" echo " [ShareName]" echo " comment = Description of share" echo " path = /path/to/folder" echo " browseable = yes" echo " read only = no" echo " writable = yes" echo " valid users = $ACTUAL_USER" echo " create mask = 0775" echo " directory mask = 0775" echo "" echo "Save (Ctrl+O, Enter) and exit (Ctrl+X), then restart Samba:" echo "" echo " sudo systemctl restart smbd nmbd" echo "" echo "Verify the share is active:" echo "" echo " testparm -s" echo "" else echo "✗ Samba installation failed, skipping configuration" fi fi else echo "Skipping Samba installation." fi # NetBird Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "NETBIRD MESH VPN (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "NetBird is a secure mesh VPN for connecting devices across networks." echo "" if is_netbird_installed; then echo "NetBird is already installed." netbird status 2>/dev/null || true echo "" prompt_yn "Reinstall NetBird? (y/n):" "n" INSTALL_NETBIRD else prompt_yn "Install NetBird? (y/n):" "n" INSTALL_NETBIRD fi if [ "$INSTALL_NETBIRD" = "y" ] || [ "$INSTALL_NETBIRD" = "Y" ]; then echo "" echo "Installing NetBird..." echo " - NetBird: Secure mesh VPN for connecting devices" echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would download and run NetBird install script" else curl -fsSL https://pkgs.netbird.io/install.sh | sh || echo "Warning: NetBird installation failed, continuing..." echo "" echo "NetBird installed. Setup instructions:" echo " 1. Create account at https://app.netbird.io (or self-host)" echo " 2. Run 'netbird up' and authenticate via browser" echo "" echo "For NetBird SSH functionality:" echo " • Enable SSH in NetBird dashboard settings" echo " • Use 'netbird ssh ' to connect to peers" echo " • NetBird manages SSH keys automatically when using 'netbird ssh'" echo " • Traditional SSH also works using peer IPs from 'netbird status'" echo " • Configure ACL rules in dashboard for SSH access (port 22)" echo "" fi else echo "Skipping NetBird installation." fi # WireGuard Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "WIREGUARD VPN (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "WireGuard is a fast, modern VPN protocol." echo " • Lightweight and high-performance" echo " • Simple configuration via config files" echo " • Built into Linux kernel" echo "" if is_wireguard_installed; then echo "WireGuard is already installed." echo "" prompt_yn "Reinstall WireGuard? (y/n):" "n" INSTALL_WIREGUARD else prompt_yn "Install WireGuard? (y/n):" "n" INSTALL_WIREGUARD fi if [ "$INSTALL_WIREGUARD" = "y" ] || [ "$INSTALL_WIREGUARD" = "Y" ]; then echo "" echo "Installing WireGuard..." echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would install wireguard wireguard-tools" else apt install -y wireguard wireguard-tools || echo "Warning: WireGuard installation failed, continuing..." echo "" echo "WireGuard installed. Setup instructions:" echo "" echo "Generate keys:" echo " wg genkey | tee /etc/wireguard/privatekey | wg pubkey > /etc/wireguard/publickey" echo "" echo "Create config at /etc/wireguard/wg0.conf:" echo " [Interface]" echo " PrivateKey = " echo " Address = 10.0.0.1/24" echo " ListenPort = 51820" echo "" echo " [Peer]" echo " PublicKey = " echo " AllowedIPs = 10.0.0.2/32" echo " Endpoint = peer.example.com:51820" echo "" echo "Start WireGuard:" echo " sudo wg-quick up wg0" echo " sudo systemctl enable wg-quick@wg0 # Start on boot" echo "" fi else echo "Skipping WireGuard installation." fi # Tailscale Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "TAILSCALE VPN (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Tailscale is a zero-config mesh VPN built on WireGuard." echo " • Easy setup - just sign in" echo " • Built on WireGuard for performance" echo " • Automatic NAT traversal" echo " • Built-in SSH (Tailscale SSH)" echo "" if is_tailscale_installed; then echo "Tailscale is already installed." tailscale status 2>/dev/null || true echo "" prompt_yn "Reinstall Tailscale? (y/n):" "n" INSTALL_TAILSCALE else prompt_yn "Install Tailscale? (y/n):" "n" INSTALL_TAILSCALE fi if [ "$INSTALL_TAILSCALE" = "y" ] || [ "$INSTALL_TAILSCALE" = "Y" ]; then echo "" echo "Installing Tailscale..." echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would add Tailscale apt repository" echo "[DRY-RUN] Would install tailscale" else # Add Tailscale's package signing key and repository curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/jammy.tailscale-keyring.list | tee /etc/apt/sources.list.d/tailscale.list apt update apt install -y tailscale || echo "Warning: Tailscale installation failed, continuing..." echo "" echo "Tailscale installed. Setup instructions:" echo "" echo "Connect to Tailscale network:" echo " sudo tailscale up" echo "" echo "This opens a browser to authenticate. After that:" echo " tailscale status # View connected devices" echo " tailscale ip # Show your Tailscale IP" echo "" echo "Tailscale SSH (optional - enable in admin console):" echo " • Enable 'SSH' in Tailscale admin console for this machine" echo " • Connect with: ssh user@device-name (uses Tailscale identity)" echo " • No SSH keys needed - Tailscale handles authentication" echo "" fi else echo "Skipping Tailscale installation." fi # RustDesk Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "RUSTDESK REMOTE DESKTOP (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "RustDesk is an open-source remote desktop software." echo "" if is_rustdesk_installed; then echo "RustDesk is already installed." echo "" prompt_yn "Reinstall RustDesk? (y/n):" "n" INSTALL_RUSTDESK else prompt_yn "Install RustDesk? (y/n):" "n" INSTALL_RUSTDESK fi if [ "$INSTALL_RUSTDESK" = "y" ] || [ "$INSTALL_RUSTDESK" = "Y" ]; then echo "" echo "Installing RustDesk..." echo " - RustDesk: Open-source remote desktop software" echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would download latest RustDesk from GitHub" echo "[DRY-RUN] Would install rustdesk .deb package" else # Download latest RustDesk .deb package RUSTDESK_VERSION=$(curl -s https://api.github.com/repos/rustdesk/rustdesk/releases/latest | grep -oP '"tag_name": "\K(.*)(?=")') RUSTDESK_URL="https://github.com/rustdesk/rustdesk/releases/download/${RUSTDESK_VERSION}/rustdesk-${RUSTDESK_VERSION}-x86_64.deb" wget -O /tmp/rustdesk.deb "$RUSTDESK_URL" || echo "Warning: RustDesk download failed, continuing..." if [ -f /tmp/rustdesk.deb ]; then apt install -y /tmp/rustdesk.deb || echo "Warning: RustDesk installation failed, continuing..." rm /tmp/rustdesk.deb echo "✓ RustDesk installed" fi fi else echo "Skipping RustDesk installation." fi # TeamViewer Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "TEAMVIEWER REMOTE DESKTOP (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "TeamViewer is commercial remote desktop software with a free tier." echo " • Cross-platform (Windows, Mac, Linux, mobile)" echo " • Easy to use - no port forwarding needed" echo " • Requires TeamViewer account for unattended access" echo "" if is_teamviewer_installed; then echo "TeamViewer is already installed." echo "" prompt_yn "Reinstall TeamViewer? (y/n):" "n" INSTALL_TEAMVIEWER else prompt_yn "Install TeamViewer? (y/n):" "n" INSTALL_TEAMVIEWER fi if [ "$INSTALL_TEAMVIEWER" = "y" ] || [ "$INSTALL_TEAMVIEWER" = "Y" ]; then echo "" echo "Installing TeamViewer..." echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would download TeamViewer .deb package" echo "[DRY-RUN] Would install teamviewer" else # Download TeamViewer .deb package wget -O /tmp/teamviewer.deb "https://download.teamviewer.com/download/linux/teamviewer_amd64.deb" || echo "Warning: TeamViewer download failed, continuing..." if [ -f /tmp/teamviewer.deb ]; then apt install -y /tmp/teamviewer.deb || echo "Warning: TeamViewer installation failed, continuing..." rm /tmp/teamviewer.deb echo "" echo "TeamViewer installed. Setup instructions:" echo "" echo "Start TeamViewer:" echo " teamviewer" echo "" echo "For unattended access:" echo " 1. Open TeamViewer" echo " 2. Go to Extras → Options → Security" echo " 3. Set a personal password for unattended access" echo " 4. Note your TeamViewer ID (shown in main window)" echo "" echo "✓ TeamViewer installed" fi fi else echo "Skipping TeamViewer installation." fi # MeshCentral Installation echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "MESHCENTRAL AGENT (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "MeshCentral is an open-source remote management solution." echo " • Self-hosted or use public servers" echo " • Web-based remote desktop (no client software needed)" echo " • Terminal, file transfer, and remote desktop" echo " • Requires a MeshCentral server to connect to" echo "" if is_meshcentral_installed; then echo "MeshCentral Agent is already installed." echo "" prompt_yn "Reinstall MeshCentral Agent? (y/n):" "n" INSTALL_MESHCENTRAL else prompt_yn "Install MeshCentral Agent? (y/n):" "n" INSTALL_MESHCENTRAL fi if [ "$INSTALL_MESHCENTRAL" = "y" ] || [ "$INSTALL_MESHCENTRAL" = "Y" ]; then echo "" echo "MeshCentral requires a server URL to connect to." echo "" echo "If you have a MeshCentral server, the agent install is typically done by:" echo " 1. Log into your MeshCentral web interface" echo " 2. Go to 'My Devices' → 'Add Agent'" echo " 3. Download and run the Linux agent installer" echo "" echo "Example (replace with your server's URL):" echo " wget -O meshagent https://your-meshcentral-server/meshagents?id=XXXXX" echo " chmod +x meshagent" echo " sudo ./meshagent -install" echo "" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would prompt for MeshCentral server URL" echo "[DRY-RUN] Would download and install meshagent" else prompt_text "Enter MeshCentral agent URL (or leave blank to skip):" "" MESHCENTRAL_URL if [ -n "$MESHCENTRAL_URL" ]; then wget -O /tmp/meshagent "$MESHCENTRAL_URL" || echo "Warning: MeshCentral agent download failed" if [ -f /tmp/meshagent ]; then chmod +x /tmp/meshagent /tmp/meshagent -install || echo "Warning: MeshCentral agent installation failed" rm /tmp/meshagent echo "" echo "✓ MeshCentral Agent installed" echo " Check your MeshCentral server - this device should appear shortly." fi else echo "" echo "No URL provided. Skipping MeshCentral agent installation." echo "You can install later by downloading the agent from your MeshCentral server." fi fi else echo "Skipping MeshCentral installation." fi # ============================================================================ # SELF-HOSTED DOCKER APPLICATIONS (Optional) # ============================================================================ # These applications run in Docker containers using docker-compose # Each app is installed to ~/docker/{appname}/ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "SELF-HOSTED DOCKER APPLICATIONS (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Install self-hosted applications using Docker Compose." echo "Each app will be installed to ~/docker/{appname}/" echo "" echo "Note: Docker must be installed for these applications." echo "" if ! is_docker_installed && [ "$INSTALL_DOCKER" != "y" ] && [ "$INSTALL_DOCKER" != "Y" ]; then echo "Docker is not installed. Skipping self-hosted applications." echo "Install Docker first, then rerun this script." else # Create docker apps directory DOCKER_DIR="$ACTUAL_HOME/docker" if [ "$DRY_RUN" != true ]; then mkdir -p "$DOCKER_DIR" chown "$ACTUAL_USER:$ACTUAL_USER" "$DOCKER_DIR" fi # ---- IMMICH ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ IMMICH - Self-hosted photo & video backup │" echo "│ Like Google Photos but private. Mobile app auto-uploads. │" echo "│ Port: 2283 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Immich? (y/n):" "n" INSTALL_IMMICH if [ "$INSTALL_IMMICH" = "y" ] || [ "$INSTALL_IMMICH" = "Y" ]; then echo "Installing Immich..." IMMICH_DIR="$DOCKER_DIR/immich" # Default photo location DEFAULT_PHOTO_DIR="$HOME_DIR/drives/primary/photos" # Ask about photo storage location echo "" echo "Photo Storage Configuration:" echo " Immich can store photos on your data drive instead of the OS drive." echo " This keeps all photos in one place and makes backups simpler." echo "" echo " Default: $DEFAULT_PHOTO_DIR" echo " (Press Enter to use default, or enter a custom path)" echo "" PHOTO_LOCATION="$DEFAULT_PHOTO_DIR" prompt_text "Photo storage path:" "$DEFAULT_PHOTO_DIR" PHOTO_LOCATION 2>/dev/null || PHOTO_LOCATION="$DEFAULT_PHOTO_DIR" # Expand ~ if used PHOTO_LOCATION="${PHOTO_LOCATION/#\~/$HOME_DIR}" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $IMMICH_DIR" echo "[DRY-RUN] Would store photos at $PHOTO_LOCATION" echo "[DRY-RUN] Would create docker-compose.yml and .env" else mkdir -p "$IMMICH_DIR" 2>/dev/null || true mkdir -p "$PHOTO_LOCATION" 2>/dev/null || true cd "$IMMICH_DIR" 2>/dev/null || cd "$DOCKER_DIR" # Create docker-compose.yml with external library support cat > docker-compose.yml << 'IMMICH_COMPOSE' name: immich services: immich-server: container_name: immich_server image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} volumes: # Main photo storage - new uploads go here - ${UPLOAD_LOCATION}:/usr/src/app/upload # External library - for existing photos (read-only by Immich) - ${EXTERNAL_LIBRARY:-/dev/null}:/usr/src/app/external:ro - /etc/localtime:/etc/localtime:ro env_file: - .env ports: - 2283:2283 depends_on: - redis - database restart: always healthcheck: disable: false immich-machine-learning: container_name: immich_machine_learning image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release} volumes: - model-cache:/cache env_file: - .env restart: always healthcheck: disable: false redis: container_name: immich_redis image: docker.io/valkey/valkey:8-bookworm healthcheck: test: valkey-cli ping || exit 1 restart: always database: container_name: immich_postgres image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_INITDB_ARGS: '--data-checksums' volumes: - ${DB_DATA_LOCATION}:/var/lib/postgresql/data healthcheck: test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(googlechecksum(googlechecksum(SPLIT_PART(googlechecksum::text, ''x'', 2)::bit(32)::int)), 0) FROM pg_catalog.pg_class c JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace WHERE n.nspname = ''public'' AND c.relkind = ''r''')"; echo "googlechecksum: $$Chksum"; exit 0 interval: 5m start_interval: 30s start_period: 5m command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"] restart: always volumes: model-cache: IMMICH_COMPOSE # Generate random password DB_PASS=$(openssl rand -base64 32 | tr -dc 'a-zA-Z0-9' | head -c 32) # Create .env file cat > .env << IMMICH_ENV # ============================================================ # IMMICH CONFIGURATION # ============================================================ # # STORAGE TEMPLATE (configure in Immich web UI): # Admin → Settings → Storage Template → Enable # Template: {{y}}/{{MM}}/{{filename}} # This organizes new uploads into: photos/2026/01/filename.jpg # # EXISTING PHOTOS: # If you have existing photos, add them as an External Library: # Admin → External Libraries → Create Library → Path: /usr/src/app/external # Then scan the library to import them (read-only, no duplicates) # # ============================================================ # Photo storage location (new uploads from phone/web) UPLOAD_LOCATION=$PHOTO_LOCATION # External library for existing photos (optional) # Set this to your existing photo folder to make them visible in Immich # They remain read-only - Immich won't modify or move them EXTERNAL_LIBRARY=$PHOTO_LOCATION # Database location (keep on fast storage) DB_DATA_LOCATION=./postgres IMMICH_VERSION=release DB_PASSWORD=$DB_PASS DB_USERNAME=postgres DB_DATABASE_NAME=immich TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") IMMICH_ENV chown -R "$ACTUAL_USER:$ACTUAL_USER" "$IMMICH_DIR" 2>/dev/null || true chown -R "$ACTUAL_USER:$ACTUAL_USER" "$PHOTO_LOCATION" 2>/dev/null || true echo "" echo "✓ Immich configured at $IMMICH_DIR" echo " Photos stored at: $PHOTO_LOCATION" echo "" echo " Start with: cd $IMMICH_DIR && docker compose up -d" echo " Access at: http://localhost:2283" echo "" echo " IMPORTANT - After first login, configure storage template:" echo " 1. Go to Admin → Settings → Storage Template" echo " 2. Enable storage template" echo " 3. Set template to: {{y}}/{{MM}}/{{filename}}" echo " 4. New uploads will be organized as: 2026/01/photo.jpg" echo "" echo " For existing photos in $PHOTO_LOCATION:" echo " 1. Go to Admin → External Libraries → Create Library" echo " 2. Set import path to: /usr/src/app/external" echo " 3. Scan library to import (photos stay in place, not duplicated)" echo "" fi fi # ---- AUDIOBOOKSHELF ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ AUDIOBOOKSHELF - Audiobook & podcast server │" echo "│ Stream audiobooks with progress sync across devices. │" echo "│ Port: 13378 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Audiobookshelf? (y/n):" "n" INSTALL_AUDIOBOOKSHELF if [ "$INSTALL_AUDIOBOOKSHELF" = "y" ] || [ "$INSTALL_AUDIOBOOKSHELF" = "Y" ]; then echo "Installing Audiobookshelf..." ABS_DIR="$DOCKER_DIR/audiobookshelf" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $ABS_DIR" else mkdir -p "$ABS_DIR" cd "$ABS_DIR" prompt_text "Path to audiobooks folder [default: $ACTUAL_HOME/drives/primary/audiobooks]:" "$ACTUAL_HOME/drives/primary/audiobooks" AUDIOBOOKS_PATH cat > docker-compose.yml << ABS_COMPOSE name: audiobookshelf services: audiobookshelf: image: ghcr.io/advplyr/audiobookshelf:latest container_name: audiobookshelf hostname: audiobookshelf restart: unless-stopped environment: - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") volumes: - ./config:/config - ./metadata:/metadata - ${AUDIOBOOKS_PATH}:/audiobooks - ${PODCASTS_PATH:-./podcasts}:/podcasts ports: - "13378:80" ABS_COMPOSE cat > .env << ABS_ENV AUDIOBOOKS_PATH=$AUDIOBOOKS_PATH PODCASTS_PATH=./podcasts ABS_ENV mkdir -p config metadata podcasts chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ABS_DIR" echo "" echo "✓ Audiobookshelf configured at $ABS_DIR" echo " Start with: cd $ABS_DIR && docker compose up -d" echo " Access at: http://localhost:13378" echo "" fi fi # ---- EMBY ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ EMBY - Media server for movies, TV, music │" echo "│ Stream your media library to any device. │" echo "│ Port: 8096 (web), 8920 (https) │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Emby? (y/n):" "n" INSTALL_EMBY if [ "$INSTALL_EMBY" = "y" ] || [ "$INSTALL_EMBY" = "Y" ]; then echo "Installing Emby..." EMBY_DIR="$DOCKER_DIR/emby" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $EMBY_DIR" else mkdir -p "$EMBY_DIR" cd "$EMBY_DIR" prompt_text "Path to media folder [default: $ACTUAL_HOME/drives/primary/media]:" "$ACTUAL_HOME/drives/primary/media" MEDIA_PATH cat > docker-compose.yml << EMBY_COMPOSE name: emby services: emby: image: emby/embyserver:latest container_name: emby hostname: emby restart: unless-stopped environment: - UID=$(id -u "$ACTUAL_USER") - GID=$(id -g "$ACTUAL_USER") - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") volumes: - ./config:/config - ${MEDIA_PATH}:/media ports: - "8096:8096" - "8920:8920" # Uncomment for hardware transcoding (Intel/AMD) # devices: # - /dev/dri:/dev/dri EMBY_COMPOSE cat > .env << EMBY_ENV MEDIA_PATH=$MEDIA_PATH EMBY_ENV mkdir -p config chown -R "$ACTUAL_USER:$ACTUAL_USER" "$EMBY_DIR" echo "" echo "✓ Emby configured at $EMBY_DIR" echo " Start with: cd $EMBY_DIR && docker compose up -d" echo " Access at: http://localhost:8096" echo "" fi fi # ---- A.R.M. (Automatic Ripping Machine) ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ A.R.M. - Automatic Ripping Machine │" echo "│ Automatically rip DVDs, Blu-rays, and CDs. │" echo "│ Port: 8080 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install A.R.M.? (y/n):" "n" INSTALL_ARM if [ "$INSTALL_ARM" = "y" ] || [ "$INSTALL_ARM" = "Y" ]; then echo "Installing A.R.M...." ARM_DIR="$DOCKER_DIR/arm" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $ARM_DIR" else mkdir -p "$ARM_DIR" cd "$ARM_DIR" prompt_text "Path for ripped media output [default: $ACTUAL_HOME/drives/primary/ripped]:" "$ACTUAL_HOME/drives/primary/ripped" ARM_OUTPUT # Detect optical drives echo "" echo "Detecting optical drives..." OPTICAL_DRIVES=$(ls /dev/sr* 2>/dev/null || echo "") if [ -n "$OPTICAL_DRIVES" ]; then echo "Found: $OPTICAL_DRIVES" else echo "No optical drives detected. You can add them later." OPTICAL_DRIVES="/dev/sr0" fi cat > docker-compose.yml << ARM_COMPOSE name: arm services: automatic-ripping-machine: image: automaticrippingmachine/automatic-ripping-machine:latest container_name: arm hostname: arm restart: unless-stopped environment: - ARM_UID=$(id -u "$ACTUAL_USER") - ARM_GID=$(id -g "$ACTUAL_USER") - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") volumes: - ./config:/etc/arm/config - ./logs:/home/arm/logs - ${ARM_OUTPUT}/movies:/home/arm/media/completed - ${ARM_OUTPUT}/music:/home/arm/music ports: - "8080:8080" devices: - /dev/sr0:/dev/sr0 # Add more drives as needed: # - /dev/sr1:/dev/sr1 privileged: true ARM_COMPOSE cat > .env << ARM_ENV ARM_OUTPUT=$ARM_OUTPUT ARM_ENV mkdir -p config logs mkdir -p "$ARM_OUTPUT/movies" "$ARM_OUTPUT/music" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ARM_DIR" echo "" echo "✓ A.R.M. configured at $ARM_DIR" echo " Start with: cd $ARM_DIR && docker compose up -d" echo " Access at: http://localhost:8080" echo " Note: Edit docker-compose.yml to add more optical drives" echo "" fi fi # ---- FILEBROWSER ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ FILEBROWSER - Web-based file manager │" echo "│ Browse, upload, download files via web interface. │" echo "│ Port: 8085 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Filebrowser? (y/n):" "n" INSTALL_FILEBROWSER if [ "$INSTALL_FILEBROWSER" = "y" ] || [ "$INSTALL_FILEBROWSER" = "Y" ]; then echo "Installing Filebrowser..." FB_DIR="$DOCKER_DIR/filebrowser" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $FB_DIR" else mkdir -p "$FB_DIR" cd "$FB_DIR" prompt_text "Path to browse [default: $ACTUAL_HOME/drives/primary]:" "$ACTUAL_HOME/drives/primary" FB_PATH cat > docker-compose.yml << FB_COMPOSE name: filebrowser services: filebrowser: image: filebrowser/filebrowser:s6 container_name: filebrowser hostname: filebrowser restart: unless-stopped environment: - PUID=$(id -u "$ACTUAL_USER") - PGID=$(id -g "$ACTUAL_USER") - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") volumes: - ${FB_PATH}:/srv - ./database/filebrowser.db:/database/filebrowser.db - ./config/settings.json:/config/settings.json ports: - "8085:80" FB_COMPOSE cat > .env << FB_ENV FB_PATH=$FB_PATH FB_ENV mkdir -p database config touch database/filebrowser.db cat > config/settings.json << 'FB_SETTINGS' { "port": 80, "baseURL": "", "address": "", "log": "stdout", "database": "/database/filebrowser.db", "root": "/srv" } FB_SETTINGS chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FB_DIR" echo "" echo "✓ Filebrowser configured at $FB_DIR" echo " Start with: cd $FB_DIR && docker compose up -d" echo " Access at: http://localhost:8085" echo " Default login: admin / admin (change immediately!)" echo "" fi fi # ---- MAGIC MIRROR ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ MAGIC MIRROR - Smart mirror / dashboard display │" echo "│ Modular smart mirror platform. Run up to 3 instances. │" echo "│ Ports: 8081, 8082, 8083 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Magic Mirror? (y/n):" "n" INSTALL_MAGICMIRROR if [ "$INSTALL_MAGICMIRROR" = "y" ] || [ "$INSTALL_MAGICMIRROR" = "Y" ]; then echo "" prompt_text "How many Magic Mirror instances? [1-3, default: 1]:" "1" MM_COUNT MM_COUNT=${MM_COUNT:-1} if [ "$MM_COUNT" -gt 3 ]; then MM_COUNT=3; fi if [ "$MM_COUNT" -lt 1 ]; then MM_COUNT=1; fi echo "Installing $MM_COUNT Magic Mirror instance(s)..." for i in $(seq 1 $MM_COUNT); do MM_PORT=$((8080 + i)) MM_DIR="$DOCKER_DIR/magicmirror-$MM_PORT" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $MM_DIR (port $MM_PORT)" else mkdir -p "$MM_DIR" cd "$MM_DIR" cat > docker-compose.yml << MM_COMPOSE name: mm-$MM_PORT services: magicmirror: image: karsten13/magicmirror:latest container_name: magicmirror-$MM_PORT hostname: magicmirror-$MM_PORT restart: unless-stopped environment: - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") volumes: - ./config:/opt/magic_mirror/config - ./modules:/opt/magic_mirror/modules - ./css:/opt/magic_mirror/css ports: - "$MM_PORT:8080" MM_COMPOSE mkdir -p config modules css # Create basic config.js cat > config/config.js << 'MM_CONFIG' let config = { address: "0.0.0.0", port: 8080, ipWhitelist: [], language: "en", timeFormat: 12, units: "imperial", modules: [ { module: "alert", }, { module: "clock", position: "top_left" }, { module: "calendar", header: "Calendar", position: "top_left", config: { calendars: [ { symbol: "calendar-check", url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics" } ] } }, { module: "weather", position: "top_right", config: { weatherProvider: "openmeteo", type: "current", lat: 40.7128, lon: -74.0060 } }, { module: "weather", position: "top_right", header: "Weather Forecast", config: { weatherProvider: "openmeteo", type: "forecast", lat: 40.7128, lon: -74.0060 } }, { module: "newsfeed", position: "bottom_bar", config: { feeds: [ { title: "BBC", url: "https://feeds.bbci.co.uk/news/rss.xml" } ], showSourceTitle: true, showPublishDate: true, broadcastNewsFeeds: true, broadcastNewsUpdates: true } }, ] }; /*************** DO NOT EDIT THE LINE BELOW ***************/ if (typeof module !== "undefined") {module.exports = config;} MM_CONFIG chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MM_DIR" echo " ✓ Magic Mirror #$i configured at $MM_DIR (port $MM_PORT)" fi done if [ "$DRY_RUN" != true ]; then echo "" echo " Start with: cd ~/docker/magicmirror-808X && docker compose up -d" echo " Access at: http://localhost:808X" echo " Edit config: ~/docker/magicmirror-808X/config/config.js" echo "" fi fi # ---- LYRION MUSIC SERVER ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ LYRION MUSIC SERVER (LMS) - Music streaming server │" echo "│ Stream music to Squeezebox devices, apps, and Chromecast. │" echo "│ Port: 9000 (web), 9090 (CLI), 3483 (players) │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Lyrion Music Server? (y/n):" "n" INSTALL_LMS if [ "$INSTALL_LMS" = "y" ] || [ "$INSTALL_LMS" = "Y" ]; then echo "Installing Lyrion Music Server..." LMS_DIR="$DOCKER_DIR/lyrion" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $LMS_DIR" else mkdir -p "$LMS_DIR" cd "$LMS_DIR" prompt_text "Path to music folder [default: $ACTUAL_HOME/drives/primary/music]:" "$ACTUAL_HOME/drives/primary/music" MUSIC_PATH cat > docker-compose.yml << LMS_COMPOSE name: lyrion services: lyrion: image: lmscommunity/lyrionmusicserver:stable container_name: lyrion hostname: lyrion restart: unless-stopped network_mode: host environment: - HTTP_PORT=9000 - PUID=$(id -u "$ACTUAL_USER") - PGID=$(id -g "$ACTUAL_USER") - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") volumes: - ./config:/config:rw - ${MUSIC_PATH}:/music:ro - ./playlists:/playlists:rw - /etc/localtime:/etc/localtime:ro LMS_COMPOSE cat > .env << LMS_ENV MUSIC_PATH=$MUSIC_PATH LMS_ENV mkdir -p config playlists chown -R "$ACTUAL_USER:$ACTUAL_USER" "$LMS_DIR" echo "" echo "✓ Lyrion Music Server configured at $LMS_DIR" echo " Start with: cd $LMS_DIR && docker compose up -d" echo " Access at: http://localhost:9000" echo " Note: Uses host networking for Chromecast support" echo "" fi fi # ---- MEALIE ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ MEALIE - Recipe manager & meal planner │" echo "│ Save recipes, plan meals, generate shopping lists. │" echo "│ Port: 9925 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Mealie? (y/n):" "n" INSTALL_MEALIE if [ "$INSTALL_MEALIE" = "y" ] || [ "$INSTALL_MEALIE" = "Y" ]; then echo "Installing Mealie..." MEALIE_DIR="$DOCKER_DIR/mealie" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $MEALIE_DIR" else mkdir -p "$MEALIE_DIR" cd "$MEALIE_DIR" cat > docker-compose.yml << MEALIE_COMPOSE name: mealie services: mealie: image: ghcr.io/mealie-recipes/mealie:latest container_name: mealie hostname: mealie restart: unless-stopped environment: - PUID=$(id -u "$ACTUAL_USER") - PGID=$(id -g "$ACTUAL_USER") - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") - ALLOW_SIGNUP=true - MAX_WORKERS=1 - WEB_CONCURRENCY=1 - BASE_URL=http://localhost:9925 volumes: - ./data:/app/data ports: - "9925:9000" MEALIE_COMPOSE mkdir -p data chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MEALIE_DIR" echo "" echo "✓ Mealie configured at $MEALIE_DIR" echo " Start with: cd $MEALIE_DIR && docker compose up -d" echo " Access at: http://localhost:9925" echo " Default: changeme@email.com / MyPassword" echo "" fi fi # ---- MINECRAFT SERVER ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ MINECRAFT SERVER - Game server with RAM limit │" echo "│ Fabric server with configurable memory allocation. │" echo "│ Port: 25565 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Minecraft Server? (y/n):" "n" INSTALL_MINECRAFT if [ "$INSTALL_MINECRAFT" = "y" ] || [ "$INSTALL_MINECRAFT" = "Y" ]; then echo "Installing Minecraft Server..." MC_DIR="$DOCKER_DIR/minecraft" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $MC_DIR" else mkdir -p "$MC_DIR" cd "$MC_DIR" echo "" prompt_text "Maximum RAM for Minecraft (e.g., 2G, 4G) [default: 2G]:" "2G" MC_RAM MC_RAM=${MC_RAM:-2G} cat > docker-compose.yml << MC_COMPOSE name: minecraft services: minecraft: image: itzg/minecraft-server:latest container_name: minecraft hostname: minecraft restart: unless-stopped tty: true stdin_open: true environment: - EULA=TRUE - TYPE=FABRIC - VERSION=LATEST - MEMORY=${MC_RAM} - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") - OPS= - MOTD=A Minecraft Server - DIFFICULTY=normal - MODE=survival volumes: - ./data:/data ports: - "25565:25565" deploy: resources: limits: memory: ${MC_RAM} MC_COMPOSE cat > .env << MC_ENV MC_RAM=$MC_RAM MC_ENV mkdir -p data chown -R "$ACTUAL_USER:$ACTUAL_USER" "$MC_DIR" echo "" echo "✓ Minecraft Server configured at $MC_DIR" echo " Start with: cd $MC_DIR && docker compose up -d" echo " Connect: localhost:25565" echo " RAM limit: $MC_RAM" echo " Console: docker attach minecraft (Ctrl+P, Ctrl+Q to detach)" echo "" fi fi # ---- LINUX-TO-SYNC (Private Repo) ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ LINUX-TO-SYNC - Private sync repository │" echo "│ Clone and set up your private linux-to-sync repository. │" echo "└─────────────────────────────────────────────────────────────────┘" echo "" echo "Note: This requires access to github.com/outis1one/linux-to-sync" echo "" echo "To grant access, you need ONE of these:" echo " 1. SSH key already added to your GitHub account" echo " 2. GitHub Personal Access Token (PAT)" echo " 3. GitHub CLI (gh) authenticated" echo "" prompt_yn "Set up linux-to-sync? (y/n):" "n" INSTALL_LINUXTOSYNC if [ "$INSTALL_LINUXTOSYNC" = "y" ] || [ "$INSTALL_LINUXTOSYNC" = "Y" ]; then SYNC_DIR="$DOCKER_DIR/linux-to-sync" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would clone linux-to-sync to $SYNC_DIR" else echo "" echo "Choose authentication method:" echo " [1] SSH (if you have SSH key added to GitHub)" echo " [2] HTTPS with token (requires Personal Access Token)" echo "" prompt_text "Enter 1 or 2 [default: 1]:" "1" AUTH_METHOD if [ "$AUTH_METHOD" = "2" ]; then echo "" echo "Create a Personal Access Token at:" echo " https://github.com/settings/tokens/new" echo " - Select 'repo' scope for full repository access" echo "" prompt_text "Enter your GitHub Personal Access Token:" "" GH_TOKEN if [ -n "$GH_TOKEN" ]; then git clone "https://$GH_TOKEN@github.com/outis1one/linux-to-sync.git" "$SYNC_DIR" 2>/dev/null if [ $? -eq 0 ]; then # Remove token from remote URL for security cd "$SYNC_DIR" git remote set-url origin "https://github.com/outis1one/linux-to-sync.git" chown -R "$ACTUAL_USER:$ACTUAL_USER" "$SYNC_DIR" echo "" echo "✓ linux-to-sync cloned to $SYNC_DIR" echo " Note: You'll need to enter token again for push/pull" echo " Or set up: git config credential.helper store" else echo "✗ Clone failed. Check your token and try again." fi else echo "No token provided. Skipping." fi else echo "" echo "Attempting SSH clone..." echo "(Make sure your SSH key is added to GitHub)" echo "" git clone git@github.com:outis1one/linux-to-sync.git "$SYNC_DIR" 2>/dev/null if [ $? -eq 0 ]; then chown -R "$ACTUAL_USER:$ACTUAL_USER" "$SYNC_DIR" echo "" echo "✓ linux-to-sync cloned to $SYNC_DIR" else echo "" echo "✗ SSH clone failed." echo "" echo "To add your SSH key to GitHub:" echo " 1. Copy your public key: cat ~/.ssh/id_rsa.pub" echo " 2. Go to: https://github.com/settings/keys" echo " 3. Click 'New SSH key' and paste your key" echo "" echo "Then retry this script or manually clone:" echo " git clone git@github.com:outis1one/linux-to-sync.git ~/docker/linux-to-sync" fi fi fi fi # ---- JELLYFIN (Alternative to Emby) ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ JELLYFIN - Free media server (alternative to Emby) │" echo "│ Stream movies, TV, music. No premium features locked. │" echo "│ Port: 8096 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Jellyfin? (y/n):" "n" INSTALL_JELLYFIN if [ "$INSTALL_JELLYFIN" = "y" ] || [ "$INSTALL_JELLYFIN" = "Y" ]; then echo "Installing Jellyfin..." JELLYFIN_DIR="$DOCKER_DIR/jellyfin" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $JELLYFIN_DIR" else mkdir -p "$JELLYFIN_DIR" cd "$JELLYFIN_DIR" prompt_text "Path to media folder [default: $ACTUAL_HOME/drives/primary/media]:" "$ACTUAL_HOME/drives/primary/media" MEDIA_PATH # Get render group ID for hardware acceleration RENDER_GID=$(getent group render | cut -d: -f3 2>/dev/null || echo "989") cat > docker-compose.yml << JELLYFIN_COMPOSE name: jellyfin services: jellyfin: image: jellyfin/jellyfin:latest container_name: jellyfin hostname: jellyfin restart: unless-stopped environment: - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") devices: - /dev/dri/renderD128:/dev/dri/renderD128 group_add: - "$RENDER_GID" volumes: - ./config:/config - ./cache:/cache - \${MEDIA_PATH}:/media:ro ports: - "8096:8096" - "1900:1900/udp" - "7359:7359/udp" JELLYFIN_COMPOSE cat > .env << JELLYFIN_ENV MEDIA_PATH=$MEDIA_PATH JELLYFIN_ENV mkdir -p config cache chown -R "$ACTUAL_USER:$ACTUAL_USER" "$JELLYFIN_DIR" echo "" echo "✓ Jellyfin configured at $JELLYFIN_DIR" echo " Start with: cd $JELLYFIN_DIR && docker compose up -d" echo " Access at: http://localhost:8096" echo " Note: Hardware acceleration enabled (Intel GPU)" echo "" fi fi # ---- FRIGATE NVR ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ FRIGATE - AI-powered NVR for security cameras │" echo "│ Object detection, recordings, 24/7 monitoring. │" echo "│ Port: 5000 (web), 8554 (RTSP), 8555 (WebRTC) │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Frigate? (y/n):" "n" INSTALL_FRIGATE if [ "$INSTALL_FRIGATE" = "y" ] || [ "$INSTALL_FRIGATE" = "Y" ]; then echo "Installing Frigate..." FRIGATE_DIR="$DOCKER_DIR/frigate" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $FRIGATE_DIR" else # STEP 1: Create directory and install docker-compose mkdir -p "$FRIGATE_DIR" 2>/dev/null || true cd "$FRIGATE_DIR" 2>/dev/null || cd "$DOCKER_DIR" # Default path FRIGATE_PATH="$ACTUAL_HOME/drives/primary/frigate" cat > docker-compose.yml << FRIGATE_COMPOSE name: frigate services: frigate: image: ghcr.io/blakeblackshear/frigate:stable container_name: frigate hostname: frigate restart: unless-stopped privileged: true shm_size: "256mb" environment: - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") devices: - /dev/dri/renderD128:/dev/dri/renderD128 volumes: - ./config:/config - \${FRIGATE_MEDIA}:/media/frigate - type: tmpfs target: /tmp/cache tmpfs: size: 1000000000 ports: - "5000:5000" - "8554:8554" - "8555:8555/tcp" - "8555:8555/udp" FRIGATE_COMPOSE echo "✓ Installed docker-compose.yml" # STEP 2: Try to configure (uses defaults if fails) prompt_text "Path for recordings [$FRIGATE_PATH]:" "$FRIGATE_PATH" FRIGATE_PATH 2>/dev/null || FRIGATE_PATH="$ACTUAL_HOME/drives/primary/frigate" cat > .env << FRIGATE_ENV FRIGATE_MEDIA=$FRIGATE_PATH FRIGATE_ENV mkdir -p config 2>/dev/null || true mkdir -p "$FRIGATE_PATH" 2>/dev/null || echo " ⚠ Could not create $FRIGATE_PATH - create manually" # Create template config cat > config/config.yml << 'FRIGATE_CONFIG' # Frigate Configuration # Docs: https://docs.frigate.video # # ⚠️ YOU MUST EDIT THIS FILE to add your cameras! mqtt: enabled: false cameras: # EXAMPLE - Replace with your camera: # front_door: # ffmpeg: # inputs: # - path: rtsp://user:pass@192.168.1.100:554/stream # roles: [detect, record] # detect: # width: 1280 # height: 720 # fps: 5 detectors: default: type: cpu record: enabled: true retain: days: 7 mode: motion snapshots: enabled: true retain: default: 7 FRIGATE_CONFIG chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FRIGATE_DIR" 2>/dev/null || true echo "" echo "✓ Frigate installed at $FRIGATE_DIR" echo " Start: cd $FRIGATE_DIR && docker compose up -d" echo " Access: http://localhost:5000" echo "" echo " ⚠️ REQUIRED: Edit config/config.yml to add your cameras!" echo " Docs: https://docs.frigate.video" echo "" fi fi # ---- CADDY REVERSE PROXY ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ CADDY - Automatic HTTPS reverse proxy │" echo "│ Route domains to containers with automatic SSL certificates. │" echo "│ Ports: 80, 443 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Caddy reverse proxy? (y/n):" "n" INSTALL_CADDY if [ "$INSTALL_CADDY" = "y" ] || [ "$INSTALL_CADDY" = "Y" ]; then echo "Installing Caddy..." CADDY_DIR="$DOCKER_DIR/caddy" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $CADDY_DIR" else # STEP 1: Create directory and install docker-compose mkdir -p "$CADDY_DIR" 2>/dev/null || true cd "$CADDY_DIR" 2>/dev/null || cd "$DOCKER_DIR" cat > docker-compose.yml << 'CADDY_COMPOSE' name: caddy services: caddy: image: caddy:latest container_name: caddy hostname: caddy restart: unless-stopped env_file: .env ports: - "80:80" - "443:443" - "443:443/udp" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ./config:/config - ./data:/data - ./site:/srv networks: default: name: caddy_net external: true CADDY_COMPOSE echo "✓ Installed docker-compose.yml" # STEP 2: Try configuration (uses defaults if fails) CADDY_DOMAIN="" prompt_text "Your domain (e.g., example.com) [blank for local]:" "" CADDY_DOMAIN 2>/dev/null || CADDY_DOMAIN="" cat > .env << CADDY_ENV MY_DOMAIN=${CADDY_DOMAIN:-localhost} TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") CADDY_ENV # Create Docker network for Caddy (ignore if exists) docker network create caddy_net 2>/dev/null || true # Create comprehensive Caddyfile with all services cat > Caddyfile << 'CADDY_FILE' # Caddy reverse proxy configuration # Edit MY_DOMAIN in .env file, then uncomment services below # # To use: containers must be on 'caddy_net' network # Add to each container's docker-compose.yml: # networks: # default: # name: caddy_net # external: true # ============================================================================ # GLOBAL OPTIONS # ============================================================================ { # Uncomment for local-only (no domain/SSL): # auto_https off } # ============================================================================ # MEDIA SERVERS # ============================================================================ # Immich (photo backup) # immich.{$MY_DOMAIN} { # reverse_proxy immich_server:2283 # } # Jellyfin (media server) # jellyfin.{$MY_DOMAIN} { # reverse_proxy jellyfin:8096 # } # Emby (media server) # emby.{$MY_DOMAIN} { # reverse_proxy emby:8096 # } # Audiobookshelf # audiobooks.{$MY_DOMAIN} { # reverse_proxy audiobookshelf:80 # } # Lyrion Music Server # music.{$MY_DOMAIN} { # reverse_proxy lms:9000 # } # ============================================================================ # HOME AUTOMATION & MONITORING # ============================================================================ # Frigate NVR # frigate.{$MY_DOMAIN} { # reverse_proxy frigate:5000 # } # Uptime Kuma # status.{$MY_DOMAIN} { # reverse_proxy uptime-kuma:3001 # } # Magic Mirror # mirror.{$MY_DOMAIN} { # reverse_proxy magicmirror:8080 # } # Traccar GPS # gps.{$MY_DOMAIN} { # reverse_proxy traccar:8082 # } # FindMyDevice # fmd.{$MY_DOMAIN} { # reverse_proxy fmd:8080 # } # ============================================================================ # UTILITIES # ============================================================================ # Filebrowser # files.{$MY_DOMAIN} { # reverse_proxy filebrowser:80 # } # Mealie (recipes) # recipes.{$MY_DOMAIN} { # reverse_proxy mealie:9000 # } # ntfy (notifications) # ntfy.{$MY_DOMAIN} { # reverse_proxy ntfy:80 # } # Portainer # docker.{$MY_DOMAIN} { # reverse_proxy portainer:9000 # } # Kopia backup UI # backup.{$MY_DOMAIN} { # reverse_proxy kopia:51515 # } # ============================================================================ # TESTING / CATCH-ALL # ============================================================================ # Local testing (always responds) :80 { respond "Caddy is running! Edit Caddyfile to enable your services." } CADDY_FILE mkdir -p config data site 2>/dev/null || true chown -R "$ACTUAL_USER:$ACTUAL_USER" "$CADDY_DIR" 2>/dev/null || true echo "" echo "✓ Caddy installed at $CADDY_DIR" echo " Start: cd $CADDY_DIR && docker compose up -d" echo " Domain: ${CADDY_DOMAIN:-localhost} (edit .env)" echo " Config: $CADDY_DIR/Caddyfile (uncomment services)" echo "" echo " ⚠️ Containers must be on 'caddy_net' network" echo " Docs: https://caddyserver.com/docs/" echo "" fi fi # ---- DDCLIENT DYNAMIC DNS ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ DDCLIENT - Dynamic DNS updater │" echo "│ Keep your domain pointing to your home IP. │" echo "│ Supports: Cloudflare, DuckDNS, No-IP, and more. │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install ddclient? (y/n):" "n" INSTALL_DDCLIENT if [ "$INSTALL_DDCLIENT" = "y" ] || [ "$INSTALL_DDCLIENT" = "Y" ]; then echo "Installing ddclient..." DDCLIENT_DIR="$DOCKER_DIR/ddclient" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $DDCLIENT_DIR" else # STEP 1: Install docker-compose mkdir -p "$DDCLIENT_DIR" 2>/dev/null || true cd "$DDCLIENT_DIR" 2>/dev/null || cd "$DOCKER_DIR" cat > docker-compose.yml << 'DDCLIENT_COMPOSE' name: ddclient services: ddclient: image: lscr.io/linuxserver/ddclient:latest container_name: ddclient hostname: ddclient restart: unless-stopped environment: - PUID=1000 - PGID=1000 - TZ=${TZ} volumes: - ./config:/config DDCLIENT_COMPOSE echo "✓ Installed docker-compose.yml" cat > .env << DDCLIENT_ENV TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") DDCLIENT_ENV mkdir -p config 2>/dev/null || true # Create template config cat > config/ddclient.conf << 'DDCLIENT_CONF' # ddclient configuration # ⚠️ YOU MUST EDIT THIS FILE! # Uncomment and edit for your DNS provider daemon=300 syslog=yes pid=/var/run/ddclient/ddclient.pid ssl=yes # Cloudflare example: # use=web, web=cloudflare # protocol=cloudflare # zone=example.com # login=token # password=your-api-token # example.com # DuckDNS example: # use=web # protocol=duckdns # password=your-duckdns-token # yourdomain.duckdns.org DDCLIENT_CONF chown -R "$ACTUAL_USER:$ACTUAL_USER" "$DDCLIENT_DIR" 2>/dev/null || true echo "" echo "✓ ddclient installed at $DDCLIENT_DIR" echo " Start: cd $DDCLIENT_DIR && docker compose up -d" echo "" echo " ⚠️ REQUIRED: Edit config/ddclient.conf first!" echo " Docs: https://ddclient.net/" echo "" fi fi # ---- NTFY NOTIFICATIONS ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ NTFY - Push notifications server │" echo "│ Send notifications from scripts to your phone. │" echo "│ Port: 8090 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install ntfy? (y/n):" "n" INSTALL_NTFY if [ "$INSTALL_NTFY" = "y" ] || [ "$INSTALL_NTFY" = "Y" ]; then echo "Installing ntfy..." NTFY_DIR="$DOCKER_DIR/ntfy" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $NTFY_DIR" else mkdir -p "$NTFY_DIR" cd "$NTFY_DIR" cat > docker-compose.yml << 'NTFY_COMPOSE' name: ntfy services: ntfy: image: binwiederhier/ntfy:latest container_name: ntfy hostname: ntfy restart: unless-stopped command: serve environment: - TZ=${TZ} volumes: - ./cache:/var/cache/ntfy - ./config:/etc/ntfy ports: - "8090:80" NTFY_COMPOSE cat > .env << NTFY_ENV TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") NTFY_ENV mkdir -p cache config chown -R "$ACTUAL_USER:$ACTUAL_USER" "$NTFY_DIR" echo "" echo "✓ ntfy configured at $NTFY_DIR" echo " Start with: cd $NTFY_DIR && docker compose up -d" echo " Access at: http://localhost:8090" echo "" echo " Send notification: curl -d \"Hello!\" localhost:8090/mytopic" echo " Subscribe on phone: ntfy app → Add subscription → localhost:8090/mytopic" echo "" fi fi # ---- UPTIME KUMA ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ UPTIME KUMA - Service monitoring dashboard │" echo "│ Monitor websites, servers, Docker containers. │" echo "│ Port: 3001 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Uptime Kuma? (y/n):" "n" INSTALL_UPTIMEKUMA if [ "$INSTALL_UPTIMEKUMA" = "y" ] || [ "$INSTALL_UPTIMEKUMA" = "Y" ]; then echo "Installing Uptime Kuma..." UPTIME_DIR="$DOCKER_DIR/uptime-kuma" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $UPTIME_DIR" else mkdir -p "$UPTIME_DIR" cd "$UPTIME_DIR" cat > docker-compose.yml << 'UPTIME_COMPOSE' name: uptime-kuma services: uptime-kuma: image: louislam/uptime-kuma:1 container_name: uptime-kuma hostname: uptime-kuma restart: unless-stopped volumes: - ./data:/app/data - /var/run/docker.sock:/var/run/docker.sock:ro ports: - "3001:3001" UPTIME_COMPOSE mkdir -p data chown -R "$ACTUAL_USER:$ACTUAL_USER" "$UPTIME_DIR" echo "" echo "✓ Uptime Kuma configured at $UPTIME_DIR" echo " Start with: cd $UPTIME_DIR && docker compose up -d" echo " Access at: http://localhost:3001" echo "" fi fi # ---- WG-EASY (WireGuard with Web UI) ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ WG-EASY - WireGuard VPN with web management │" echo "│ Easy WireGuard setup with QR codes for clients. │" echo "│ Port: 51821 (web), 51820 (VPN) │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install wg-easy? (y/n):" "n" INSTALL_WGEASY if [ "$INSTALL_WGEASY" = "y" ] || [ "$INSTALL_WGEASY" = "Y" ]; then echo "Installing wg-easy..." WGEASY_DIR="$DOCKER_DIR/wg-easy" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $WGEASY_DIR" else mkdir -p "$WGEASY_DIR" cd "$WGEASY_DIR" # Get public IP or hostname PUBLIC_IP=$(curl -s ifconfig.me 2>/dev/null || echo "your-public-ip") WG_PASSWORD=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) prompt_text "Public IP or hostname for VPN [default: $PUBLIC_IP]:" "$PUBLIC_IP" WG_HOST cat > docker-compose.yml << WGEASY_COMPOSE name: wg-easy services: wg-easy: image: ghcr.io/wg-easy/wg-easy:latest container_name: wg-easy hostname: wg-easy restart: unless-stopped cap_add: - NET_ADMIN - SYS_MODULE sysctls: - net.ipv4.ip_forward=1 - net.ipv4.conf.all.src_valid_mark=1 environment: - WG_HOST=\${WG_HOST} - PASSWORD=\${WG_PASSWORD} - WG_DEFAULT_DNS=1.1.1.1 volumes: - ./config:/etc/wireguard ports: - "51820:51820/udp" - "51821:51821/tcp" WGEASY_COMPOSE cat > .env << WGEASY_ENV WG_HOST=$WG_HOST WG_PASSWORD=$WG_PASSWORD WGEASY_ENV mkdir -p config chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WGEASY_DIR" echo "" echo "✓ wg-easy configured at $WGEASY_DIR" echo " Start with: cd $WGEASY_DIR && docker compose up -d" echo " Web UI: http://localhost:51821" echo " Password: $WG_PASSWORD (saved in .env)" echo " VPN Port: 51820/udp (forward this in your router)" echo "" fi fi # ---- TRACCAR GPS TRACKING ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ TRACCAR - GPS tracking server │" echo "│ Track phones, vehicles, assets with OwnTracks/Traccar apps. │" echo "│ Port: 8082 (web), 5055 (OsmAnd), 5000+ (devices) │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Traccar? (y/n):" "n" INSTALL_TRACCAR if [ "$INSTALL_TRACCAR" = "y" ] || [ "$INSTALL_TRACCAR" = "Y" ]; then echo "Installing Traccar..." TRACCAR_DIR="$DOCKER_DIR/traccar" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $TRACCAR_DIR" else mkdir -p "$TRACCAR_DIR" cd "$TRACCAR_DIR" cat > docker-compose.yml << 'TRACCAR_COMPOSE' name: traccar services: traccar: image: traccar/traccar:latest container_name: traccar hostname: traccar restart: unless-stopped volumes: - ./logs:/opt/traccar/logs:rw - ./data:/opt/traccar/data:rw - ./config/traccar.xml:/opt/traccar/conf/traccar.xml:ro ports: - "8082:8082" - "5000-5150:5000-5150" - "5000-5150:5000-5150/udp" TRACCAR_COMPOSE mkdir -p logs data config # Create basic traccar.xml config cat > config/traccar.xml << 'TRACCAR_XML' ./conf/default.xml org.h2.Driver jdbc:h2:/opt/traccar/data/database sa TRACCAR_XML chown -R "$ACTUAL_USER:$ACTUAL_USER" "$TRACCAR_DIR" echo "" echo "✓ Traccar configured at $TRACCAR_DIR" echo " Start with: cd $TRACCAR_DIR && docker compose up -d" echo " Access at: http://localhost:8082" echo " Default: admin@admin.com / admin (change immediately!)" echo "" fi fi # ---- PORTAINER ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ PORTAINER - Docker management web UI │" echo "│ Manage containers, images, volumes via browser. │" echo "│ Port: 9443 (https), 9000 (http) │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Portainer? (y/n):" "n" INSTALL_PORTAINER if [ "$INSTALL_PORTAINER" = "y" ] || [ "$INSTALL_PORTAINER" = "Y" ]; then echo "Installing Portainer..." PORTAINER_DIR="$DOCKER_DIR/portainer" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $PORTAINER_DIR" else mkdir -p "$PORTAINER_DIR" cd "$PORTAINER_DIR" cat > docker-compose.yml << 'PORTAINER_COMPOSE' name: portainer services: portainer: image: portainer/portainer-ce:latest container_name: portainer hostname: portainer restart: always volumes: - /var/run/docker.sock:/var/run/docker.sock - ./data:/data ports: - "9000:9000" - "9443:9443" PORTAINER_COMPOSE mkdir -p data chown -R "$ACTUAL_USER:$ACTUAL_USER" "$PORTAINER_DIR" echo "" echo "✓ Portainer configured at $PORTAINER_DIR" echo " Start with: cd $PORTAINER_DIR && docker compose up -d" echo " Access at: https://localhost:9443" echo " Create admin account on first visit" echo "" 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 "┌─────────────────────────────────────────────────────────────────┐" echo "│ FINDMYDEVICE - Self-hosted device tracking │" echo "│ Track and locate Android devices. Alternative to Google Find. │" echo "│ Port: 8084 │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install FindMyDevice server? (y/n):" "n" INSTALL_FMD if [ "$INSTALL_FMD" = "y" ] || [ "$INSTALL_FMD" = "Y" ]; then echo "Installing FindMyDevice..." FMD_DIR="$DOCKER_DIR/fmd" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $FMD_DIR" else mkdir -p "$FMD_DIR" cd "$FMD_DIR" # Generate random admin password FMD_ADMIN_PASS=$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 16) cat > docker-compose.yml << FMD_COMPOSE name: fmd services: fmd: image: nulide/findmydevice container_name: fmd hostname: fmd restart: unless-stopped environment: - FMD_ADMIN_PASSWORD=\${FMD_ADMIN_PASSWORD} volumes: - ./data:/fmd/data ports: - "8084:8080" FMD_COMPOSE cat > .env << FMD_ENV FMD_ADMIN_PASSWORD=$FMD_ADMIN_PASS FMD_ENV mkdir -p data chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FMD_DIR" echo "" echo "✓ FindMyDevice configured at $FMD_DIR" echo " Start with: cd $FMD_DIR && docker compose up -d" echo " Access at: http://localhost:8084" echo " Admin password: $FMD_ADMIN_PASS (saved in .env)" echo "" echo " Mobile app: Install 'FindMyDevice' from F-Droid" echo " Configure app to point to: http://YOUR-SERVER-IP:8084" echo "" fi fi # ---- FRIGATE-NOTIFY ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ FRIGATE-NOTIFY - Push notifications for Frigate events │" echo "│ Get alerts when Frigate detects people, cars, etc. │" echo "│ Sends to: ntfy, Pushover, Discord, Gotify, and more. │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Frigate-Notify? (y/n):" "n" INSTALL_FRIGATE_NOTIFY if [ "$INSTALL_FRIGATE_NOTIFY" = "y" ] || [ "$INSTALL_FRIGATE_NOTIFY" = "Y" ]; then echo "Installing Frigate-Notify..." FN_DIR="$DOCKER_DIR/frigate-notify" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $FN_DIR" else # STEP 1: Create directory and docker-compose (always succeeds) mkdir -p "$FN_DIR" 2>/dev/null || true cd "$FN_DIR" 2>/dev/null || cd "$DOCKER_DIR" cat > docker-compose.yml << 'FN_COMPOSE' name: frigate-notify services: frigate-notify: image: ghcr.io/0x2142/frigate-notify:latest container_name: frigate-notify hostname: frigate-notify restart: unless-stopped volumes: - ./config.yml:/app/config.yml:ro FN_COMPOSE echo "✓ Installed docker-compose.yml" # STEP 2: Try configuration (uses defaults if prompts fail) echo "" echo "Attempting auto-configuration..." # Set smart defaults based on what's installed FRIGATE_URL="http://frigate:5000" NTFY_URL="https://ntfy.sh" NTFY_TOPIC="frigate-alerts" [ -d "$DOCKER_DIR/frigate" ] && echo " ✓ Frigate detected" || echo " ⚠ Frigate not found (using default URL)" [ -d "$DOCKER_DIR/ntfy" ] && { NTFY_URL="http://ntfy:80"; echo " ✓ ntfy detected"; } || echo " ⚠ ntfy not found (using ntfy.sh)" # Try prompts, use defaults if they fail echo "" prompt_text "Frigate URL [$FRIGATE_URL]:" "$FRIGATE_URL" FRIGATE_URL 2>/dev/null || FRIGATE_URL="http://frigate:5000" prompt_text "ntfy server [$NTFY_URL]:" "$NTFY_URL" NTFY_URL 2>/dev/null || NTFY_URL="https://ntfy.sh" prompt_text "ntfy topic [frigate-alerts]:" "frigate-alerts" NTFY_TOPIC 2>/dev/null || NTFY_TOPIC="frigate-alerts" # Create config (template with user values or defaults) cat > config.yml << FN_CONFIG # Frigate-Notify Configuration # Docs: https://frigate-notify.0x2142.com # # ⚠️ YOU MAY NEED TO EDIT THIS FILE! # If notifications don't work, check: # - Frigate server URL is correct # - ntfy server is reachable # - Containers are on same Docker network frigate: server: $FRIGATE_URL webapi: enabled: true interval: 30 alerts: general: send_startup_message: true labels: - person - car # - dog # - package notifiers: - name: ntfy enabled: true provider: ntfy config: server: $NTFY_URL topic: $NTFY_TOPIC FN_CONFIG chown -R "$ACTUAL_USER:$ACTUAL_USER" "$FN_DIR" 2>/dev/null || true echo "" echo "✓ Frigate-Notify installed at $FN_DIR" echo " Start: cd $FN_DIR && docker compose up -d" echo " Config: $FN_DIR/config.yml (edit if needed)" echo " Docs: https://frigate-notify.0x2142.com" echo "" fi fi # ---- WATCHTOWER ---- echo "" echo "┌─────────────────────────────────────────────────────────────────┐" echo "│ WATCHTOWER - Container update monitoring │" echo "│ Monitor containers for updates. NOTIFY ONLY by default. │" echo "│ Why notify-only? Apps like Immich have breaking DB migrations. │" echo "└─────────────────────────────────────────────────────────────────┘" prompt_yn "Install Watchtower? (y/n):" "n" INSTALL_WATCHTOWER if [ "$INSTALL_WATCHTOWER" = "y" ] || [ "$INSTALL_WATCHTOWER" = "Y" ]; then echo "Installing Watchtower..." WT_DIR="$DOCKER_DIR/watchtower" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $WT_DIR" else mkdir -p "$WT_DIR" 2>/dev/null || true cd "$WT_DIR" 2>/dev/null || cd "$DOCKER_DIR" # Ask about mode echo "" echo "Watchtower Mode:" echo " [M] Monitor only - Get notifications about available updates (SAFE)" echo " [A] Auto-update - Automatically pull and restart containers (RISKY)" echo "" echo " ⚠️ Auto-update can break apps like Immich that need DB migrations!" echo " Recommendation: Use monitor mode, update manually when ready." echo "" WT_MODE="M" prompt_text "Mode [M/A]:" "M" WT_MODE 2>/dev/null || WT_MODE="M" WT_MODE=$(echo "$WT_MODE" | tr '[:lower:]' '[:upper:]') if [ "$WT_MODE" = "A" ]; then MONITOR_ONLY="false" echo " Mode: Auto-update (containers will be updated automatically)" else MONITOR_ONLY="true" echo " Mode: Monitor only (you'll be notified of updates)" fi # Check for ntfy NTFY_URL="" if [ -d "$DOCKER_DIR/ntfy" ]; then echo " ✓ ntfy detected - configuring notifications" NTFY_URL="http://ntfy/watchtower" fi cat > docker-compose.yml << WT_COMPOSE name: watchtower services: watchtower: image: containrrr/watchtower:latest container_name: watchtower hostname: watchtower restart: unless-stopped environment: # Check for updates daily at 4 AM - WATCHTOWER_SCHEDULE=0 0 4 * * * # Monitor only - don't auto-update (change to false for auto-update) - WATCHTOWER_MONITOR_ONLY=${MONITOR_ONLY} # Cleanup old images after update - WATCHTOWER_CLEANUP=true # Include stopped containers - WATCHTOWER_INCLUDE_STOPPED=true # Notification URL (ntfy, Discord, Slack, etc.) - WATCHTOWER_NOTIFICATION_URL=${NOTIFICATION_URL:-} # Show debug info - WATCHTOWER_DEBUG=false volumes: - /var/run/docker.sock:/var/run/docker.sock:ro WT_COMPOSE # Create .env cat > .env << WT_ENV # Watchtower Configuration # ========================= # # Monitor-only mode: Watchtower checks for updates but doesn't apply them. # This is SAFER because some apps (Immich, Mealie) have database migrations # that can break if you update without proper procedures. # # To update manually: # cd ~/docker/{app} # docker compose pull # docker compose up -d # Set to "false" to enable auto-updates (RISKY!) MONITOR_ONLY=$MONITOR_ONLY # Notification URL (optional) # Examples: # ntfy: ntfy://ntfy.example.com/watchtower # Discord: discord://token@id # Slack: slack://hook-url # Gotify: gotify://hostname/token # # Full list: https://containrrr.dev/shoutrrr/services/overview/ NOTIFICATION_URL=$NTFY_URL WT_ENV chown -R "$ACTUAL_USER:$ACTUAL_USER" "$WT_DIR" 2>/dev/null || true echo "" echo "✓ Watchtower installed at $WT_DIR" echo " Start: cd $WT_DIR && docker compose up -d" echo " Mode: $([ "$MONITOR_ONLY" = "true" ] && echo "Monitor only" || echo "Auto-update")" echo "" echo " Checks for updates daily at 4 AM." if [ -n "$NTFY_URL" ]; then echo " Notifications: $NTFY_URL" else echo " Configure NOTIFICATION_URL in .env for alerts." fi echo "" echo " To exclude a container from Watchtower:" echo " Add label: com.centurylinklabs.watchtower.enable=false" echo "" fi fi # ============================================================================ # KOPIA BACKUP FOR DOCKER CONTAINERS # ============================================================================ echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DOCKER CONTAINER BACKUP (Kopia)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Backup all Docker containers (configs, databases, app data) to your" echo "backup drives. Essential for disaster recovery - if your OS drive" echo "fails, you can restore everything including:" echo " • Immich memories, facial recognition data" echo " • Emby/Jellyfin metadata, watch history" echo " • Minecraft worlds, mods, permissions" echo " • All app configs, users, and databases" echo "" prompt_yn "Set up Kopia container backup? (y/n):" "n" INSTALL_KOPIA if [ "$INSTALL_KOPIA" = "y" ] || [ "$INSTALL_KOPIA" = "Y" ]; then echo "Installing Kopia backup..." KOPIA_DIR="$DOCKER_DIR/kopia" if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would create $KOPIA_DIR" else mkdir -p "$KOPIA_DIR" cd "$KOPIA_DIR" KOPIA_PASSWORD=$(openssl rand -base64 24 | tr -dc 'a-zA-Z0-9' | head -c 24) echo "" echo "Select backup destination(s):" echo " Backups will be stored in ~/drives/{backup-drive}/kopia-repo/" echo "" # List available backup drives echo "Available mount points in ~/drives/:" ls -1 "$ACTUAL_HOME/drives/" 2>/dev/null | grep -v "^primary$" || echo " (none found - set up drives first)" echo "" prompt_text "Backup drive name [default: backup1]:" "backup1" KOPIA_BACKUP_DRIVE KOPIA_REPO="$ACTUAL_HOME/drives/$KOPIA_BACKUP_DRIVE/kopia-repo" cat > docker-compose.yml << KOPIA_COMPOSE name: kopia services: kopia: image: kopia/kopia:latest container_name: kopia hostname: kopia restart: unless-stopped privileged: true devices: - /dev/fuse:/dev/fuse:rwm environment: - TZ=$(cat /etc/timezone 2>/dev/null || echo "UTC") - KOPIA_PASSWORD=\${KOPIA_PASSWORD} command: > server start --tls-generate-cert --disable-csrf-token-checks --address=0.0.0.0:51515 --server-username=admin --server-password=\${KOPIA_PASSWORD} volumes: - ./config:/app/config - ./cache:/app/cache - ./logs:/app/logs - $DOCKER_DIR:/data/docker:ro - $KOPIA_REPO:/repository - ./tmp:/tmp:shared ports: - "51515:51515" KOPIA_COMPOSE cat > .env << KOPIA_ENV KOPIA_PASSWORD=$KOPIA_PASSWORD KOPIA_ENV mkdir -p config cache logs tmp mkdir -p "$KOPIA_REPO" # Create backup script cat > backup-containers.sh << 'BACKUP_SCRIPT' #!/bin/bash # Backup all Docker containers using Kopia DOCKER_DIR="$HOME/docker" TIMESTAMP=$(date +%Y%m%d-%H%M%S) echo "=== Docker Container Backup: $TIMESTAMP ===" echo "" # Stop containers before backup for consistency (optional) read -p "Stop containers during backup for consistency? (y/n): " STOP_CONTAINERS if [ "$STOP_CONTAINERS" = "y" ]; then echo "Stopping containers..." for dir in "$DOCKER_DIR"/*/; do if [ -f "$dir/docker-compose.yml" ]; then echo " Stopping $(basename $dir)..." (cd "$dir" && docker compose stop) 2>/dev/null fi done fi echo "" echo "Running Kopia backup..." docker exec kopia kopia snapshot create /data/docker --description "Container backup $TIMESTAMP" if [ "$STOP_CONTAINERS" = "y" ]; then echo "" echo "Restarting containers..." for dir in "$DOCKER_DIR"/*/; do if [ -f "$dir/docker-compose.yml" ]; then echo " Starting $(basename $dir)..." (cd "$dir" && docker compose start) 2>/dev/null fi done fi echo "" echo "=== Backup Complete ===" docker exec kopia kopia snapshot list /data/docker --max-results 5 BACKUP_SCRIPT # Create restore script cat > restore-containers.sh << 'RESTORE_SCRIPT' #!/bin/bash # Restore Docker containers from Kopia backup echo "=== Docker Container Restore ===" echo "" echo "Available snapshots:" docker exec kopia kopia snapshot list /data/docker echo "" echo "To restore a specific snapshot:" echo " docker exec kopia kopia restore /tmp/restore" echo " Then copy files from ~/docker/kopia/tmp/restore/ to ~/docker/" echo "" echo "To mount snapshots for browsing:" echo " docker exec kopia kopia mount all /tmp/mnt &" echo " Then browse: ~/docker/kopia/tmp/mnt/" RESTORE_SCRIPT chmod +x backup-containers.sh restore-containers.sh chown -R "$ACTUAL_USER:$ACTUAL_USER" "$KOPIA_DIR" echo "" echo "✓ Kopia backup configured at $KOPIA_DIR" echo " Start Kopia: cd $KOPIA_DIR && docker compose up -d" echo " Web UI: https://localhost:51515" echo " Username: admin" echo " Password: $KOPIA_PASSWORD (saved in .env)" echo "" echo " Repository at: $KOPIA_REPO" echo "" echo " Backup now: cd $KOPIA_DIR && ./backup-containers.sh" echo " Restore: cd $KOPIA_DIR && ./restore-containers.sh" echo "" echo " ⚠️ SAVE YOUR KOPIA PASSWORD! Without it, backups cannot be restored." echo "" fi fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Docker applications configured in: $DOCKER_DIR" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "To start an application:" echo " cd ~/docker/{appname}" echo " docker compose up -d" echo "" echo "To view logs:" echo " docker compose logs -f" echo "" echo "To stop:" echo " docker compose down" echo "" fi # Backup System (Optional) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "LOCAL BACKUP SYSTEM (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Set up rsync backup from primary drive(s) to backup drive(s)." echo "" echo "Why rsync instead of RAID?" echo " • RAID mirrors corruption instantly - rsync gives you time to notice" echo " • RAID requires identical drives - rsync works with any sizes" echo " • RAID is complex to set up/recover - rsync is simple copy" echo " • rsync can run on schedule - RAID is always-on (more wear)" echo " • With rsync, backup drives can be disconnected for safety" echo "" # Check if backup is already configured BACKUP_CONFIGURED=false if [ -f /usr/local/bin/backup-scripts/rsync-backup.sh ]; then BACKUP_CONFIGURED=true echo "Local backup system is already configured." echo "" prompt_yn "Reconfigure local backup system? (y/n):" "n" SETUP_BACKUP else prompt_yn "Set up local backup system? (y/n):" "n" SETUP_BACKUP fi if [ "$SETUP_BACKUP" = "y" ] || [ "$SETUP_BACKUP" = "Y" ]; then echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DRIVE CONFIGURATION" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Configure your drive mount points in ~/drives/" echo "Default names: primary, backup1, backup2, etc." echo "You can customize these (e.g., 'media', 'documents', 'photos-backup')" echo "" # Install rsync if needed if ! is_rsync_installed; then run_cmd apt install -y rsync || echo "Warning: rsync installation failed" fi # Create backup script directory mkdir -p /usr/local/bin/backup-scripts # Ask for primary drive name echo "PRIMARY DRIVE (source for backups):" prompt_text " Mount point name [default: primary]:" "primary" PRIMARY_NAME PRIMARY_NAME="${PRIMARY_NAME:-primary}" # Ask for number of backup drives echo "" echo "BACKUP DRIVES (destinations):" prompt_text " How many backup drives? [1-4, default: 1]:" "1" NUM_BACKUPS NUM_BACKUPS="${NUM_BACKUPS:-1}" # Validate number case $NUM_BACKUPS in 1|2|3|4) ;; *) NUM_BACKUPS=1 ;; esac # Collect backup drive names declare -a BACKUP_NAMES for i in $(seq 1 $NUM_BACKUPS); do prompt_text " Backup drive $i name [default: backup$i]:" "backup$i" "BACKUP_NAME_$i" eval "BACKUP_NAMES[$i]=\${BACKUP_NAME_$i:-backup$i}" done # Create mount point directories echo "" echo "Creating mount point directories..." mkdir -p "$ACTUAL_HOME/drives/$PRIMARY_NAME" for i in $(seq 1 $NUM_BACKUPS); do mkdir -p "$ACTUAL_HOME/drives/${BACKUP_NAMES[$i]}" done chown -R "$ACTUAL_USER:$ACTUAL_USER" "$ACTUAL_HOME/drives" echo "✓ Created mount points:" echo " Primary: $ACTUAL_HOME/drives/$PRIMARY_NAME" for i in $(seq 1 $NUM_BACKUPS); do echo " Backup$i: $ACTUAL_HOME/drives/${BACKUP_NAMES[$i]}" done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DRIVE MOUNTING" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Available block devices:" echo "" lsblk -o NAME,SIZE,TYPE,FSTYPE,MOUNTPOINT,LABEL echo "" prompt_yn "Mount drives now? (y/n):" "n" MOUNT_NOW if [ "$MOUNT_NOW" = "y" ] || [ "$MOUNT_NOW" = "Y" ]; then echo "" echo "Enter device paths (e.g., /dev/sdb1) or leave blank to skip" echo "" read -p "Primary drive ($PRIMARY_NAME) device: " PRIMARY_DEV declare -a BACKUP_DEVS for i in $(seq 1 $NUM_BACKUPS); do read -p "Backup drive ${BACKUP_NAMES[$i]} device: " "BACKUP_DEV_$i" eval "BACKUP_DEVS[$i]=\$BACKUP_DEV_$i" done # Mount primary if [ -n "$PRIMARY_DEV" ] && [ -b "$PRIMARY_DEV" ]; then echo "Mounting $PRIMARY_DEV to $ACTUAL_HOME/drives/$PRIMARY_NAME..." mount "$PRIMARY_DEV" "$ACTUAL_HOME/drives/$PRIMARY_NAME" && echo "✓ $PRIMARY_NAME mounted" || echo "✗ Failed to mount $PRIMARY_NAME" fi # Mount backups for i in $(seq 1 $NUM_BACKUPS); do if [ -n "${BACKUP_DEVS[$i]}" ] && [ -b "${BACKUP_DEVS[$i]}" ]; then echo "Mounting ${BACKUP_DEVS[$i]} to $ACTUAL_HOME/drives/${BACKUP_NAMES[$i]}..." mount "${BACKUP_DEVS[$i]}" "$ACTUAL_HOME/drives/${BACKUP_NAMES[$i]}" && echo "✓ ${BACKUP_NAMES[$i]} mounted" || echo "✗ Failed to mount ${BACKUP_NAMES[$i]}" fi done echo "" echo "Current mounts:" df -h | grep "$ACTUAL_HOME/drives" || echo " (no drives currently mounted)" echo "" prompt_yn "Add to /etc/fstab for auto-mount at boot? (y/n):" "n" ADD_FSTAB if [ "$ADD_FSTAB" = "y" ] || [ "$ADD_FSTAB" = "Y" ]; then echo "" echo "Adding entries to /etc/fstab..." cp /etc/fstab /etc/fstab.backup-$(date +%Y%m%d-%H%M%S) if [ -n "$PRIMARY_DEV" ] && [ -b "$PRIMARY_DEV" ]; then PRIMARY_UUID=$(blkid -s UUID -o value "$PRIMARY_DEV") if [ -n "$PRIMARY_UUID" ]; then echo "UUID=$PRIMARY_UUID $ACTUAL_HOME/drives/$PRIMARY_NAME auto defaults,nofail 0 2" >> /etc/fstab echo "✓ Added $PRIMARY_NAME to fstab" fi fi for i in $(seq 1 $NUM_BACKUPS); do if [ -n "${BACKUP_DEVS[$i]}" ] && [ -b "${BACKUP_DEVS[$i]}" ]; then BACKUP_UUID=$(blkid -s UUID -o value "${BACKUP_DEVS[$i]}") if [ -n "$BACKUP_UUID" ]; then echo "UUID=$BACKUP_UUID $ACTUAL_HOME/drives/${BACKUP_NAMES[$i]} auto defaults,nofail 0 2" >> /etc/fstab echo "✓ Added ${BACKUP_NAMES[$i]} to fstab" fi fi done fi fi # Create rsync backup script echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "CREATING BACKUP SCRIPT" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" # Build backup destinations list BACKUP_DESTS="" for i in $(seq 1 $NUM_BACKUPS); do BACKUP_DESTS="$BACKUP_DESTS \"$ACTUAL_HOME/drives/${BACKUP_NAMES[$i]}\"" done cat > /usr/local/bin/backup-scripts/rsync-backup.sh << BACKUP_SCRIPT #!/bin/bash ################################################################################ # rsync Local Backup Script # Backs up primary drive to all backup drives ################################################################################ PRIMARY="$ACTUAL_HOME/drives/$PRIMARY_NAME" BACKUP_DRIVES=($BACKUP_DESTS) LOG="/var/log/rsync-backup.log" echo "=== rsync Backup Started: \$(date) ===" | tee -a "\$LOG" echo "Source: \$PRIMARY" | tee -a "\$LOG" echo "" | tee -a "\$LOG" if [ ! -d "\$PRIMARY" ] || [ -z "\$(ls -A \$PRIMARY 2>/dev/null)" ]; then echo "ERROR: Primary drive not mounted or empty at \$PRIMARY" | tee -a "\$LOG" exit 1 fi for BACKUP in "\${BACKUP_DRIVES[@]}"; do echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" | tee -a "\$LOG" echo "Backing up to: \$BACKUP" | tee -a "\$LOG" if [ ! -d "\$BACKUP" ]; then echo "⚠️ WARNING: \$BACKUP not mounted, skipping" | tee -a "\$LOG" continue fi # rsync options: # -a = archive mode (preserves permissions, timestamps, etc.) # -v = verbose # -h = human-readable sizes # --delete = remove files from backup that don't exist on primary # --progress = show progress rsync -avh --delete --progress "\$PRIMARY/" "\$BACKUP/" 2>&1 | tee -a "\$LOG" if [ \$? -eq 0 ]; then echo "✓ Backup to \$BACKUP completed" | tee -a "\$LOG" else echo "✗ Backup to \$BACKUP FAILED" | tee -a "\$LOG" fi done echo "" | tee -a "\$LOG" echo "=== Backup Completed: \$(date) ===" | tee -a "\$LOG" BACKUP_SCRIPT chmod +x /usr/local/bin/backup-scripts/rsync-backup.sh # Create systemd service cat > /etc/systemd/system/rsync-backup.service << SERVICE [Unit] Description=rsync Local Backup Service After=network.target [Service] Type=oneshot ExecStart=/usr/local/bin/backup-scripts/rsync-backup.sh User=root SERVICE # Create systemd timer for daily backups at 2 AM cat > /etc/systemd/system/rsync-backup.timer << TIMER [Unit] Description=Daily rsync Backup Timer Requires=rsync-backup.service [Timer] OnCalendar=*-*-* 02:00:00 Persistent=true [Install] WantedBy=timers.target TIMER echo "" echo "✓ Backup script created: /usr/local/bin/backup-scripts/rsync-backup.sh" echo "✓ Systemd service/timer created (disabled by default)" echo "" echo "Quick start:" echo " Test backup: sudo /usr/local/bin/backup-scripts/rsync-backup.sh" echo " Enable daily: sudo systemctl enable --now rsync-backup.timer" echo " View log: tail -f /var/log/rsync-backup.log" else echo "Skipping local backup setup." fi # Cloud Backup with rclone (Optional) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "CLOUD BACKUP (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Set up encrypted cloud backup using rclone." echo "Supports: Google Drive, OneDrive, Dropbox, and 40+ other providers." echo "" echo "Your files are encrypted BEFORE upload - the cloud provider cannot read them." echo "" prompt_yn "Set up encrypted cloud backup? (y/n):" "n" SETUP_CLOUD_BACKUP if [ "$SETUP_CLOUD_BACKUP" = "y" ] || [ "$SETUP_CLOUD_BACKUP" = "Y" ]; then echo "" # Install rclone if needed if ! is_rclone_installed; then echo "Installing rclone..." run_cmd apt install -y rclone || echo "Warning: rclone installation failed" fi if is_rclone_installed; then echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "CHOOSE CLOUD PROVIDER" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo " [1] Google Drive (15GB free)" echo " [2] Microsoft OneDrive (5GB free, 1TB with Microsoft 365)" echo " [3] Other (manual rclone config)" echo "" prompt_text "Select provider [1/2/3]:" "1" CLOUD_PROVIDER echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "RCLONE CONFIGURATION" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" case $CLOUD_PROVIDER in 1) echo "Setting up Google Drive..." echo "" echo "Step 1: Run 'rclone config' to create a Google Drive remote" echo "Step 2: When prompted:" echo " - Choose 'n' for new remote" echo " - Name it 'gdrive'" echo " - Choose 'drive' (Google Drive)" echo " - Leave client_id and client_secret blank" echo " - Choose scope '1' (full access)" echo " - Leave root_folder_id blank" echo " - Leave service_account_file blank" echo " - Choose 'n' for advanced config" echo " - Choose 'y' for auto config (opens browser)" echo " - Choose 'n' for team drive" echo " - Confirm with 'y'" echo "" CLOUD_REMOTE="gdrive" ;; 2) echo "Setting up Microsoft OneDrive..." echo "" echo "Step 1: Run 'rclone config' to create a OneDrive remote" echo "Step 2: When prompted:" echo " - Choose 'n' for new remote" echo " - Name it 'onedrive'" echo " - Choose 'onedrive' (Microsoft OneDrive)" echo " - Leave client_id and client_secret blank" echo " - Choose region (usually 'global')" echo " - Choose 'n' for advanced config" echo " - Choose 'y' for auto config (opens browser)" echo " - Choose 'onedrive' for account type" echo " - Choose your drive from the list (usually option 0)" echo " - Confirm with 'y'" echo "" CLOUD_REMOTE="onedrive" ;; *) echo "Manual configuration selected." echo "Run 'rclone config' to set up your cloud provider." echo "" prompt_text "Enter the remote name you will create:" "cloud" CLOUD_REMOTE ;; esac if [ "$UNATTENDED" != true ]; then echo "Press Enter to launch rclone config..." read sudo -u "$ACTUAL_USER" rclone config else echo "Skipping interactive rclone config (unattended mode)" echo "Run 'rclone config' manually to complete setup" fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "ENCRYPTION SETUP" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Now we'll create an encrypted wrapper around your cloud storage." echo "This encrypts file names AND contents before upload." echo "" echo "⚠️ IMPORTANT: You will set a password. Without this password and" echo " the rclone config file, your files CANNOT be recovered!" echo "" if [ "$UNATTENDED" != true ]; then echo "In rclone config:" echo " - Choose 'n' for new remote" echo " - Name it '${CLOUD_REMOTE}-crypt'" echo " - Choose 'crypt' (Encrypt/Decrypt)" echo " - Remote: '${CLOUD_REMOTE}:backup' (folder on cloud storage)" echo " - Choose 'standard' for filename encryption" echo " - Choose 'true' for directory name encryption" echo " - Choose 'y' to enter your own password" echo " - Enter a STRONG password (you'll need this to decrypt!)" echo " - Choose 'y' for salt password (or 'n' to skip)" echo " - Confirm with 'y'" echo "" echo "Press Enter to continue rclone config..." read sudo -u "$ACTUAL_USER" rclone config fi # Backup the rclone config to local drives echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "BACKUP YOUR RCLONE CONFIG" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "Your rclone config contains your encryption keys." echo "WITHOUT IT, YOUR ENCRYPTED CLOUD FILES CANNOT BE DECRYPTED!" echo "" echo "The config file is at: $ACTUAL_HOME/.config/rclone/rclone.conf" echo "" # Copy config to any mounted backup drives if [ -d "$ACTUAL_HOME/drives" ]; then for drive_dir in "$ACTUAL_HOME/drives"/*/; do if [ -d "$drive_dir" ] && mountpoint -q "$drive_dir" 2>/dev/null; then mkdir -p "${drive_dir}.rclone-config-backup" if [ -f "$ACTUAL_HOME/.config/rclone/rclone.conf" ]; then cp "$ACTUAL_HOME/.config/rclone/rclone.conf" "${drive_dir}.rclone-config-backup/rclone.conf.backup" echo "✓ Config backed up to: ${drive_dir}.rclone-config-backup/" fi fi done fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "SECURE OFF-SITE BACKUP OF CONFIG" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "For disaster recovery, store a copy of rclone.conf OFF-SITE:" echo "" echo "Recommended secure methods:" echo " • Signal (end-to-end encrypted, send to yourself or trusted contact)" echo " • Box.com (better privacy policy than Dropbox)" echo " • Password manager (1Password, Bitwarden, etc.)" echo " • Encrypted USB drive stored at another location" echo "" echo "⚠️ Dropbox: Works but has broader data access policies." echo " Consider encrypting the config file before uploading." echo "" echo "To use on another computer:" echo " 1. Install rclone" echo " 2. Copy rclone.conf to ~/.config/rclone/" echo " 3. Run: rclone ls ${CLOUD_REMOTE}-crypt:" echo "" # Create cloud backup script mkdir -p /usr/local/bin/backup-scripts SOURCE_PATH="${PRIMARY_NAME:-primary}" cat > /usr/local/bin/backup-scripts/cloud-backup.sh << CLOUD_SCRIPT #!/bin/bash ################################################################################ # rclone Encrypted Cloud Backup Script ################################################################################ SOURCE="$ACTUAL_HOME/drives/$SOURCE_PATH" REMOTE="${CLOUD_REMOTE:-gdrive}-crypt" LOG="/var/log/cloud-backup.log" echo "=== Cloud Backup Started: \$(date) ===" | tee -a "\$LOG" echo "Source: \$SOURCE" | tee -a "\$LOG" echo "Destination: \$REMOTE:" | tee -a "\$LOG" echo "" | tee -a "\$LOG" if [ ! -d "\$SOURCE" ] || [ -z "\$(ls -A \$SOURCE 2>/dev/null)" ]; then echo "ERROR: Source not mounted or empty at \$SOURCE" | tee -a "\$LOG" exit 1 fi # Sync to encrypted cloud storage rclone sync "\$SOURCE" "\$REMOTE:" \\ --progress \\ --stats=30s \\ --log-file="\$LOG" \\ --log-level INFO if [ \$? -eq 0 ]; then echo "" | tee -a "\$LOG" echo "✓ Cloud backup completed: \$(date)" | tee -a "\$LOG" else echo "" | tee -a "\$LOG" echo "✗ Cloud backup FAILED: \$(date)" | tee -a "\$LOG" fi CLOUD_SCRIPT chmod +x /usr/local/bin/backup-scripts/cloud-backup.sh chown "$ACTUAL_USER:$ACTUAL_USER" /usr/local/bin/backup-scripts/cloud-backup.sh echo "" echo "✓ Cloud backup script created: /usr/local/bin/backup-scripts/cloud-backup.sh" echo "" echo "Quick start:" echo " Test backup: sudo /usr/local/bin/backup-scripts/cloud-backup.sh" echo " View log: tail -f /var/log/cloud-backup.log" else echo "rclone installation failed. Skipping cloud backup setup." fi else echo "Skipping cloud backup setup." fi # UFW Firewall Configuration (Optional) echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "UFW FIREWALL (Optional)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo "UFW (Uncomplicated Firewall) provides a simple interface for managing" echo "iptables firewall rules." echo "" UFW_ACTIVE=false if is_ufw_installed && ufw status 2>/dev/null | grep -q "Status: active"; then UFW_ACTIVE=true echo "UFW is already enabled." ufw status 2>/dev/null | head -20 echo "" prompt_yn "Reconfigure UFW? (y/n):" "n" CONFIGURE_UFW else if is_ufw_installed; then echo "UFW is installed but not enabled." else echo "UFW is not installed." fi echo "" prompt_yn "Enable and configure UFW firewall? (y/n):" "y" CONFIGURE_UFW fi if [ "$CONFIGURE_UFW" = "y" ] || [ "$CONFIGURE_UFW" = "Y" ]; then echo "" echo "Configuring UFW firewall..." if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would install ufw if needed" echo "[DRY-RUN] Would allow SSH (port 22)" if [ "$INSTALL_SAMBA" = "y" ] || [ "$INSTALL_SAMBA" = "Y" ]; then echo "[DRY-RUN] Would allow Samba" fi echo "[DRY-RUN] Would enable UFW" else # Install UFW if not present if ! is_ufw_installed; then apt install -y ufw || echo "Warning: UFW installation failed" fi if is_ufw_installed; then # Always allow SSH first (before enabling!) ufw allow ssh echo "✓ Allowed SSH (port 22)" # Allow Samba if installed if [ "$INSTALL_SAMBA" = "y" ] || [ "$INSTALL_SAMBA" = "Y" ] || is_samba_installed; then ufw allow samba echo "✓ Allowed Samba" fi # Enable UFW (with --force to avoid prompt) ufw --force enable echo "✓ UFW firewall enabled" echo "" echo "Current UFW status:" ufw status else echo "✗ UFW installation failed, skipping configuration" fi fi else echo "Skipping UFW configuration." fi CONFIGURE_UFW="${CONFIGURE_UFW:-n}" # Full system upgrade echo "" echo "Performing full system upgrade..." run_cmd apt upgrade -y # Clean up echo "" echo "Cleaning up..." run_cmd apt autoremove -y run_cmd apt autoclean echo "" echo "=== Installation Complete! ===" echo "" echo "Installed Software:" echo " ✓ net-tools, ncdu, git, curl, wget, htop, tree, zip/unzip" echo " ✓ OpenSSH Server - SSH remote access" if [ "$INSTALL_FAIL2BAN" = "y" ] || [ "$INSTALL_FAIL2BAN" = "Y" ]; then echo " ✓ fail2ban - SSH brute-force protection" fi if [ "$INSTALL_DOCKER" = "y" ] || [ "$INSTALL_DOCKER" = "Y" ]; then echo " ✓ Docker Engine + Docker Compose" fi if [ "$INSTALL_SAMBA" = "y" ] || [ "$INSTALL_SAMBA" = "Y" ]; then echo " ✓ Samba - File sharing (Primary drive shared)" fi if [ "$INSTALL_NETBIRD" = "y" ] || [ "$INSTALL_NETBIRD" = "Y" ]; then echo " ✓ NetBird - Mesh VPN" fi if [ "$INSTALL_WIREGUARD" = "y" ] || [ "$INSTALL_WIREGUARD" = "Y" ]; then echo " ✓ WireGuard - VPN" fi if [ "$INSTALL_TAILSCALE" = "y" ] || [ "$INSTALL_TAILSCALE" = "Y" ]; then echo " ✓ Tailscale - Mesh VPN" fi if [ "$INSTALL_RUSTDESK" = "y" ] || [ "$INSTALL_RUSTDESK" = "Y" ]; then echo " ✓ RustDesk - Remote desktop" fi if [ "$INSTALL_TEAMVIEWER" = "y" ] || [ "$INSTALL_TEAMVIEWER" = "Y" ]; then echo " ✓ TeamViewer - Remote desktop" fi if [ "$INSTALL_MESHCENTRAL" = "y" ] || [ "$INSTALL_MESHCENTRAL" = "Y" ]; then echo " ✓ MeshCentral Agent - Remote management" fi if [ "$SETUP_BACKUP" = "y" ] || [ "$SETUP_BACKUP" = "Y" ]; then echo " ✓ Local backup configured (rsync)" fi if [ "$SETUP_CLOUD_BACKUP" = "y" ] || [ "$SETUP_CLOUD_BACKUP" = "Y" ]; then echo " ✓ Cloud backup configured (rclone)" fi if [ "$CONFIGURE_UFW" = "y" ] || [ "$CONFIGURE_UFW" = "Y" ]; then echo " ✓ UFW Firewall - enabled" fi echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "SSH AUTHENTICATION SETUP" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo " SSH Key for This Computer:" if [ "$GENERATE_KEY" = "y" ] || [ "$GENERATE_KEY" = "Y" ]; then if [ -f "$ACTUAL_HOME/.ssh/id_rsa.pub" ]; then echo " ✓ 4096-bit RSA key generated" echo " Public key: $ACTUAL_HOME/.ssh/id_rsa.pub" echo " View with: cat ~/.ssh/id_rsa.pub" else echo " ✗ Key generation was attempted but may have failed" fi else echo " Not generated (skipped during install)" echo " Generate later: ssh-keygen -t rsa -b 4096 -C \"your@email.com\"" fi echo "" echo " SSH Server Status:" if [ "$KEYS_IMPORTED" = true ]; then echo " Password authentication: DISABLED (key-only access)" echo " Imported SSH keys: ~/.ssh/authorized_keys" else echo " Password authentication: ENABLED" fi echo "" echo " Traditional SSH Access:" echo " - Uses keys from GitHub/Launchpad (if imported)" echo " - Connect with: ssh user@hostname" if [ "$KEYS_IMPORTED" = true ]; then echo " - Password login: DISABLED (keys required)" else echo " - Password login: ENABLED" fi echo "" echo " NetBird SSH Access (independent of traditional SSH):" echo " - NetBird manages its own keys automatically" echo " - Works even with password auth disabled" echo " - Connect with: netbird ssh " echo " - Enable in NetBird dashboard first" echo "" echo " You can use ANY combination:" echo " ✓ GitHub + Launchpad + NetBird SSH" echo " ✓ GitHub + NetBird SSH" echo " ✓ Launchpad + NetBird SSH" echo " ✓ GitHub + Launchpad (no NetBird)" echo " ✓ Just GitHub or just Launchpad" echo " ✓ Just NetBird SSH" echo " ✓ None (password auth only - if no keys imported)" echo "" if [ "$SETUP_BACKUP" = "y" ] || [ "$SETUP_BACKUP" = "Y" ]; then echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "BACKUP SYSTEM - $BACKUP_TOOL ($BACKUP_MODE mode)" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" echo " Mount points:" echo " $ACTUAL_HOME/drives/primary" echo " $ACTUAL_HOME/drives/backup1" if [ "$BACKUP_MODE" = "split" ]; then echo " $ACTUAL_HOME/drives/backup2" fi echo "" echo " Backup script: /usr/local/bin/backup-scripts/${BACKUP_TOOL}-backup.sh" echo " Log file: /var/log/${BACKUP_TOOL}-backup.log" echo "" if [ "$BACKUP_MODE" = "split" ]; then echo " ⚠️ CONFIGURE BEFORE RUNNING:" echo " Edit the script to set which folders go to which backup drive." echo " sudo nano /usr/local/bin/backup-scripts/${BACKUP_TOOL}-backup.sh" echo "" fi echo " Quick start:" echo " 1. Test (dry-run): sudo ${BACKUP_TOOL}-backup.sh --dry-run # (edit script first)" echo " 2. Run manually: sudo /usr/local/bin/backup-scripts/${BACKUP_TOOL}-backup.sh" echo " 3. Enable auto: sudo systemctl enable ${BACKUP_TOOL}-backup.timer" echo " sudo systemctl start ${BACKUP_TOOL}-backup.timer" echo "" echo " Monitor: tail -f /var/log/${BACKUP_TOOL}-backup.log" fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "OTHER IMPORTANT NOTES" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "" if [ "$INSTALL_SAMBA" = "y" ] || [ "$INSTALL_SAMBA" = "Y" ]; then echo " Samba File Sharing:" if command -v smbd &> /dev/null && grep -q "\[Primary\]" /etc/samba/smb.conf 2>/dev/null; then echo " ✓ Share 'Primary' is accessible at:" echo " Windows: \\\\$(hostname)\\Primary" echo " Mac/Linux: smb://$(hostname)/Primary" echo " • Username: $ACTUAL_USER" echo " • Use the Samba password you just set" echo "" else echo " ✗ Installation may have failed - check 'systemctl status smbd'" echo "" fi fi if [ "$INSTALL_DOCKER" = "y" ] || [ "$INSTALL_DOCKER" = "Y" ]; then echo " Docker: Log out and back in for group membership to take effect" echo "" fi if [ "$INSTALL_NETBIRD" = "y" ] || [ "$INSTALL_NETBIRD" = "Y" ]; then echo " NetBird:" echo " 1. Run 'netbird up' (opens browser for authentication)" echo " 2. View connected peers: netbird status" echo " 3. Configure ACLs in dashboard: https://app.netbird.io" echo "" fi if [ "$INSTALL_RUSTDESK" = "y" ] || [ "$INSTALL_RUSTDESK" = "Y" ]; then echo " RustDesk: Launch from applications menu or run 'rustdesk'" echo "" fi if [ "$INSTALL_FAIL2BAN" = "y" ] || [ "$INSTALL_FAIL2BAN" = "Y" ]; then echo " fail2ban:" echo " • Check status: sudo fail2ban-client status sshd" echo " • View banned IPs: sudo fail2ban-client status sshd" echo " • Unban IP: sudo fail2ban-client set sshd unbanip " echo "" fi if [ "$CONFIGURE_UFW" = "y" ] || [ "$CONFIGURE_UFW" = "Y" ]; then echo " UFW Firewall:" echo " • Check status: sudo ufw status" echo " • Allow port: sudo ufw allow " echo " • Deny port: sudo ufw deny " echo " • Disable: sudo ufw disable" echo "" fi