#!/usr/bin/env bash # ============================================================================ # DashCaddy Linux Installer v1.0 # # Zero-effort install: # curl -fsSL https://get.dashcaddy.net | bash # # Zero-typing install (local mode, instant access): # curl -fsSL https://get.dashcaddy.net | bash -s -- quick # # With a domain (only thing you type is the domain): # curl -fsSL https://get.dashcaddy.net | bash -s -- --domain my.example.com # # Designed for accessibility — all choices are single keystrokes. # ============================================================================ set -euo pipefail # ---- Constants ------------------------------------------------------------- readonly DASHCADDY_VERSION="1.1.0" readonly DASHCADDY_DOWNLOAD="https://get.dashcaddy.net/release/latest.tar.gz" readonly DASHCADDY_REPO="" # Set to a git URL to clone instead of downloading readonly INSTALL_DIR="/etc/dashcaddy" readonly DOCKER_DATA="/opt/dockerdata" readonly SITES_DIR="${INSTALL_DIR}/sites" readonly API_DIR="${SITES_DIR}/dashcaddy-api" readonly DASHBOARD_DIR="${SITES_DIR}/status" readonly CONTAINER_NAME="dashcaddy-api" readonly CADDY_ADMIN_PORT=2019 # ---- Tunables (overridable via flags) -------------------------------------- API_PORT=3001 LOCAL_PORT=8080 # ---- Runtime state --------------------------------------------------------- DOMAIN_MODE="" # public | custom-tld | local DOMAIN="" EMAIL="" TLD="" CA_NAME="DashCaddy Local CA" SOURCE_PATH="" GIT_BRANCH="main" SKIP_DOCKER=false SKIP_CADDY=false UNINSTALL=false KEEP_CONFIG=false AUTO_YES=false QUICK=false DISTRO="" DISTRO_FAMILY="" # debian | rhel | arch PUBLIC_IP="" LAN_IP="" STEP=0 TOTAL_STEPS=7 # ---- Colors ---------------------------------------------------------------- if [[ -t 1 ]]; then RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[0;33m' BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m' DIM='\033[2m'; NC='\033[0m' else RED=''; GREEN=''; YELLOW=''; BLUE=''; CYAN=''; BOLD=''; DIM=''; NC='' fi # ============================================================================ # Output Helpers # ============================================================================ log() { echo -e "${CYAN}[DashCaddy]${NC} $*"; } ok() { echo -e "${GREEN} ✓${NC} $*"; } warn() { echo -e "${YELLOW} !${NC} $*"; } err() { echo -e "${RED} ✗${NC} $*" >&2; } fatal() { err "$*"; echo -e "${DIM} If this keeps failing, see: https://dashcaddy.net/docs/troubleshoot${NC}" >&2; exit 1; } step() { STEP=$((STEP + 1)) echo "" echo -e "${BOLD}${BLUE} [${STEP}/${TOTAL_STEPS}] $*${NC}" echo -e "${BLUE} ─────────────────────────────────────────${NC}" } # Simple progress: run command in background, show dots progress() { local msg="$1"; shift printf "${CYAN} ▸${NC} %s " "$msg" # Run the command, capture output for error reporting local logfile="/tmp/dashcaddy-install-$$.log" if "$@" >"$logfile" 2>&1; then echo -e "${GREEN}done${NC}" rm -f "$logfile" return 0 else echo -e "${RED}failed${NC}" echo -e "${DIM} Last output:${NC}" tail -5 "$logfile" 2>/dev/null | sed 's/^/ /' rm -f "$logfile" return 1 fi } elapsed() { local diff=$(( $(date +%s) - $1 )) if (( diff < 60 )); then echo "${diff}s" else printf '%dm%02ds' $((diff / 60)) $((diff % 60)) fi } # ============================================================================ # System Detection # ============================================================================ detect_system() { # --- Root check --- if [[ $EUID -ne 0 ]]; then fatal "Must run as root. Try: sudo bash install.sh" fi # --- OS detection --- if [[ ! -f /etc/os-release ]]; then fatal "Cannot detect OS — /etc/os-release not found." fi source /etc/os-release case "${ID,,}" in ubuntu|debian|pop|linuxmint|elementary|zorin|raspbian) DISTRO="${ID}"; DISTRO_FAMILY="debian" ;; fedora|rhel|centos|rocky|almalinux|ol|amzn) DISTRO="${ID}"; DISTRO_FAMILY="rhel" ;; arch|manjaro|endeavouros) DISTRO="${ID}"; DISTRO_FAMILY="arch" ;; *) DISTRO="${ID}"; DISTRO_FAMILY="debian" warn "Unknown distro '${ID}' — using Debian-style commands" ;; esac ok "OS: ${PRETTY_NAME:-$DISTRO}" # --- Architecture --- local arch arch=$(uname -m) case "$arch" in x86_64|amd64|aarch64|arm64) ok "Arch: ${arch}" ;; *) warn "Arch '${arch}' may not be fully supported" ;; esac # --- Network --- LAN_IP=$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{print $7; exit}' || hostname -I 2>/dev/null | awk '{print $1}' || echo "unknown") PUBLIC_IP=$(curl -fsSL --max-time 3 https://api.ipify.org 2>/dev/null || curl -fsSL --max-time 3 https://ifconfig.me 2>/dev/null || echo "unknown") ok "LAN IP: ${LAN_IP}" ok "Public IP: ${PUBLIC_IP}" # --- Existing install? --- if [[ -f "${INSTALL_DIR}/config.json" ]]; then warn "Existing DashCaddy installation detected" warn "Re-running will upgrade in place (configs preserved)" fi } # ============================================================================ # Interactive Setup — minimal typing, number keys only # ============================================================================ interactive_setup() { # Skip if mode already set via flags if [[ -n "$DOMAIN_MODE" ]]; then return; fi # Detect environment to offer smart defaults local is_vps=false if [[ "$PUBLIC_IP" != "unknown" && "$PUBLIC_IP" != "$LAN_IP" ]] || \ [[ "$LAN_IP" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|192\.168\.) ]]; then : # Private IP — probably homelab or local else is_vps=true fi echo "" echo -e "${BOLD} How will you access DashCaddy?${NC}" echo "" if $is_vps; then echo -e " ${BOLD}1${NC}) Public domain ${DIM}— Let's Encrypt TLS (recommended for VPS)${NC}" echo -e " ${BOLD}2${NC}) Quick start ${DIM}— http://${PUBLIC_IP}:${LOCAL_PORT} right now, add domain later${NC}" echo -e " ${BOLD}3${NC}) Custom TLD ${DIM}— Internal CA for homelab (e.g., .home, .lab)${NC}" else echo -e " ${BOLD}1${NC}) Quick start ${DIM}— http://${LAN_IP}:${LOCAL_PORT} right now${NC}" echo -e " ${BOLD}2${NC}) Custom TLD ${DIM}— Internal CA for homelab (e.g., .home, .lab)${NC}" echo -e " ${BOLD}3${NC}) Public domain ${DIM}— Let's Encrypt TLS${NC}" fi echo "" local choice read -rp "$(echo -e " ${YELLOW}Press 1, 2, or 3:${NC} ")" -n1 choice echo "" if $is_vps; then case "$choice" in 1) setup_public_domain ;; 2) DOMAIN_MODE="local" ;; 3) setup_custom_tld ;; *) DOMAIN_MODE="local"; warn "Defaulting to Quick Start" ;; esac else case "$choice" in 1) DOMAIN_MODE="local" ;; 2) setup_custom_tld ;; 3) setup_public_domain ;; *) DOMAIN_MODE="local"; warn "Defaulting to Quick Start" ;; esac fi } setup_public_domain() { DOMAIN_MODE="public" echo "" read -rp "$(echo -e " ${YELLOW}Domain name:${NC} ")" DOMAIN if [[ -z "$DOMAIN" ]]; then fatal "Domain is required for public mode." fi # Auto-check if domain points to this server local resolved resolved=$(dig +short "$DOMAIN" 2>/dev/null | head -1) if [[ -n "$resolved" && "$resolved" == "$PUBLIC_IP" ]]; then ok "${DOMAIN} correctly points to ${PUBLIC_IP}" elif [[ -n "$resolved" ]]; then warn "${DOMAIN} resolves to ${resolved}, but this server is ${PUBLIC_IP}" echo -e " ${DIM}Let's Encrypt will fail if DNS doesn't point here.${NC}" echo "" read -rp "$(echo -e " ${YELLOW}Continue anyway? [y/N]:${NC} ")" -n1 cont echo "" [[ "$cont" =~ ^[Yy]$ ]] || fatal "Fix DNS first, then re-run the installer." else warn "Could not verify DNS for ${DOMAIN}" echo -e " ${DIM}Make sure ${DOMAIN} points to ${PUBLIC_IP} for TLS to work.${NC}" fi # Email — offer skip echo "" echo -e " ${DIM}Email for Let's Encrypt notifications (optional, press Enter to skip):${NC}" read -rp " " EMAIL } setup_custom_tld() { DOMAIN_MODE="custom-tld" echo "" echo -e " ${DIM}Common choices: .home .local .lab .lan${NC}" read -rp "$(echo -e " ${YELLOW}TLD (default .home):${NC} ")" TLD [[ -z "$TLD" ]] && TLD=".home" [[ "$TLD" != .* ]] && TLD=".$TLD" echo "" read -rp "$(echo -e " ${YELLOW}CA name (default: DashCaddy Local CA):${NC} ")" input [[ -n "$input" ]] && CA_NAME="$input" } # ============================================================================ # Dependency Installation # ============================================================================ install_prereqs() { # Only install what's missing local needed=() command -v curl &>/dev/null || needed+=(curl) command -v jq &>/dev/null || needed+=(jq) command -v dig &>/dev/null || needed+=(dnsutils) # git only needed if using --source with a repo URL [[ -n "$DASHCADDY_REPO" ]] && ! command -v git &>/dev/null && needed+=(git) if [[ ${#needed[@]} -eq 0 ]]; then ok "Prerequisites already installed" return fi progress "Installing ${needed[*]}..." bash -c " case '$DISTRO_FAMILY' in debian) apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq ${needed[*]} ;; rhel) dnf install -y -q ${needed[*]} 2>/dev/null || yum install -y -q ${needed[*]} ;; arch) pacman -Sy --noconfirm ${needed[*]} ;; esac " || warn "Some prerequisites may not have installed" } install_docker() { if $SKIP_DOCKER; then ok "Docker: skipped (--skip-docker)"; return; fi if command -v docker &>/dev/null && docker info &>/dev/null; then ok "Docker: $(docker --version | grep -oP 'Docker version [\d.]+')" return fi progress "Installing Docker (this is the slowest step)..." bash -c " case '$DISTRO_FAMILY' in debian) curl -fsSL https://get.docker.com | sh ;; rhel) if command -v dnf &>/dev/null; then dnf install -y -q dnf-plugins-core dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo dnf install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin else yum install -y -q yum-utils yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo yum install -y -q docker-ce docker-ce-cli containerd.io docker-compose-plugin fi ;; arch) pacman -Sy --noconfirm docker docker-compose ;; esac systemctl enable docker systemctl start docker " || fatal "Docker installation failed. Install manually: https://docs.docker.com/engine/install/" if docker info &>/dev/null; then ok "Docker: $(docker --version | grep -oP 'Docker version [\d.]+')" else fatal "Docker installed but daemon not running. Try: systemctl start docker" fi } install_caddy() { if $SKIP_CADDY; then ok "Caddy: skipped (--skip-caddy)"; return; fi if command -v caddy &>/dev/null; then ok "Caddy: $(caddy version 2>/dev/null | head -1)" return fi progress "Installing Caddy..." bash -c " case '$DISTRO_FAMILY' in debian) apt-get install -y -qq debian-keyring debian-archive-keyring apt-transport-https curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null apt-get update -qq apt-get install -y -qq caddy ;; rhel) if command -v dnf &>/dev/null; then dnf install -y -q 'dnf-command(copr)' dnf copr enable -y @caddy/caddy dnf install -y -q caddy else yum install -y -q yum-plugin-copr yum copr enable -y @caddy/caddy yum install -y -q caddy fi ;; arch) pacman -Sy --noconfirm caddy ;; esac # Stop default Caddy — we configure it ourselves systemctl stop caddy 2>/dev/null || true " || fatal "Caddy installation failed. Install manually: https://caddyserver.com/docs/install" ok "Caddy: $(caddy version 2>/dev/null | head -1)" } # ============================================================================ # Fix known OS issues automatically # ============================================================================ fix_system_issues() { # systemd-resolved stub — breaks Docker DNS and Tailscale if [[ -L /etc/resolv.conf ]] && readlink /etc/resolv.conf | grep -q stub; then rm -f /etc/resolv.conf cat > /etc/resolv.conf <<'EOF' # Set by DashCaddy installer (replaced systemd-resolved stub) nameserver 1.1.1.1 nameserver 8.8.8.8 nameserver 1.0.0.1 EOF ok "Fixed systemd-resolved stub (replaced with public DNS)" fi # Ensure /etc/caddy exists mkdir -p /etc/caddy } # ============================================================================ # Directory & File Setup # ============================================================================ create_directories() { mkdir -p "$INSTALL_DIR" "$DOCKER_DATA" "$SITES_DIR" "$API_DIR" "$DASHBOARD_DIR" "${DASHBOARD_DIR}/assets" mkdir -p /opt/dashcaddy/updates /opt/dashcaddy/scripts ok "Directories created" } fetch_source() { local tmp_src="/tmp/dashcaddy-src-$$" if [[ -n "$SOURCE_PATH" ]]; then if [[ ! -d "$SOURCE_PATH" ]]; then fatal "Source path not found: $SOURCE_PATH" fi tmp_src="$SOURCE_PATH" ok "Using local source: $SOURCE_PATH" elif [[ -n "$DASHCADDY_REPO" ]]; then # Git clone mode (for development) progress "Cloning DashCaddy..." \ git clone --depth 1 --branch "$GIT_BRANCH" "$DASHCADDY_REPO" "$tmp_src" \ || fatal "Failed to clone DashCaddy. Check network." else # Download release tarball (default — no git needed) mkdir -p "$tmp_src" progress "Downloading DashCaddy v${DASHCADDY_VERSION}..." bash -c " curl -fsSL '${DASHCADDY_DOWNLOAD}' | tar xz -C '$tmp_src' --strip-components=1 " || fatal "Failed to download DashCaddy. Check network or try --source with a local copy." fi # Find API source (handle different repo layouts) local api_src="" for try in "${tmp_src}/dashcaddy-api" "${tmp_src}/sites/dashcaddy-api" "${tmp_src}/api"; do [[ -d "$try" ]] && api_src="$try" && break done [[ -z "$api_src" ]] && fatal "Cannot find dashcaddy-api/ in source" # Find dashboard source local dash_src="" for try in "${tmp_src}/status" "${tmp_src}/sites/status" "${tmp_src}/dashboard"; do [[ -d "$try" ]] && dash_src="$try" && break done [[ -z "$dash_src" ]] && fatal "Cannot find dashboard source in source" # Deploy API files cp -f "${api_src}"/*.js "$API_DIR/" 2>/dev/null || true cp -f "${api_src}/package.json" "$API_DIR/" cp -f "${api_src}/package-lock.json" "$API_DIR/" 2>/dev/null || true cp -f "${api_src}/Dockerfile" "$API_DIR/" cp -f "${api_src}/openapi.yaml" "$API_DIR/" 2>/dev/null || true [[ -d "${api_src}/routes" ]] && cp -rf "${api_src}/routes" "$API_DIR/" ok "API files deployed" # Deploy dashboard files cp -f "${dash_src}/index.html" "$DASHBOARD_DIR/" cp -f "${dash_src}/sw.js" "$DASHBOARD_DIR/" 2>/dev/null || true for dir in css js dist vendor assets; do [[ -d "${dash_src}/${dir}" ]] && cp -rf "${dash_src}/${dir}" "$DASHBOARD_DIR/" done ok "Dashboard files deployed" # Deploy updater scripts local scripts_src="" for try in "${api_src}/scripts" "${tmp_src}/scripts"; do [[ -d "$try" ]] && scripts_src="$try" && break done if [[ -n "$scripts_src" ]]; then cp -f "${scripts_src}/dashcaddy-update.sh" /opt/dashcaddy/scripts/ 2>/dev/null || true cp -f "${scripts_src}/dashcaddy-updater.path" /opt/dashcaddy/scripts/ 2>/dev/null || true cp -f "${scripts_src}/dashcaddy-updater.service" /opt/dashcaddy/scripts/ 2>/dev/null || true chmod +x /opt/dashcaddy/scripts/dashcaddy-update.sh 2>/dev/null || true ok "Updater scripts deployed" fi # Cleanup [[ -z "$SOURCE_PATH" ]] && rm -rf "$tmp_src" } create_seed_configs() { local config_type dashboard_host tz case "$DOMAIN_MODE" in public) config_type="public"; dashboard_host="$DOMAIN" ;; custom-tld) config_type="homelab"; dashboard_host="dashcaddy${TLD}" ;; local) config_type="local"; dashboard_host="${LAN_IP}:${LOCAL_PORT}" ;; esac tz=$(timedatectl show --property=Timezone --value 2>/dev/null || echo 'UTC') # Only create files that don't already exist (preserve on re-install) [[ -f "${INSTALL_DIR}/services.json" ]] || echo '[]' > "${INSTALL_DIR}/services.json" [[ -f "${INSTALL_DIR}/dns-credentials.json" ]] || echo '{}' > "${INSTALL_DIR}/dns-credentials.json" [[ -f "${INSTALL_DIR}/credentials.json" ]] || echo '{}' > "${INSTALL_DIR}/credentials.json" [[ -f "${INSTALL_DIR}/notifications.json" ]] || echo '[]' > "${INSTALL_DIR}/notifications.json" if [[ ! -f "${INSTALL_DIR}/.encryption-key" ]]; then openssl rand -hex 32 > "${INSTALL_DIR}/.encryption-key" chmod 600 "${INSTALL_DIR}/.encryption-key" fi # config.json — always regenerate (contains dashboard URL which may change) cat > "${INSTALL_DIR}/config.json" < "$cf" < "$cf" < "$cf" < "${API_DIR}/docker-compose.yml" < /etc/systemd/system/dashcaddy-updater.path <<'PATHEOF' [Unit] Description=Watch for DashCaddy update trigger [Path] PathChanged=/opt/dashcaddy/updates/trigger.json MakeDirectory=yes [Install] WantedBy=multi-user.target PATHEOF fi if [[ -f "${scripts_dir}/dashcaddy-updater.service" ]]; then cp -f "${scripts_dir}/dashcaddy-updater.service" /etc/systemd/system/ else cat > /etc/systemd/system/dashcaddy-updater.service <<'SVCEOF' [Unit] Description=DashCaddy auto-update handler After=docker.service Requires=docker.service [Service] Type=oneshot ExecStart=/opt/dashcaddy/scripts/dashcaddy-update.sh TimeoutStartSec=300 StandardOutput=journal StandardError=journal SyslogIdentifier=dashcaddy-update SVCEOF fi systemctl daemon-reload systemctl enable dashcaddy-updater.path >/dev/null 2>&1 systemctl start dashcaddy-updater.path >/dev/null 2>&1 ok "Auto-updater service installed" } # ============================================================================ # Build & Launch # ============================================================================ build_and_start() { # Remove old container if exists docker rm -f "$CONTAINER_NAME" 2>/dev/null || true cd "$API_DIR" progress "Building container image (30-60s)..." \ docker compose build --quiet \ || progress "Building container image (fallback)..." \ docker-compose build --quiet \ || fatal "Docker build failed. Check: docker info" progress "Starting container..." \ docker compose up -d \ || docker-compose up -d 2>/dev/null \ || fatal "Failed to start container" # Wait for healthy local i=0 printf "${CYAN} ▸${NC} Waiting for API health check " while (( i < 30 )); do if curl -fsSL --max-time 2 "http://localhost:${API_PORT}/health" &>/dev/null; then echo -e "${GREEN}healthy${NC}" return fi printf "." sleep 1 i=$((i + 1)) done echo -e "${YELLOW}timeout${NC}" warn "API may still be starting. Check: docker logs ${CONTAINER_NAME}" } start_caddy() { # Symlink our Caddyfile to where Caddy's systemd unit expects it if [[ -f /etc/caddy/Caddyfile && ! -L /etc/caddy/Caddyfile ]]; then mv /etc/caddy/Caddyfile /etc/caddy/Caddyfile.original 2>/dev/null || true fi ln -sf "${INSTALL_DIR}/Caddyfile" /etc/caddy/Caddyfile systemctl enable caddy >/dev/null 2>&1 systemctl restart caddy 2>/dev/null sleep 2 if systemctl is-active --quiet caddy; then ok "Caddy is running" else warn "Caddy may have issues. Check: systemctl status caddy" fi } # ============================================================================ # Firewall # ============================================================================ open_firewall() { if command -v ufw &>/dev/null && ufw status 2>/dev/null | grep -q "active"; then case "$DOMAIN_MODE" in public) ufw allow 80/tcp >/dev/null 2>&1; ufw allow 443/tcp >/dev/null 2>&1 ok "Firewall: opened 80, 443 (UFW)" ;; local) ufw allow "${LOCAL_PORT}"/tcp >/dev/null 2>&1 ok "Firewall: opened ${LOCAL_PORT} (UFW)" ;; esac elif command -v firewall-cmd &>/dev/null && systemctl is-active --quiet firewalld 2>/dev/null; then case "$DOMAIN_MODE" in public) firewall-cmd --permanent --add-service=http --add-service=https >/dev/null 2>&1 firewall-cmd --reload >/dev/null 2>&1 ok "Firewall: opened 80, 443 (firewalld)" ;; local) firewall-cmd --permanent --add-port="${LOCAL_PORT}"/tcp >/dev/null 2>&1 firewall-cmd --reload >/dev/null 2>&1 ok "Firewall: opened ${LOCAL_PORT} (firewalld)" ;; esac fi } # ============================================================================ # Uninstall # ============================================================================ do_uninstall() { echo -e "\n${BOLD} Uninstalling DashCaddy${NC}\n" echo " This will:" echo " - Stop and remove the dashcaddy-api container" echo " - Remove DashCaddy files from ${INSTALL_DIR}/" if $KEEP_CONFIG; then echo " - KEEP your config files (--keep-config)" fi echo " - NOT remove Docker or Caddy" echo "" read -rp "$(echo -e " ${YELLOW}Continue? [y/N]:${NC} ")" -n1 answer echo "" [[ "$answer" =~ ^[Yy]$ ]] || { echo " Cancelled."; exit 0; } docker rm -f "$CONTAINER_NAME" 2>/dev/null && ok "Container removed" || true # Stop and remove updater service systemctl stop dashcaddy-updater.path 2>/dev/null || true systemctl disable dashcaddy-updater.path 2>/dev/null || true rm -f /etc/systemd/system/dashcaddy-updater.path /etc/systemd/system/dashcaddy-updater.service systemctl daemon-reload 2>/dev/null || true rm -rf /opt/dashcaddy && ok "Updater service removed" || true if [[ -L /etc/caddy/Caddyfile ]] && readlink /etc/caddy/Caddyfile | grep -q dashcaddy; then rm -f /etc/caddy/Caddyfile [[ -f /etc/caddy/Caddyfile.original ]] && mv /etc/caddy/Caddyfile.original /etc/caddy/Caddyfile systemctl restart caddy 2>/dev/null || true ok "Caddy config restored" fi if $KEEP_CONFIG; then rm -rf "$API_DIR" "$DASHBOARD_DIR" ok "App files removed, config preserved in ${INSTALL_DIR}/" else rm -rf "$INSTALL_DIR" ok "All files removed from ${INSTALL_DIR}/" fi echo -e "\n${GREEN} DashCaddy uninstalled.${NC}\n" exit 0 } # ============================================================================ # Argument Parsing # ============================================================================ parse_args() { while [[ $# -gt 0 ]]; do case "$1" in quick) QUICK=true; DOMAIN_MODE="local"; shift ;; --domain) DOMAIN_MODE="public"; DOMAIN="${2:-}"; shift; shift ;; --email) EMAIL="${2:-}"; shift; shift ;; --tld) DOMAIN_MODE="custom-tld"; TLD="${2:-}"; shift; shift ;; --ca-name) CA_NAME="${2:-}"; shift; shift ;; --local) DOMAIN_MODE="local"; shift ;; --port) LOCAL_PORT="${2:-8080}"; shift; shift ;; --api-port) API_PORT="${2:-3001}"; shift; shift ;; --source) SOURCE_PATH="${2:-}"; shift; shift ;; --branch) GIT_BRANCH="${2:-main}"; shift; shift ;; --skip-docker) SKIP_DOCKER=true; shift ;; --skip-caddy) SKIP_CADDY=true; shift ;; --uninstall) UNINSTALL=true; shift ;; --keep-config) KEEP_CONFIG=true; shift ;; --yes|-y) AUTO_YES=true; shift ;; --help|-h) print_help; exit 0 ;; *) warn "Unknown option: $1 (ignored)"; shift ;; esac done # Normalize TLD if [[ -n "$TLD" && "$TLD" != .* ]]; then TLD=".$TLD"; fi } print_help() { cat <<'HELP' DashCaddy Linux Installer QUICK INSTALL (zero typing): curl -fsSL https://get.dashcaddy.net | bash -s -- quick WITH DOMAIN: curl -fsSL https://get.dashcaddy.net | bash -s -- --domain my.example.com INTERACTIVE: curl -fsSL https://get.dashcaddy.net | bash OPTIONS: quick Instant local install, zero questions --domain DOMAIN Public domain (Let's Encrypt TLS) --email EMAIL Let's Encrypt email (optional) --tld TLD Custom TLD, internal CA (e.g., .home) --local Local mode (http://IP:port) --port PORT Port for local mode (default: 8080) --source PATH Use local source files --skip-docker Already have Docker --skip-caddy Already have Caddy --uninstall Remove DashCaddy --keep-config Keep configs during uninstall --yes Skip confirmations HELP } # ============================================================================ # Banner & Success # ============================================================================ print_banner() { echo "" echo -e "${BOLD}${BLUE} ╔══════════════════════════════════════════════╗${NC}" echo -e "${BOLD}${BLUE} ║${NC}${BOLD} DashCaddy Installer v${DASHCADDY_VERSION} ${BLUE}║${NC}" echo -e "${BOLD}${BLUE} ╚══════════════════════════════════════════════╝${NC}" echo "" } print_success() { local total_time=$1 local url case "$DOMAIN_MODE" in public) url="https://${DOMAIN}" ;; custom-tld) url="https://dashcaddy${TLD}" ;; local) url="http://${PUBLIC_IP}:${LOCAL_PORT}" ;; esac # Also show LAN URL for local mode local lan_url="" if [[ "$DOMAIN_MODE" == "local" && "$LAN_IP" != "$PUBLIC_IP" ]]; then lan_url="http://${LAN_IP}:${LOCAL_PORT}" fi echo "" echo -e "${GREEN}${BOLD} ┌──────────────────────────────────────────────┐${NC}" echo -e "${GREEN}${BOLD} │ Installation Complete! │${NC}" echo -e "${GREEN}${BOLD} └──────────────────────────────────────────────┘${NC}" echo "" echo -e " ${BOLD}Open in browser:${NC} ${url}" [[ -n "$lan_url" ]] && echo -e " ${BOLD}LAN access:${NC} ${lan_url}" echo "" echo -e " ${DIM}Config: ${INSTALL_DIR}/ | Logs: docker logs dashcaddy-api${NC}" echo -e " ${DIM}Installed in: ${total_time}${NC}" if [[ "$DOMAIN_MODE" == "public" ]]; then echo "" echo -e " ${CYAN}TLS will auto-provision on first visit.${NC}" echo -e " ${CYAN}Ensure DNS for ${DOMAIN} → ${PUBLIC_IP}${NC}" fi echo "" echo -e " ${BOLD}Everything is managed from the dashboard — no CLI needed.${NC}" echo -e " ${DIM}Deploy apps, manage Docker containers, edit Caddy configs,${NC}" echo -e " ${DIM}and monitor services all from the web UI.${NC}" echo "" } # ============================================================================ # Main # ============================================================================ main() { local start_time start_time=$(date +%s) parse_args "$@" # Handle uninstall if $UNINSTALL; then do_uninstall; fi print_banner # ---- Step 1: System check ---- step "Checking system" detect_system fix_system_issues # ---- Step 2: Configuration ---- if $QUICK; then ok "Quick mode: http://${PUBLIC_IP}:${LOCAL_PORT}" else step "Configuration" interactive_setup fi # Show what we're doing echo "" case "$DOMAIN_MODE" in public) log "Installing with domain: ${DOMAIN}" ;; custom-tld) log "Installing with TLD: ${TLD}" ;; local) log "Installing in local mode (port ${LOCAL_PORT})" ;; esac # ---- Step 3: Dependencies ---- step "Installing dependencies" install_prereqs install_docker install_caddy # ---- Step 4: Deploy files ---- step "Deploying DashCaddy" create_directories fetch_source create_seed_configs # ---- Step 5: Configuration ---- step "Generating configuration" generate_caddyfile generate_docker_compose open_firewall # ---- Step 6: Build & start ---- step "Building & starting services" build_and_start install_updater_service # ---- Step 7: Start Caddy ---- step "Starting web server" start_caddy print_success "$(elapsed "$start_time")" } main "$@"