feat(docker): add rescue and diagnostic options to betterdesk-docker.sh

Enhanced the betterdesk-docker.sh script with new command-line options for non-interactive rescue operations, including --rescue, --diagnose, and --repair-permissions. Updated the install.sh script to support these options, allowing for safe repairs and diagnostics of Docker deployments.
This commit is contained in:
UNITRONIX
2026-06-18 18:31:24 +02:00
parent 79c8d4d872
commit 23931ed16e
2 changed files with 350 additions and 7 deletions
+289 -7
View File
@@ -19,6 +19,9 @@
# - CDAP (Custom Device API Protocol) support
#
# Usage: ./betterdesk-docker.sh
# ./betterdesk-docker.sh --rescue
# ./betterdesk-docker.sh --diagnose
# ./betterdesk-docker.sh --repair-permissions
#
#===============================================================================
@@ -84,6 +87,8 @@ DIM='\033[2m'
# Logging
LOG_FILE="/tmp/betterdesk_docker_$(date +%Y%m%d_%H%M%S).log"
CLI_ACTION=""
NONINTERACTIVE=false
#===============================================================================
# SELinux / Volume Helper Functions
@@ -125,7 +130,7 @@ log() {
}
print_header() {
clear
clear 2>/dev/null || true
echo -e "${CYAN}"
echo "╔══════════════════════════════════════════════════════════════════╗"
echo "║ ║"
@@ -153,7 +158,53 @@ print_warning() { echo -e "${YELLOW}!${NC} $1"; log "WARNING: $1"; }
print_info() { echo -e "${BLUE}${NC} $1"; log "INFO: $1"; }
print_step() { echo -e "${MAGENTA}${NC} $1"; log "STEP: $1"; }
usage() {
sed -n '4,22p' "$0" | sed 's/^# \?//'
cat <<'EOF'
Non-interactive rescue options:
--rescue Safe repair: permissions, restart containers, health checks
--diagnose Read-only diagnostics without interactive prompts
--repair-permissions Only repair Docker bind/volume permissions and SELinux context
--help Show this help
EOF
}
parse_cli_args() {
while [ $# -gt 0 ]; do
case "$1" in
--rescue)
CLI_ACTION="rescue"
NONINTERACTIVE=true
shift
;;
--diagnose|--diagnostics)
CLI_ACTION="diagnose"
NONINTERACTIVE=true
shift
;;
--repair-permissions)
CLI_ACTION="repair-permissions"
NONINTERACTIVE=true
shift
;;
-h|--help)
usage
exit 0
;;
*)
print_error "Unknown option: $1"
usage
exit 1
;;
esac
done
}
press_enter() {
if [ "$NONINTERACTIVE" = true ]; then
return 0
fi
echo ""
echo -e "${CYAN}Press Enter to continue...${NC}"
read -r
@@ -1497,11 +1548,12 @@ do_repair() {
$'Rebuild images\tRecreate the Docker images'
$'Restart containers\tStop and start the stack'
$'Repair database\tRun database repair routines'
$'Repair permissions\tFix Docker bind/volume ownership and SELinux context'
$'Clean Docker\tPrune images and volumes'
$'Full repair\tDo everything above'
$'Back\tReturn to the main menu'
)
local _menu_returns=( 1 2 3 4 5 0 )
local _menu_returns=( 1 2 3 4 5 6 0 )
menu_choose "Docker Repair" "Choose what to repair"
local repair_choice="$MENU_CHOICE"
@@ -1521,12 +1573,15 @@ do_repair() {
repair_database_docker
;;
4)
repair_docker_permissions
;;
5)
if confirm "Are you sure you want to clean up unused Docker resources?"; then
docker system prune -f
print_success "Docker cleaned"
fi
;;
5)
6)
preserve_compose_database_config
create_compose_file
stop_containers
@@ -1534,6 +1589,7 @@ do_repair() {
build_images
start_containers
repair_database_docker
repair_docker_permissions
print_success "Full repair completed!"
;;
0) return ;;
@@ -1562,6 +1618,207 @@ repair_database_docker() {
fi
}
repair_named_volume_permissions() {
local volume="$1"
[ -n "$volume" ] || return 0
if ! docker volume inspect "$volume" >/dev/null 2>&1; then
return 0
fi
print_info "Repairing Docker volume permissions: $volume"
docker run --rm -v "$volume:/target" alpine:3.20 sh -c '
set -e
mkdir -p /target
chown -R 10001:10001 /target
chmod -R u+rwX,g+rwX /target
' >/dev/null 2>&1 || {
print_warning "Could not repair volume $volume (Docker may need to pull alpine:3.20)"
return 1
}
}
repair_docker_permissions() {
print_step "Repairing Docker data permissions..."
auto_detect_docker_paths
if [ -n "$DATA_DIR" ]; then
create_data_directory "$DATA_DIR" || {
print_error "Failed to repair data directory: $DATA_DIR"
return 1
}
# Containers drop to uid/gid 10001. Keep secrets readable to the
# container user without making them world-readable.
chown -R 10001:10001 "$DATA_DIR" 2>/dev/null || true
chmod 755 "$DATA_DIR" 2>/dev/null || true
for secret in ".api_key" ".admin_credentials" "id_ed25519"; do
if [ -f "$DATA_DIR/$secret" ]; then
chmod 600 "$DATA_DIR/$secret" 2>/dev/null || true
fi
done
if [ -f "$DATA_DIR/id_ed25519.pub" ]; then
chmod 644 "$DATA_DIR/id_ed25519.pub" 2>/dev/null || true
fi
print_success "Host data directory permissions repaired: $DATA_DIR"
fi
local repaired_volumes=0
local volume
while IFS= read -r volume; do
[ -n "$volume" ] || continue
if repair_named_volume_permissions "$volume"; then
repaired_volumes=$((repaired_volumes + 1))
fi
done < <(docker volume ls --format '{{.Name}}' 2>/dev/null | grep -E '(^|_)(betterdesk-data|console-data|console_data|betterdesk.*data)$' || true)
if [ "$repaired_volumes" -gt 0 ]; then
print_success "Repaired $repaired_volumes Docker volume(s)"
else
print_info "No BetterDesk named Docker volumes found to repair"
fi
}
run_compose_safe() {
local action="$1"
local compose_dir compose_name
if [ ! -f "$COMPOSE_FILE" ]; then
print_warning "Compose file not found: $COMPOSE_FILE"
return 1
fi
compose_dir="$(cd "$(dirname "$COMPOSE_FILE")" && pwd)"
compose_name="$(basename "$COMPOSE_FILE")"
(cd "$compose_dir" && $COMPOSE_CMD -f "$compose_name" "$action")
}
restart_containers_safe() {
print_step "Restarting BetterDesk containers safely..."
if [ ! -f "$COMPOSE_FILE" ]; then
print_warning "Compose file not found; trying docker restart for known containers"
docker restart "$SERVER_CONTAINER" "$CONSOLE_CONTAINER" >/dev/null 2>&1 || {
print_warning "Could not restart known containers"
return 1
}
return 0
fi
local compose_dir compose_name
compose_dir="$(cd "$(dirname "$COMPOSE_FILE")" && pwd)"
compose_name="$(basename "$COMPOSE_FILE")"
(cd "$compose_dir" && $COMPOSE_CMD -f "$compose_name" up -d) || {
print_warning "docker compose up -d failed"
return 1
}
}
check_local_http() {
local label="$1"
local url="$2"
printf " %-28s " "$label:"
if curl -sfo /dev/null --connect-timeout 3 "$url" 2>/dev/null; then
echo -e "${GREEN}OK${NC} ($url)"
return 0
fi
echo -e "${YELLOW}UNREACHABLE${NC} ($url)"
return 1
}
print_rescue_diagnostics() {
echo -e "${WHITE}${BOLD}══════════ DOCKER RESCUE DIAGNOSTICS ══════════${NC}"
echo ""
auto_detect_docker_paths
if ! command -v docker >/dev/null 2>&1; then
print_warning "Docker CLI is not installed or not in PATH"
echo " Compose file: $COMPOSE_FILE"
echo " DATA_DIR: $DATA_DIR"
echo " Next step: install Docker, then rerun --rescue"
return 0
fi
if ! check_docker; then
print_warning "Docker daemon is not running or not accessible"
echo " Compose file: $COMPOSE_FILE"
echo " DATA_DIR: $DATA_DIR"
echo " Next step: start Docker, then rerun --rescue"
return 0
fi
detect_installation
print_status
echo ""
echo -e "${WHITE}${BOLD}═══ Compose and volumes ═══${NC}"
echo " Compose file: $COMPOSE_FILE"
if [ -f "$COMPOSE_FILE" ]; then
echo -e " Compose file exists: ${GREEN}yes${NC}"
else
echo -e " Compose file exists: ${YELLOW}no${NC}"
fi
echo " DATA_DIR: $DATA_DIR"
if [ -d "$DATA_DIR" ]; then
echo -e " DATA_DIR writable: $(test -w "$DATA_DIR" && echo "${GREEN}yes${NC}" || echo "${YELLOW}no${NC}")"
fi
echo " Docker volumes:"
docker volume ls --format ' - {{.Name}}' 2>/dev/null | grep -E 'betterdesk|console' || echo " none detected"
echo ""
echo -e "${WHITE}${BOLD}═══ Container logs (last 15 lines) ═══${NC}"
for container in "$SERVER_CONTAINER" "$CONSOLE_CONTAINER"; do
echo ""
echo -e "${CYAN}--- $container ---${NC}"
docker logs --tail 15 "$container" 2>&1 || echo "Container does not exist"
done
echo ""
echo -e "${WHITE}${BOLD}═══ Local health checks ═══${NC}"
check_local_http "Go API 21114" "http://127.0.0.1:21114/api/health" || true
check_local_http "Go API 21121" "http://127.0.0.1:21121/api/health" || true
check_local_http "Web console" "http://127.0.0.1:5000/health" || true
echo ""
echo -e "${WHITE}${BOLD}═══ Port listeners ═══${NC}"
for port in 21114 21115 21116 21117 21121 5000; do
printf " Port %-5s " "$port"
if ss -tulnp 2>/dev/null | grep -q ":${port} "; then
echo -e "${GREEN}LISTENING${NC}"
else
echo -e "${YELLOW}NOT LISTENING${NC}"
fi
done
}
do_safe_rescue() {
print_header
echo -e "${WHITE}${BOLD}══════════ SAFE DOCKER RESCUE ══════════${NC}"
echo ""
check_docker || {
print_error "Docker is not installed or the daemon is not running"
return 1
}
check_docker_compose || {
print_error "Docker Compose is not available"
return 1
}
repair_docker_permissions || true
restart_containers_safe || true
sleep 3
repair_database_docker || true
echo ""
print_rescue_diagnostics
echo ""
print_success "Safe rescue completed. No data or volumes were deleted."
}
#===============================================================================
# Validation Functions
#===============================================================================
@@ -3159,17 +3416,41 @@ main() {
print_warning "Some operations may require root privileges (sudo)"
fi
# Check docker compose
# Check docker compose. Diagnostics can still return useful evidence without
# Compose, but repair actions need it for safe stack restarts.
if ! check_docker_compose; then
print_error "Docker Compose is not available!"
exit 1
if [ "$CLI_ACTION" = "diagnose" ]; then
print_warning "Docker Compose is not available; diagnostics will be partial"
COMPOSE_CMD="docker compose"
else
print_error "Docker Compose is not available!"
exit 1
fi
fi
# Auto-detect paths on startup
echo -e "${CYAN}Detecting installation...${NC}"
auto_detect_docker_paths
echo ""
sleep 1
if [ "$NONINTERACTIVE" != true ]; then
sleep 1
fi
case "$CLI_ACTION" in
rescue)
do_safe_rescue
exit $?
;;
diagnose)
print_rescue_diagnostics
exit $?
;;
repair-permissions)
repair_docker_permissions
exit $?
;;
esac
# Action tokens map 1:1 to the classic case dispatch below, so both the
# arrow-key TUI and the numeric fallback share the exact same handlers.
@@ -3234,4 +3515,5 @@ main() {
}
# Run
parse_cli_args "$@"
main "$@"
+61
View File
@@ -18,6 +18,9 @@
# --admin-password PASS Set admin password (Docker: ADMIN_PASSWORD env)
# --skip-docker-install Do not install Docker when missing
# --skip-firewall Skip UFW/firewalld/iptables configuration
# --rescue Safe repair: permissions, restart, health checks
# --diagnose Read-only diagnostics for Docker deployments
# --repair-permissions Only repair Docker data/volume permissions
# --purge With --uninstall: also remove Docker volumes
# --uninstall Remove BetterDesk Docker installation
# --help Show usage
@@ -43,6 +46,8 @@ SKIP_DOCKER_INSTALL=false
SKIP_FIREWALL=false
DO_UNINSTALL=false
DO_PURGE=false
DO_RESCUE=false
RESCUE_ACTION="rescue"
# Docker quick-start ports (docker-compose.quick.yml)
DOCKER_PORTS="21114 21115 21116 21117 21118 21119 5000"
@@ -86,6 +91,9 @@ while [ $# -gt 0 ]; do
--admin-password) ADMIN_PASSWORD="$2"; shift 2 ;;
--skip-docker-install) SKIP_DOCKER_INSTALL=true; shift ;;
--skip-firewall) SKIP_FIREWALL=true; shift ;;
--rescue) DO_RESCUE=true; RESCUE_ACTION="rescue"; shift ;;
--diagnose|--diagnostics) DO_RESCUE=true; RESCUE_ACTION="diagnose"; shift ;;
--repair-permissions) DO_RESCUE=true; RESCUE_ACTION="repair-permissions"; shift ;;
--purge) DO_PURGE=true; shift ;;
--uninstall) DO_UNINSTALL=true; shift ;;
-h|--help) usage ;;
@@ -400,6 +408,36 @@ EOF
print_docker_summary "$relay"
}
rescue_docker_mode() {
local rescue_dir="${INSTALL_DIR}/rescue"
local rescue_script="${rescue_dir}/betterdesk-docker.sh"
local rescue_url="${BETTERDESK_RAW_BASE}/betterdesk-docker.sh"
local rescue_flag
case "$RESCUE_ACTION" in
diagnose) rescue_flag="--diagnose" ;;
repair-permissions) rescue_flag="--repair-permissions" ;;
rescue|*) rescue_flag="--rescue" ;;
esac
log "Preparing BetterDesk Docker rescue toolkit..."
require_command curl
if [ "$RESCUE_ACTION" != "diagnose" ]; then
check_docker || die "Docker is not available or the daemon is not running"
check_docker_compose || die "Docker Compose plugin is not available"
fi
mkdir -p "$rescue_dir"
fetch_url_to_file "$rescue_url" "$rescue_script.tmp" 4096 524288
install -m 0755 "$rescue_script.tmp" "$rescue_script"
rm -f "$rescue_script.tmp"
ok "Rescue script ready: $rescue_script"
log "Running Docker rescue action: $RESCUE_ACTION"
DATA_DIR="${DATA_DIR:-}" COMPOSE_FILE="${COMPOSE_FILE:-}" \
"$rescue_script" "$rescue_flag"
}
uninstall_docker_mode() {
local compose_dir="${INSTALL_DIR}/docker"
local compose_file="${compose_dir}/docker-compose.yml"
@@ -456,6 +494,20 @@ install_native_mode() {
ok "Native installation finished. See ${repo_dir} for logs and credentials."
}
rescue_native_mode() {
local repo_dir="${INSTALL_DIR}/source"
warn "Native rescue is handled by betterdesk.sh on the installed host."
if [ -x "$repo_dir/betterdesk.sh" ]; then
echo "Run the interactive native repair toolkit with:"
echo " sudo ${repo_dir}/betterdesk.sh"
echo "Then choose: Repair Installation or Diagnostics."
return 0
fi
die "Native checkout not found at ${repo_dir}. Re-run without --rescue to install, or use --docker for Docker rescue."
}
# ── Main ──────────────────────────────────────────────────────────────────────
main() {
@@ -470,6 +522,15 @@ main() {
require_root
if [ "$DO_RESCUE" = true ]; then
case "$INSTALL_MODE" in
docker) rescue_docker_mode ;;
native) rescue_native_mode ;;
*) die "Unknown install mode: $INSTALL_MODE" ;;
esac
exit 0
fi
case "$INSTALL_MODE" in
docker) install_docker_mode ;;
native) install_native_mode ;;