#!/bin/bash set -e # ============================================ # ProxCenter Community Installation Script # ============================================ # Usage: curl -fsSL https://proxcenter.io/install/community | sudo bash # # Behind an HTTP proxy, keep the proxy variables of your shell with sudo -E: # curl -fsSL https://proxcenter.io/install/community | sudo -E bash # The installer then configures the Docker daemon to use the same proxy. # ============================================ # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' NC='\033[0m' # Configuration INSTALL_DIR="/opt/proxcenter" COMPOSE_URL="https://raw.githubusercontent.com/adminsyspro/proxcenter-ui/main/docker-compose.community.yml" FRONTEND_IMAGE="ghcr.io/adminsyspro/proxcenter-frontend:latest" LOG_FILE="/var/log/proxcenter-install.log" DOCKER_DROPIN_DIR="/etc/systemd/system/docker.service.d" TOTAL_STEPS=4 START_TIME=$(date +%s) # ============================================ # Helper Functions # ============================================ step() { local step_num=$1 local msg=$2 echo "" echo -e "${BOLD}${BLUE}[$step_num/$TOTAL_STEPS]${NC} ${BOLD}$msg${NC}" } log_info() { echo -e " ${DIM}$1${NC}" } log_success() { echo -e " ${GREEN}✓${NC} $1" } log_warning() { echo -e " ${YELLOW}!${NC} $1" } log_error() { echo -e "\n ${RED}✗ $1${NC}" if [ -s "$LOG_FILE" ]; then echo -e " ${DIM}Full log: $LOG_FILE${NC}" fi exit 1 } spinner() { local pid=$1 local msg=${2:-"Please wait"} local chars="⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" local i=0 tput civis 2>/dev/null || true while kill -0 "$pid" 2>/dev/null; do printf "\r ${DIM}%s %s${NC}" "${chars:i++%${#chars}:1}" "$msg" sleep 0.1 done printf "\r\033[K" tput cnorm 2>/dev/null || true } format_duration() { local secs=$1 if [ "$secs" -lt 60 ]; then echo "${secs}s" else echo "$((secs / 60))m $((secs % 60))s" fi } # Output of package managers and Docker is appended to LOG_FILE instead of # being discarded, so a failure can show what actually went wrong. init_log() { if ! { mkdir -p "$(dirname "$LOG_FILE")" && touch "$LOG_FILE"; } 2>/dev/null; then LOG_FILE="/tmp/proxcenter-install.log" touch "$LOG_FILE" fi chmod 600 "$LOG_FILE" 2>/dev/null || true echo "===== ProxCenter Community installer started $(date -Iseconds) =====" >> "$LOG_FILE" } cleanup_on_error() { echo "" echo -e "${RED}${BOLD}Installation failed.${NC}" if [ -s "$LOG_FILE" ]; then echo -e "${DIM} Last lines of $LOG_FILE:${NC}" tail -n 15 "$LOG_FILE" | sed 's/^/ | /' echo "" echo -e "${DIM} - Full log: $LOG_FILE${NC}" fi echo -e "${DIM} - Check Docker: docker compose -f $INSTALL_DIR/docker-compose.yml logs${NC}" echo -e "${DIM} - Re-run this script to retry${NC}" echo "" tput cnorm 2>/dev/null || true exit 1 } trap cleanup_on_error ERR print_banner() { echo "" echo -e "${CYAN}${BOLD}" cat << 'BANNER' ____ ____ _ | _ \ _ __ _____ __/ ___|___ _ __ | |_ ___ _ __ | |_) | '__/ _ \ \/ / | / _ \ '_ \| __/ _ \ '__| | __/| | | (_) > <| |__| __/ | | | || __/ | |_| |_| \___/_/\_\\____\___|_| |_|\__\___|_| BANNER echo -e "${NC}" echo -e " ${GREEN}${BOLD}Community Edition${NC} ${DIM}— Free & Open Source${NC}" echo "" } # ============================================ # Pre-flight Checks # ============================================ preflight_checks() { if [ "$EUID" -ne 0 ]; then log_error "This script must be run as root. Use: sudo bash install-community.sh" fi if [ -f /etc/os-release ]; then . /etc/os-release OS=$ID OS_VERSION_ID=$VERSION_ID elif [ -f /etc/debian_version ]; then OS="debian" elif [ -f /etc/redhat-release ]; then OS="rhel" else log_error "Unsupported operating system" fi case $OS in ubuntu|debian) PKG_MANAGER="apt-get" PKG_UPDATE="apt-get update" PKG_INSTALL="apt-get install -y" ;; centos|rhel|rocky|almalinux|fedora) PKG_MANAGER="dnf" PKG_UPDATE="dnf check-update || true" PKG_INSTALL="dnf install -y" ;; *) log_error "Unsupported OS: $OS" ;; esac log_info "OS: $OS $OS_VERSION_ID" } # ============================================ # Step 1: Install Docker # ============================================ install_docker() { step 1 "Installing Docker" $PKG_UPDATE >> "$LOG_FILE" 2>&1 || true if command -v docker &> /dev/null; then local docker_version docker_version=$(docker --version | grep -oP '\d+\.\d+\.\d+' | head -1) log_success "Docker $docker_version already installed" return fi log_info "Installing dependencies..." $PKG_INSTALL ca-certificates curl openssl >> "$LOG_FILE" 2>&1 case $OS in ubuntu|debian) apt-get remove -y docker docker-engine docker.io containerd runc >> "$LOG_FILE" 2>&1 || true $PKG_INSTALL gnupg lsb-release >> "$LOG_FILE" 2>&1 install -m 0755 -d /etc/apt/keyrings local docker_key docker_key=$(mktemp) curl -fsSL --connect-timeout 15 --max-time 120 "https://download.docker.com/linux/$OS/gpg" -o "$docker_key" 2>> "$LOG_FILE" gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg "$docker_key" 2>> "$LOG_FILE" rm -f "$docker_key" 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/$OS $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get update >> "$LOG_FILE" 2>&1 log_info "Installing Docker packages (this can take a few minutes)..." $PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >> "$LOG_FILE" 2>&1 ;; centos|rhel|rocky|almalinux|fedora) dnf remove -y docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine >> "$LOG_FILE" 2>&1 || true $PKG_INSTALL dnf-plugins-core >> "$LOG_FILE" 2>&1 dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo >> "$LOG_FILE" 2>&1 log_info "Installing Docker packages (this can take a few minutes)..." $PKG_INSTALL docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >> "$LOG_FILE" 2>&1 ;; esac systemctl start docker systemctl enable docker >> "$LOG_FILE" 2>&1 log_success "Docker installed" } # ============================================ # Docker daemon proxy # ============================================ # The Docker daemon performs registry logins and pulls itself and does not # inherit the proxy variables of this shell. Mirror them into a systemd # drop-in when the daemon has no proxy of its own. configure_docker_proxy() { local http_val="${HTTP_PROXY:-${http_proxy:-}}" local https_val="${HTTPS_PROXY:-${https_proxy:-}}" local no_val="${NO_PROXY:-${no_proxy:-}}" if [ -z "$http_val" ] && [ -z "$https_val" ]; then return 0 fi local current current=$(docker info --format '{{.HTTPProxy}}{{.HTTPSProxy}}' 2>/dev/null || true) if [ -n "$current" ]; then log_info "Docker daemon already configured with a proxy" return 0 fi local no_list="localhost,127.0.0.1,::1" if [ -n "$no_val" ]; then no_list="$no_list,$no_val" fi # systemd expands % specifiers in unit files: escape them in the values. local http_unit="${http_val//%/%%}" local https_unit="${https_val//%/%%}" local no_unit="${no_list//%/%%}" log_info "Proxy detected in this shell, configuring the Docker daemon..." mkdir -p "$DOCKER_DROPIN_DIR" { echo "# Written by the ProxCenter installer from the proxy variables of the installing shell." echo "# Edit or delete this file, then run: systemctl daemon-reload && systemctl restart docker" echo "[Service]" if [ -n "$http_unit" ]; then echo "Environment=\"HTTP_PROXY=$http_unit\"" fi if [ -n "$https_unit" ]; then echo "Environment=\"HTTPS_PROXY=$https_unit\"" fi echo "Environment=\"NO_PROXY=$no_unit\"" } > "$DOCKER_DROPIN_DIR/proxcenter-http-proxy.conf" systemctl daemon-reload systemctl restart docker log_success "Docker daemon configured to use proxy ${https_val:-$http_val}" } # ============================================ # Step 2: Configure ProxCenter # ============================================ setup_proxcenter() { step 2 "Configuring ProxCenter" mkdir -p "$INSTALL_DIR" cd "$INSTALL_DIR" if ! curl -fsSL --connect-timeout 15 --max-time 120 "$COMPOSE_URL" -o docker-compose.yml 2>> "$LOG_FILE"; then log_error "Failed to download the compose file from $COMPOSE_URL" fi # Mark volumes as external sed -i '/^volumes:/,$d' docker-compose.yml cat >> docker-compose.yml << 'VOLUMES' volumes: proxcenter_data: external: true postgres_data: external: true VOLUMES log_success "Downloaded compose configuration" # Generate or reuse secrets. If /opt/proxcenter/.env already exists # (re-install on top of a previous deployment), preserve POSTGRES_PASSWORD # so the existing postgres_data volume keeps authenticating, and keep # APP_SECRET / NEXTAUTH_SECRET so encrypted credentials and active # sessions survive the upgrade. existing_env="$INSTALL_DIR/.env" get_existing() { [ -f "$existing_env" ] || return 0 grep "^$1=" "$existing_env" 2>/dev/null | head -1 | cut -d= -f2- } APP_SECRET=$(get_existing APP_SECRET) [ -z "$APP_SECRET" ] && APP_SECRET=$(openssl rand -hex 32) NEXTAUTH_SECRET=$(get_existing NEXTAUTH_SECRET) [ -z "$NEXTAUTH_SECRET" ] && NEXTAUTH_SECRET=$(openssl rand -hex 32) # Postgres password is required by the compose file (it uses the # ${VAR:?msg} form). Postgres pins it on first init of postgres_data, # so on re-install we MUST reuse the old value, otherwise auth fails. POSTGRES_PASSWORD=$(get_existing POSTGRES_PASSWORD) [ -z "$POSTGRES_PASSWORD" ] && POSTGRES_PASSWORD=$(openssl rand -hex 24) SERVER_IP=$(hostname -I | awk '{print $1}' | head -1) if [ -z "$SERVER_IP" ]; then SERVER_IP="localhost" fi cat > "$INSTALL_DIR/.env" << EOF # ProxCenter Community Edition # Generated on $(date -Iseconds) APP_SECRET=$APP_SECRET NEXTAUTH_SECRET=$NEXTAUTH_SECRET NEXTAUTH_URL=http://$SERVER_IP:3000 VERSION=latest # Postgres POSTGRES_PASSWORD=$POSTGRES_PASSWORD EOF chmod 600 "$INSTALL_DIR/.env" log_success "Secrets generated and configuration saved" } # ============================================ # Step 3: Pull & Initialize # ============================================ pull_and_init() { step 3 "Pulling image and initializing" cd "$INSTALL_DIR" # The pull runs in the background so a spinner can show progress; its # output goes to the log. Checking the exit status of "wait" is what # turns a failed pull into an error instead of a false "Image pulled". docker compose pull >> "$LOG_FILE" 2>&1 & local pull_pid=$! spinner $pull_pid "Pulling image (this can take a few minutes)..." if ! wait $pull_pid; then tail -n 5 "$LOG_FILE" | sed 's/^/ | /' log_error "Failed to pull the image" fi log_success "Image pulled" # Create volumes. postgres_data is declared external in the compose # file, so docker compose up won't auto-create it; the absence of # this line is what made fresh installs fail before. docker volume create proxcenter_data >> "$LOG_FILE" 2>&1 || true docker volume create postgres_data >> "$LOG_FILE" 2>&1 || true # Init the frontend data directory so the non-root container user # (uid 1001) can write into it. Postgres init is handled by the # postgres image on first boot, no prep needed here. Prisma migrations # run automatically from the frontend container's entrypoint. docker run --rm --user root --entrypoint "" \ -v proxcenter_data:/app/data \ "$FRONTEND_IMAGE" \ sh -c "mkdir -p /app/data && chown -R 1001:1001 /app/data" >> "$LOG_FILE" 2>&1 log_success "Volumes initialized" } # ============================================ # Step 4: Start Services # ============================================ start_and_wait() { step 4 "Starting ProxCenter" cd "$INSTALL_DIR" docker compose up -d 2>&1 | tee -a "$LOG_FILE" if [ "${PIPESTATUS[0]}" -ne 0 ]; then log_error "docker compose up failed" fi log_success "Container started" log_info "Waiting for service to be ready..." # The probe bypasses any proxy of this shell: localhost must never be # sent to a corporate proxy. ( attempt=1 while [ $attempt -le 60 ]; do if curl --noproxy '*' -s -f --max-time 5 http://localhost:3000/api/health > /dev/null 2>&1; then exit 0 fi sleep 2 attempt=$((attempt + 1)) done exit 1 ) & local wait_pid=$! spinner $wait_pid "Starting frontend..." wait $wait_pid || log_error "Frontend failed to start within 2 minutes. Check: docker compose logs" log_success "Service healthy" } # ============================================ # Final Summary # ============================================ print_summary() { local end_time=$(date +%s) local duration=$((end_time - START_TIME)) SERVER_IP=$(hostname -I | awk '{print $1}' | head -1) echo "" echo -e "${GREEN}${BOLD} ┌─────────────────────────────────────────────┐${NC}" echo -e "${GREEN}${BOLD} │ ProxCenter Community is ready! │${NC}" echo -e "${GREEN}${BOLD} └─────────────────────────────────────────────┘${NC}" echo "" echo -e " ${BOLD}URL${NC} ${CYAN}http://$SERVER_IP:3000${NC}" echo -e " ${BOLD}Install${NC} $INSTALL_DIR" echo -e " ${BOLD}Log${NC} $LOG_FILE" echo -e " ${BOLD}Duration${NC} $(format_duration $duration)" echo "" echo -e " ${DIM}Features: Dashboard, Inventory, VM/CT Management, Backups, Storage${NC}" echo "" echo -e " ${YELLOW}${BOLD}Upgrade to Enterprise for:${NC}" echo -e " ${DIM}DRS, RBAC & LDAP, Advanced Monitoring, AI Insights, and more${NC}" echo -e " ${CYAN}https://proxcenter.io/pricing${NC}" echo "" echo -e " ${DIM}Manage:${NC}" echo -e " ${DIM}cd $INSTALL_DIR && docker compose logs -f${NC} ${DIM}# Logs${NC}" echo -e " ${DIM}cd $INSTALL_DIR && docker compose down${NC} ${DIM}# Stop${NC}" echo -e " ${DIM}cd $INSTALL_DIR && docker compose pull &&${NC}" echo -e " ${DIM}docker compose up -d${NC} ${DIM}# Update${NC}" echo "" } # ============================================ # Main # ============================================ main() { print_banner preflight_checks init_log install_docker configure_docker_proxy setup_proxcenter pull_and_init start_and_wait print_summary } main "$@"