Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
960
dashcaddy-installer/install.sh
Normal file
960
dashcaddy-installer/install.sh
Normal file
@@ -0,0 +1,960 @@
|
||||
#!/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.0.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"
|
||||
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"
|
||||
|
||||
# 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
|
||||
- /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
|
||||
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"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# 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
|
||||
|
||||
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
|
||||
|
||||
# ---- Step 7: Start Caddy ----
|
||||
step "Starting web server"
|
||||
start_caddy
|
||||
|
||||
print_success "$(elapsed "$start_time")"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user