#!/usr/bin/env bash # ============================================================================ # OpenClaw CalDAV (Radicale) Installer # Deploys Radicale via Docker with Nginx + Let's Encrypt SSL # Compatible with OpenClaw gateway via localhost # # Usage: sudo bash install.sh # GitHub: https://github.com/openclaw/caldav-installer # ============================================================================ set -euo pipefail # ── Colors ─────────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # ── Helpers ────────────────────────────────────────────────────────────────── info() { echo -e "${CYAN}[INFO]${NC} $*"; } success() { echo -e "${GREEN}[ OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } fail() { echo -e "${RED}[FAIL]${NC} $*"; exit 1; } banner() { echo "" echo -e "${BOLD}${CYAN}" echo " ┌──────────────────────────────────────────────┐" echo " │ 🦞 OpenClaw CalDAV Installer 🦞 │" echo " │ Radicale + Docker + Nginx + SSL │" echo " └──────────────────────────────────────────────┘" echo -e "${NC}" echo "" } # ── Pre-flight checks ─────────────────────────────────────────────────────── preflight() { if [[ $EUID -ne 0 ]]; then fail "This script must be run as root (sudo bash install.sh)" fi if ! grep -qiE 'ubuntu|debian' /etc/os-release 2>/dev/null; then fail "This installer supports Ubuntu/Debian only." fi if ss -tlnp | grep -qE ':80\b' 2>/dev/null; then warn "Port 80 is already in use. This is OK if Nginx is already running." fi if ss -tlnp | grep -qE ':443\b' 2>/dev/null; then warn "Port 443 is already in use. This is OK if Nginx is already running." fi success "Pre-flight checks passed." } # ── Interactive prompts ────────────────────────────────────────────────────── gather_input() { echo -e "${BOLD}Step 1: Configuration${NC}" echo "" # Domain while true; do read -rp " Enter your CalDAV domain (e.g. cal.example.com): " CAL_DOMAIN if [[ -z "$CAL_DOMAIN" ]]; then warn "Domain cannot be empty." elif [[ "$CAL_DOMAIN" == *" "* ]]; then warn "Domain cannot contain spaces." else break fi done # Email for SSL while true; do read -rp " Enter your email (for Let's Encrypt SSL certificate): " CAL_EMAIL if [[ -z "$CAL_EMAIL" ]]; then warn "Email cannot be empty." elif [[ "$CAL_EMAIL" != *"@"* ]]; then warn "Please enter a valid email address." else break fi done # CalDAV username while true; do read -rp " CalDAV username (e.g. openclaw): " CAL_USER if [[ -z "$CAL_USER" ]]; then warn "Username cannot be empty." else break fi done # CalDAV password while true; do read -rsp " CalDAV password: " CAL_PASS echo "" if [[ -z "$CAL_PASS" ]]; then warn "Password cannot be empty." else read -rsp " Confirm password: " CAL_PASS_CONFIRM echo "" if [[ "$CAL_PASS" != "$CAL_PASS_CONFIRM" ]]; then warn "Passwords do not match." else break fi fi done # Internal port read -rp " Internal proxy port [5232]: " CAL_PORT CAL_PORT=${CAL_PORT:-5232} # Install directory read -rp " Installation directory [~/caldav-docker]: " INSTALL_DIR INSTALL_DIR=${INSTALL_DIR:-"$HOME/caldav-docker"} # Expand tilde INSTALL_DIR="${INSTALL_DIR/#\~/$HOME}" echo "" echo -e "${BOLD} Summary:${NC}" echo " Domain: https://${CAL_DOMAIN}" echo " Email: ${CAL_EMAIL}" echo " Username: ${CAL_USER}" echo " Port: 127.0.0.1:${CAL_PORT} → :5232" echo " Directory: ${INSTALL_DIR}" echo "" read -rp " Proceed? (Y/n): " confirm [[ "$confirm" =~ ^[Nn]$ ]] && { info "Aborted."; exit 0; } } # ── Install dependencies ──────────────────────────────────────────────────── install_deps() { echo "" echo -e "${BOLD}Step 2: Installing dependencies${NC}" echo "" info "Updating package index..." apt-get update -qq # Docker if command -v docker &>/dev/null; then success "Docker already installed: $(docker --version)" else info "Installing Docker..." apt-get install -y -qq ca-certificates curl gnupg lsb-release install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 2>/dev/null || true chmod a+r /etc/apt/keyrings/docker.gpg echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ tee /etc/apt/sources.list.d/docker.list > /dev/null apt-get update -qq apt-get install -y -qq docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin success "Docker installed." fi # Docker Compose if docker compose version &>/dev/null; then COMPOSE_CMD="docker compose" success "Docker Compose (v2 plugin) available." elif command -v docker-compose &>/dev/null; then COMPOSE_CMD="docker-compose" success "Docker Compose (v1 standalone) available." else info "Installing Docker Compose plugin..." apt-get install -y -qq docker-compose-plugin COMPOSE_CMD="docker compose" success "Docker Compose installed." fi # Nginx if command -v nginx &>/dev/null; then success "Nginx already installed." else info "Installing Nginx..." apt-get install -y -qq nginx systemctl enable nginx systemctl start nginx success "Nginx installed and started." fi # Certbot if command -v certbot &>/dev/null; then success "Certbot already installed." else info "Installing Certbot..." apt-get install -y -qq certbot python3-certbot-nginx success "Certbot installed." fi # apache2-utils (for htpasswd / bcrypt) if command -v htpasswd &>/dev/null; then success "htpasswd (apache2-utils) already installed." else info "Installing apache2-utils for bcrypt password hashing..." apt-get install -y -qq apache2-utils success "apache2-utils installed." fi # UFW (optional) if command -v ufw &>/dev/null; then info "Configuring firewall (UFW)..." ufw allow 'Nginx Full' >/dev/null 2>&1 || true ufw allow OpenSSH >/dev/null 2>&1 || true success "Firewall rules updated." fi success "All dependencies ready." } # ── Create Docker stack ────────────────────────────────────────────────────── create_docker_stack() { echo "" echo -e "${BOLD}Step 3: Creating Docker stack${NC}" echo "" mkdir -p "${INSTALL_DIR}/config" mkdir -p "${INSTALL_DIR}/data" # Docker Compose cat > "${INSTALL_DIR}/docker-compose.yml" < "${INSTALL_DIR}/config/config" < "${INSTALL_DIR}/.env" < "${NGINX_CONF}" </dev/null 2>&1; then success "Radicale is responding on port ${CAL_PORT}!" break fi sleep 5 done # Show container status echo "" $COMPOSE_CMD ps echo "" } # ── Print summary ─────────────────────────────────────────────────────────── print_summary() { echo "" echo -e "${BOLD}${GREEN}" echo " ┌──────────────────────────────────────────────┐" echo " │ ✅ Installation Complete! ✅ │" echo " └──────────────────────────────────────────────┘" echo -e "${NC}" echo "" echo -e "${BOLD} Access your CalDAV server:${NC}" echo " 🌐 https://${CAL_DOMAIN}" echo "" echo -e "${BOLD} Internal (localhost) access:${NC}" echo " 📡 http://127.0.0.1:${CAL_PORT}" echo "" echo -e "${BOLD} Credentials:${NC}" echo " 👤 Username: ${CAL_USER}" echo " 📄 Env file: ${INSTALL_DIR}/.env" echo "" echo -e "${BOLD} Management commands:${NC}" echo " Start: cd ${INSTALL_DIR} && ${COMPOSE_CMD} up -d" echo " Stop: cd ${INSTALL_DIR} && ${COMPOSE_CMD} down" echo " Logs: cd ${INSTALL_DIR} && ${COMPOSE_CMD} logs -f" echo " Status: cd ${INSTALL_DIR} && ${COMPOSE_CMD} ps" echo "" echo -e "${BOLD} Add a new CalDAV user:${NC}" echo " sudo htpasswd -B ${INSTALL_DIR}/config/users " echo " cd ${INSTALL_DIR} && ${COMPOSE_CMD} restart" echo "" echo -e "${BOLD}${CYAN} ── Client Setup ──${NC}" echo "" echo " Apple Calendar (macOS / iOS):" echo " Settings → Accounts → Add Account → Other → CalDAV" echo " Server: https://${CAL_DOMAIN}" echo " Username: ${CAL_USER}" echo " Password: (the password you entered)" echo "" echo " Thunderbird / GNOME Calendar:" echo " Server URL: https://${CAL_DOMAIN}/${CAL_USER}/calendar.ics/" echo "" echo -e "${BOLD}${CYAN} ── OpenClaw Integration ──${NC}" echo "" echo " Configure OpenClaw to sync calendars:" echo -e " ${YELLOW}openclaw config set tools.calendar.provider \"caldav\"${NC}" echo -e " ${YELLOW}openclaw config set tools.calendar.caldav.url \"https://${CAL_DOMAIN}\"${NC}" echo -e " ${YELLOW}openclaw config set tools.calendar.caldav.username \"${CAL_USER}\"${NC}" echo -e " ${YELLOW}openclaw config set tools.calendar.caldav.password \"\"${NC}" echo "" echo " OpenClaw will connect internally via localhost:${CAL_PORT} 🦞" echo "" } # ── Main ───────────────────────────────────────────────────────────────────── main() { banner preflight gather_input install_deps create_docker_stack configure_nginx obtain_ssl start_stack print_summary } main "$@"