- self-updater.js: polls for new versions, downloads/verifies tarballs, triggers host-side rebuild via systemd path unit - dashcaddy-update.sh + systemd units: host-side container rebuild with automatic rollback on health check failure - 7 new /api/v1/system/* endpoints for version info, update check/apply, rollback, and update history - Frontend: DashCaddy tab in Updates modal with version display, changelog, update button, rollback, and notification dot - install.sh: updater service installation, volume mounts, env vars - build-release.sh + webhook-handler.js: release server pipeline (Gitea webhook → build tarball → deploy to get.dashcaddy.net) - Dockerfile: DASHCADDY_COMMIT build arg → VERSION file - Version bump to 1.1.0 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1042 lines
33 KiB
Bash
1042 lines
33 KiB
Bash
#!/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" <<CFGEOF
|
|
{
|
|
"setupComplete": true,
|
|
"configurationType": "${config_type}",
|
|
"timestamp": "$(date -u +%Y-%m-%dT%H:%M:%S.000Z)",
|
|
"timezone": "${tz}",
|
|
"dashboardTitle": "DashCaddy",
|
|
"dashboardHost": "${dashboard_host}",
|
|
"tld": "${TLD}",
|
|
"caName": "${CA_NAME}",
|
|
"license": {
|
|
"active": true,
|
|
"tier": "community",
|
|
"features": []
|
|
}
|
|
}
|
|
CFGEOF
|
|
|
|
ok "Config files ready"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Caddyfile Generation
|
|
# ============================================================================
|
|
|
|
generate_caddyfile() {
|
|
local cf="${INSTALL_DIR}/Caddyfile"
|
|
|
|
# --- Shared snippets ---
|
|
local snippets
|
|
read -r -d '' snippets <<'SNIP' || true
|
|
(cors-preflight) {
|
|
@preflight method OPTIONS
|
|
header @preflight {
|
|
Access-Control-Allow-Origin "*"
|
|
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
|
Access-Control-Allow-Headers "*"
|
|
Access-Control-Max-Age "600"
|
|
}
|
|
respond @preflight 204
|
|
}
|
|
(cors-allow) {
|
|
header {
|
|
Access-Control-Allow-Origin "*"
|
|
Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS"
|
|
Access-Control-Allow-Headers "*"
|
|
Access-Control-Max-Age "600"
|
|
}
|
|
}
|
|
SNIP
|
|
|
|
local auth_snippet="(dashcaddy_auth) {
|
|
forward_auth localhost:${API_PORT} {
|
|
uri /api/auth/gate/{args[0]}
|
|
copy_headers Authorization X-Api-Key X-App-Cookie X-Emby-Token X-Plex-Token
|
|
}
|
|
}"
|
|
|
|
local site_body=" root * ${DASHBOARD_DIR}
|
|
encode gzip
|
|
|
|
handle /api/* {
|
|
reverse_proxy localhost:${API_PORT}
|
|
}
|
|
|
|
handle {
|
|
@notFile not file {path}
|
|
rewrite @notFile /index.html
|
|
file_server
|
|
}"
|
|
|
|
# --- Mode-specific Caddyfile ---
|
|
case "$DOMAIN_MODE" in
|
|
public)
|
|
cat > "$cf" <<CEOF
|
|
# DashCaddy - Public Domain (Let's Encrypt)
|
|
{
|
|
admin localhost:${CADDY_ADMIN_PORT}
|
|
$([ -n "$EMAIL" ] && echo " email ${EMAIL}")
|
|
}
|
|
|
|
${snippets}
|
|
|
|
${auth_snippet}
|
|
|
|
${DOMAIN} {
|
|
${site_body}
|
|
}
|
|
CEOF
|
|
;;
|
|
|
|
custom-tld)
|
|
cat > "$cf" <<CEOF
|
|
# DashCaddy - Custom TLD (${TLD})
|
|
{
|
|
admin localhost:${CADDY_ADMIN_PORT}
|
|
pki {
|
|
ca local {
|
|
name "${CA_NAME}"
|
|
root_cn "${CA_NAME} Root CA"
|
|
intermediate_cn "${CA_NAME} Intermediate CA"
|
|
}
|
|
}
|
|
}
|
|
|
|
${snippets}
|
|
|
|
${auth_snippet}
|
|
|
|
dashcaddy${TLD} {
|
|
tls internal
|
|
${site_body}
|
|
}
|
|
CEOF
|
|
;;
|
|
|
|
local)
|
|
cat > "$cf" <<CEOF
|
|
# DashCaddy - Local Mode
|
|
{
|
|
admin localhost:${CADDY_ADMIN_PORT}
|
|
auto_https off
|
|
}
|
|
|
|
${snippets}
|
|
|
|
:${LOCAL_PORT} {
|
|
${site_body}
|
|
}
|
|
CEOF
|
|
;;
|
|
esac
|
|
|
|
ok "Caddyfile generated"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Docker Compose
|
|
# ============================================================================
|
|
|
|
generate_docker_compose() {
|
|
cat > "${API_DIR}/docker-compose.yml" <<DCEOF
|
|
services:
|
|
dashcaddy-api:
|
|
build: .
|
|
container_name: ${CONTAINER_NAME}
|
|
ports:
|
|
- "${API_PORT}:${API_PORT}"
|
|
volumes:
|
|
- ${INSTALL_DIR}/Caddyfile:/caddyfile:rw
|
|
- ${INSTALL_DIR}/services.json:/app/services.json:rw
|
|
- ${INSTALL_DIR}/dns-credentials.json:/app/dns-credentials.json:rw
|
|
- ${INSTALL_DIR}/config.json:/app/config.json:rw
|
|
- ${INSTALL_DIR}/credentials.json:/app/credentials.json:rw
|
|
- ${INSTALL_DIR}/.encryption-key:/app/.encryption-key:rw
|
|
- ${INSTALL_DIR}/.license-secret:/app/.license-secret:ro
|
|
- ${INSTALL_DIR}/totp-config.json:/app/totp-config.json:rw
|
|
- ${INSTALL_DIR}/notifications.json:/app/notifications.json:rw
|
|
- ${DASHBOARD_DIR}/assets:/app/assets:rw
|
|
- ${DASHBOARD_DIR}:/app/dashboard:rw
|
|
- /opt/dashcaddy/updates:/app/updates:rw
|
|
- /var/run/docker.sock:/var/run/docker.sock
|
|
environment:
|
|
- CADDYFILE_PATH=/caddyfile
|
|
- CADDY_ADMIN_URL=http://host.docker.internal:${CADDY_ADMIN_PORT}
|
|
- ASSETS_PATH=/app/assets
|
|
- CREDENTIALS_FILE=/app/credentials.json
|
|
- CONFIG_FILE=/app/config.json
|
|
- SERVICES_FILE=/app/services.json
|
|
- DNS_CREDENTIALS_FILE=/app/dns-credentials.json
|
|
- HOST_LAN_IP=${LAN_IP}
|
|
- NODE_ENV=production
|
|
- DASHCADDY_UPDATE_ENABLED=true
|
|
- DASHCADDY_UPDATE_URL=https://get.dashcaddy.net/release
|
|
- DASHCADDY_MIRROR_URL=https://get2.dashcaddy.net/release
|
|
- DASHCADDY_UPDATES_DIR=/app/updates
|
|
- DASHCADDY_API_SOURCE_DIR=${API_DIR}
|
|
- DASHCADDY_FRONTEND_DIR=/app/dashboard
|
|
extra_hosts:
|
|
- "host.docker.internal:host-gateway"
|
|
restart: unless-stopped
|
|
logging:
|
|
driver: json-file
|
|
options:
|
|
max-size: "10m"
|
|
max-file: "3"
|
|
DCEOF
|
|
|
|
ok "docker-compose.yml generated"
|
|
}
|
|
|
|
# ============================================================================
|
|
# Auto-Updater Service
|
|
# ============================================================================
|
|
|
|
install_updater_service() {
|
|
local scripts_dir="/opt/dashcaddy/scripts"
|
|
|
|
if [[ ! -f "${scripts_dir}/dashcaddy-update.sh" ]]; then
|
|
warn "Updater script not found — skipping auto-update service"
|
|
return
|
|
fi
|
|
|
|
# Install systemd units
|
|
if [[ -f "${scripts_dir}/dashcaddy-updater.path" ]]; then
|
|
cp -f "${scripts_dir}/dashcaddy-updater.path" /etc/systemd/system/
|
|
else
|
|
cat > /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 "$@"
|