#!/bin/bash # ══════════════════════════════════════════════════════════════════════════════ # enconf Webpanel — One-Liner Installer # # curl -fsSL https://get.enconf.com | sudo bash # # Supported: Debian 12 (Bookworm), Debian 13 (Trixie) — amd64 + arm64 # # On a minimal Debian install neither curl nor sudo are pre-installed: # apt update && apt install -y curl sudo # ══════════════════════════════════════════════════════════════════════════════ set -euo pipefail # ─── Colors + helpers ───────────────────────────────────────────────────────── GRN="\033[0;32m"; RED="\033[0;31m"; YLW="\033[0;33m"; CYN="\033[0;36m" BLD="\033[1m"; DIM="\033[2m"; NC="\033[0m" CHECK="${GRN}✓${NC}" CROSS="${RED}✗${NC}" SPIN="${CYN}⟳${NC}" step() { local label="$1"; shift local spin_chars='⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏' local pid # Start command in background "$@" >/dev/null 2>&1 & pid=$! # Animate spinner with elapsed time local i=0 local start=$SECONDS while kill -0 "$pid" 2>/dev/null; do local c="${spin_chars:i%${#spin_chars}:1}" local elapsed=$(( SECONDS - start )) printf "\r ${CYN}${c}${NC} ${label}... ${DIM}${elapsed}s${NC} " sleep 0.1 i=$((i + 1)) done local total=$(( SECONDS - start )) if wait "$pid"; then printf "\r ${CHECK} ${label} ${DIM}(${total}s)${NC} \n" return 0 else printf "\r ${CROSS} ${label} ${DIM}(${total}s)${NC} \n" return 1 fi } step_log() { # Print step result directly (no command) printf " ${CHECK} $1\n" } fail() { echo -e "\n ${CROSS} ${RED}$*${NC}\n"; exit 1; } # ─── Banner ─────────────────────────────────────────────────────────────────── clear 2>/dev/null || true echo "" echo -e " ${CYN}${BLD}enconf Webpanel${NC} — Installer" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" # ─── Root check ─────────────────────────────────────────────────────────────── [ "$(id -u)" -ne 0 ] && fail "Please run as root: curl -fsSL https://get.enconf.com | sudo bash" # ─── OS check ───────────────────────────────────────────────────────────────── if [ -f /etc/os-release ]; then . /etc/os-release OS_ID="$ID" OS_VERSION="$VERSION_ID" OS_CODENAME="${VERSION_CODENAME:-}" else fail "Operating system not detected." fi case "$OS_ID" in debian) [[ "$OS_VERSION" =~ ^(12|13)$ ]] || fail "Debian $OS_VERSION not supported (only 12/13)." ;; *) fail "$OS_ID not supported. enconf Webpanel only runs on Debian 12 (Bookworm) or 13 (Trixie)." ;; esac ARCH=$(dpkg --print-architecture 2>/dev/null || uname -m) case "$ARCH" in amd64|x86_64) ARCH="amd64" ;; arm64|aarch64) ARCH="arm64" ;; *) fail "Architecture $ARCH not supported (only amd64/arm64)." ;; esac step_log "${OS_ID^} ${OS_VERSION} (${OS_CODENAME}) · ${ARCH}" # ─── 1. Update system ──────────────────────────────────────────────────────── rm -f /etc/apt/sources.list.d/netcell.list /etc/apt/keyrings/netcell-gitea.asc 2>/dev/null || true step "Update system" apt-get update -qq step "Install prerequisites" bash -c "DEBIAN_FRONTEND=noninteractive apt-get install -y -qq -o Dpkg::Options::=--force-confold curl gnupg ca-certificates apt-transport-https" # ─── 2. Set up repository ──────────────────────────────────────────────────── setup_repo() { rm -f /etc/apt/sources.list.d/netcell.list /etc/apt/keyrings/netcell-gitea.asc 2>/dev/null || true mkdir -p /etc/apt/keyrings curl -fsSL "https://git.netcell-it.de/api/packages/projekte/debian/repository.key" \ -o /etc/apt/keyrings/enconf-gitea.asc echo "deb [signed-by=/etc/apt/keyrings/enconf-gitea.asc] https://git.netcell-it.de/api/packages/projekte/debian bookworm main" \ > /etc/apt/sources.list.d/enconf.list apt-get update -qq } step "Set up enconf apt repository" setup_repo # ─── Sury PHP repository ───────────────────────────────────────────────────── # Sury (packages.sury.org) is the canonical upstream for PHP on Debian. # We install it BEFORE the panel so apt pulls PHP from Sury instead of Debian's # own trixie-security php8.4-fpm, whose postinst tries to start the FPM service # before /etc/php/8.4/fpm/php-fpm.conf is deployed — that race leaves PHP-FPM # failed on first install and cascades the panel's configure step into # half-configured state. Sury deploys the conffile before triggering the # service, which avoids the race entirely. # # APT pin priority 600 > default 500 so Sury wins over Debian for every php* # package, including on machines that already had Debian's PHP installed. setup_sury() { curl -fsSL https://packages.sury.org/php/apt.gpg \ -o /etc/apt/keyrings/sury-php.gpg chmod 644 /etc/apt/keyrings/sury-php.gpg echo "deb [signed-by=/etc/apt/keyrings/sury-php.gpg] https://packages.sury.org/php ${OS_CODENAME} main" \ > /etc/apt/sources.list.d/sury-php.list cat > /etc/apt/preferences.d/sury-php </dev/null | grep Candidate | awk '{print $2}') step_log "Version ${BLD}${AVAILABLE}${NC} available" # ─── 3. Unbound DNS resolver ───────────────────────────────────────────────── setup_unbound() { DEBIAN_FRONTEND=noninteractive apt-get install -y -qq unbound # Always use port 5353 — PowerDNS will likely be installed later during # role setup and needs port 53. Unbound on 5353 works for rspamd/RBL # and avoids any conflict regardless of which roles are assigned. UB_PORT=5353 mkdir -p /etc/unbound/unbound.conf.d cat > /etc/unbound/unbound.conf.d/netcell.conf < /etc/systemd/resolved.conf.d/no-stub.conf systemctl restart systemd-resolved 2>/dev/null || true systemctl enable --now unbound } step "Install Unbound DNS resolver" setup_unbound # ─── 4. Install enconf Webpanel ────────────────────────────────────────────── install_panel() { local apt_opts=(-y -qq -o Dpkg::Options::=--force-confold -o Dpkg::Options::=--force-confdef) if ! DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" enconf-webpanel; then # Safety net for any upstream package whose postinst still misbehaves. # DEBIAN_FRONTEND=noninteractive is critical on dpkg --configure too — # without it, roundcube-core.config opens a debconf prompt and the # whole installer stalls waiting on stdin that will never arrive. DEBIAN_FRONTEND=noninteractive dpkg --configure -a >/dev/null 2>&1 || true DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" --fix-broken >/dev/null 2>&1 || true DEBIAN_FRONTEND=noninteractive apt-get install "${apt_opts[@]}" enconf-webpanel fi } step "Install enconf Webpanel" install_panel # ─── 5. Dienste prüfen ─────────────────────────────────────────────────────── # Install Chromium in background (for site thumbnails) — user can already # start the setup wizard while this runs silently. nohup bash -c 'DEBIAN_FRONTEND=noninteractive apt-get install -y -qq chromium >/dev/null 2>&1' >/dev/null 2>&1 & for svc in enconf-api enconf-agent nginx; do if systemctl is-active --quiet "$svc" 2>/dev/null; then step_log "${svc} running" else printf " ${CROSS} ${svc} not active\n" fi done # ─── Result ────────────────────────────────────────────────────────────────── SERVER_IP=$(hostname -I | awk '{print $1}') echo "" echo -e " ${DIM}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo "" echo -e " ${GRN}${BLD}Installation complete!${NC}" echo "" echo -e " ${BLD}▸ Setup wizard:${NC} ${CYN}https://${SERVER_IP}:3443/setup${NC}" echo "" echo -e " ${DIM}Open the URL in your browser to configure the panel.${NC}" echo -e " ${DIM}(SSL certificate is self-signed — confirm the browser warning)${NC}" echo "" # Anonymous install-success beacon — only fires after the full script completed. # Server uses GET /installed to count "real" installs (vs. mere preview-curls). # host-hash is SHA-truncated machine-id so reinstalls on the same host don't double-count. { HOST_HASH=$(printf '%s' "$(cat /etc/machine-id 2>/dev/null || hostname)" | sha256sum 2>/dev/null | cut -c1-16) OS_ID=$(. /etc/os-release 2>/dev/null && echo "${ID:-?}-${VERSION_ID:-?}") curl -fsS --max-time 5 "https://get.enconf.com/installed?h=${HOST_HASH}&os=${OS_ID}" >/dev/null 2>&1 || true } & disown 2>/dev/null || true