Merge pull request #6 from outis1one/claude/cleanup-setup-webui-bCgPE
Claude/cleanup setup webui b cg pe
This commit is contained in:
+702
-106
@@ -1,15 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# Local AI Stack — Ubuntu 24.04
|
||||
# Services: Ollama · Open WebUI · RAG · MCP · ChromaDB · SearXNG
|
||||
# Kiwix · Gitea · InvokeAI · Portainer
|
||||
# Local AI Setup — Ubuntu 24.04
|
||||
# One script to rule them all. Select what you need, answer questions, walk away.
|
||||
#
|
||||
# Options (checklist at launch):
|
||||
# • Full system setup — Ubuntu apps, security, backups (ubuntu-post-install.sh)
|
||||
# • AI stack — Ollama · Open WebUI · RAG · MCP · ChromaDB · SearXNG
|
||||
# Gitea · InvokeAI · Portainer
|
||||
# • Kiwix — Offline Wikipedia, Stack Overflow, Arch Wiki, etc.
|
||||
#
|
||||
# Usage:
|
||||
# ./laptop_full_setup.sh — new install or update
|
||||
# ./laptop_full_setup.sh — interactive (asks everything upfront)
|
||||
# ./laptop_full_setup.sh --force — overwrite existing config files too
|
||||
#
|
||||
# ZIM downloads: run ./kiwix_download.sh separately (large files)
|
||||
# GPU: Ollama auto-detects VRAM — works with any NVIDIA GPU
|
||||
# GPU: Ollama auto-detects VRAM — works with any NVIDIA GPU
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
@@ -33,37 +37,422 @@ LOCAL_IP=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K\S+' \
|
||||
|| hostname -I | awk '{print $1}')
|
||||
[[ -z "$LOCAL_IP" ]] && read -rp "Enter your LAN IP: " LOCAL_IP
|
||||
|
||||
# Models pulled on first install (adjust to your VRAM)
|
||||
EMBED_MODEL="nomic-embed-text" # always needed for RAG
|
||||
# Models (defaults, may be adjusted below based on VRAM)
|
||||
EMBED_MODEL="nomic-embed-text"
|
||||
CHAT_MODEL="qwen2.5:14b"
|
||||
CODE_MODEL="qwen2.5-coder:7b"
|
||||
FAST_MODEL="qwen2.5:7b"
|
||||
|
||||
# ── detect GPU ────────────────────────────────────────────────────────────────
|
||||
VRAM_GB=$(nvidia-smi --query-gpu=memory.total --format=csv,noheader,nounits 2>/dev/null \
|
||||
| head -1 | awk '{printf "%d", $1/1024}' 2>/dev/null || echo "0")
|
||||
GPU_NAME=$(nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | head -1 || echo "None")
|
||||
|
||||
if [[ "$VRAM_GB" -ge 14 ]]; then
|
||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"
|
||||
GPU_TIER="16GB VRAM — 14B models"
|
||||
elif [[ "$VRAM_GB" -ge 8 ]]; then
|
||||
CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:7b"
|
||||
GPU_TIER="8GB VRAM — 14B chat, 7B code"
|
||||
elif [[ "$VRAM_GB" -ge 4 ]]; then
|
||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
||||
GPU_TIER="6GB VRAM — 7B models"
|
||||
elif [[ "$VRAM_GB" -gt 0 ]]; then
|
||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
||||
GPU_TIER="${VRAM_GB}GB VRAM — 7B models"
|
||||
else
|
||||
CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"
|
||||
GPU_TIER="CPU only — 7B models (slow)"
|
||||
fi
|
||||
|
||||
# ── new vs update ─────────────────────────────────────────────────────────────
|
||||
IS_UPDATE=false
|
||||
[[ -f "$BASE/docker-compose.yml" ]] && IS_UPDATE=true
|
||||
|
||||
section "Local AI Stack — $($IS_UPDATE && echo UPDATE || echo NEW INSTALL)"
|
||||
info "Base : $BASE"
|
||||
info "Host : $LOCAL_IP"
|
||||
# =============================================================================
|
||||
# QUESTIONS UPFRONT — answer everything, then walk away
|
||||
# =============================================================================
|
||||
section "Local AI Stack — Setup Wizard"
|
||||
echo ""
|
||||
echo -e " ${BOLD}Answer the questions below, then leave it overnight.${NC}"
|
||||
echo -e " Everything will install and download while you sleep."
|
||||
echo ""
|
||||
info "Machine : $(hostname)"
|
||||
info "LAN IP : $LOCAL_IP"
|
||||
info "GPU : ${GPU_NAME} (${VRAM_GB}GB VRAM)"
|
||||
$IS_UPDATE && warn "Existing install found. Config files kept unless --force is passed."
|
||||
|
||||
# ── component menu ─────────────────────────────────────────────────────────────
|
||||
# ── Q1: Top-level — what to run ───────────────────────────────────────────────
|
||||
INSTALL_POSTINSTALL=false
|
||||
INSTALL_AI=true
|
||||
|
||||
# Per-service flags (all default ON — toggled off in Q1a)
|
||||
SVC_WEBUI=true; SVC_RAG=true; SVC_MCP=true; SVC_SEARXNG=true
|
||||
SVC_GITEA=true; SVC_INVOKEAI=true; SVC_PORTAINER=true; SVC_KIWIX=true
|
||||
|
||||
POSTINSTALL_SCRIPT="$SCRIPT_DIR/ubuntu-post-install.sh"
|
||||
HAS_POSTINSTALL=false
|
||||
[[ -f "$POSTINSTALL_SCRIPT" ]] && HAS_POSTINSTALL=true
|
||||
|
||||
if command -v whiptail &>/dev/null; then
|
||||
WHIP_ARGS=()
|
||||
$HAS_POSTINSTALL && WHIP_ARGS+=(
|
||||
"postinstall" "Full system setup — Ubuntu apps, security, backups" OFF
|
||||
)
|
||||
WHIP_ARGS+=( "aistack" "AI stack — select services on next screen" ON )
|
||||
SELECTED=$(whiptail --title "Local AI Setup" \
|
||||
--checklist "SPACE = toggle TAB = move ENTER = confirm" \
|
||||
12 68 "${#WHIP_ARGS[@]}" "${WHIP_ARGS[@]}" 3>&1 1>&2 2>&3) || die "Cancelled."
|
||||
INSTALL_AI=false
|
||||
[[ "$SELECTED" == *"postinstall"* ]] && INSTALL_POSTINSTALL=true
|
||||
[[ "$SELECTED" == *"aistack"* ]] && INSTALL_AI=true
|
||||
else
|
||||
echo ""
|
||||
echo -e " ${BOLD}[1] Select what to run${NC}"
|
||||
echo " (Type a number to toggle, Enter to confirm)"
|
||||
SEL_POST=false; SEL_AI=true
|
||||
while true; do
|
||||
echo ""
|
||||
$HAS_POSTINSTALL && printf " [%s] 1. Full system setup — Ubuntu apps, security, backups\n" "$($SEL_POST && echo '*' || echo ' ')"
|
||||
printf " [%s] 2. AI stack — select services on next screen\n" "$($SEL_AI && echo '*' || echo ' ')"
|
||||
echo ""
|
||||
read -rp " Toggle [number] or Enter to confirm: " T
|
||||
case "$T" in
|
||||
1) $HAS_POSTINSTALL && { $SEL_POST && SEL_POST=false || SEL_POST=true; } ;;
|
||||
2) $SEL_AI && SEL_AI=false || SEL_AI=true ;;
|
||||
"") break ;;
|
||||
esac
|
||||
done
|
||||
INSTALL_POSTINSTALL=$SEL_POST
|
||||
INSTALL_AI=$SEL_AI
|
||||
fi
|
||||
|
||||
$INSTALL_AI || $INSTALL_POSTINSTALL \
|
||||
|| die "Nothing selected — run again and select at least one option."
|
||||
|
||||
# ── Q1a: AI stack service selection ───────────────────────────────────────────
|
||||
if $INSTALL_AI; then
|
||||
# detect which containers already exist (mark them ON)
|
||||
existing_svc() {
|
||||
docker ps -a --format '{{.Names}}' 2>/dev/null | grep -q "^$1$" && echo ON || echo OFF
|
||||
}
|
||||
|
||||
if command -v whiptail &>/dev/null; then
|
||||
SELECTED_SVCS=$(whiptail --title "AI Stack — Select Services" \
|
||||
--checklist "Ollama always installed (core). SPACE = toggle ENTER = confirm" \
|
||||
20 72 9 \
|
||||
"open-webui" "Open WebUI — Chat interface (like ChatGPT, uses Ollama)" "$(existing_svc open-webui)" \
|
||||
"rag" "RAG + ChromaDB— Code & document search (needed by MCP)" "$(existing_svc rag-server)" \
|
||||
"mcp" "MCP Server — Claude Code tools (bash, files, git, search)" "$(existing_svc mcp-server)" \
|
||||
"searxng" "SearXNG — Private local web search engine" "$(existing_svc searxng)" \
|
||||
"gitea" "Gitea — Self-hosted Git server" "$(existing_svc gitea)" \
|
||||
"invokeai" "InvokeAI — Image generation (Stable Diffusion)" "$(existing_svc invokeai)" \
|
||||
"portainer" "Portainer — Docker management web UI" "$(existing_svc portainer)" \
|
||||
"kiwix" "Kiwix — Offline Wikipedia, Stack Overflow, Arch Wiki" "$(existing_svc kiwix)" \
|
||||
3>&1 1>&2 2>&3) || die "Cancelled."
|
||||
|
||||
SVC_WEBUI=false; [[ "$SELECTED_SVCS" == *'"open-webui"'* ]] && SVC_WEBUI=true
|
||||
SVC_RAG=false; [[ "$SELECTED_SVCS" == *'"rag"'* ]] && SVC_RAG=true
|
||||
SVC_MCP=false; [[ "$SELECTED_SVCS" == *'"mcp"'* ]] && SVC_MCP=true
|
||||
SVC_SEARXNG=false; [[ "$SELECTED_SVCS" == *'"searxng"'* ]] && SVC_SEARXNG=true
|
||||
SVC_GITEA=false; [[ "$SELECTED_SVCS" == *'"gitea"'* ]] && SVC_GITEA=true
|
||||
SVC_INVOKEAI=false; [[ "$SELECTED_SVCS" == *'"invokeai"'* ]] && SVC_INVOKEAI=true
|
||||
SVC_PORTAINER=false;[[ "$SELECTED_SVCS" == *'"portainer"'* ]] && SVC_PORTAINER=true
|
||||
SVC_KIWIX=false; [[ "$SELECTED_SVCS" == *'"kiwix"'* ]] && SVC_KIWIX=true
|
||||
else
|
||||
# Text toggle fallback
|
||||
echo ""
|
||||
echo -e " ${BOLD}[1a] AI Stack — select services to install${NC}"
|
||||
echo " Ollama always included (required core). Type number to toggle, Enter to confirm."
|
||||
_W=$SVC_WEBUI; _R=$SVC_RAG; _M=$SVC_MCP; _S=$SVC_SEARXNG
|
||||
_G=$SVC_GITEA; _I=$SVC_INVOKEAI; _P=$SVC_PORTAINER; _K=$SVC_KIWIX
|
||||
while true; do
|
||||
echo ""
|
||||
printf " [%s] 1. Open WebUI — Chat interface\n" "$($_W && echo '*' || echo ' ')"
|
||||
printf " [%s] 2. RAG+ChromaDB — Code & document search\n" "$($_R && echo '*' || echo ' ')"
|
||||
printf " [%s] 3. MCP Server — Claude Code tools (needs RAG)\n" "$($_M && echo '*' || echo ' ')"
|
||||
printf " [%s] 4. SearXNG — Private web search\n" "$($_S && echo '*' || echo ' ')"
|
||||
printf " [%s] 5. Gitea — Self-hosted Git\n" "$($_G && echo '*' || echo ' ')"
|
||||
printf " [%s] 6. InvokeAI — Image generation\n" "$($_I && echo '*' || echo ' ')"
|
||||
printf " [%s] 7. Portainer — Docker management UI\n" "$($_P && echo '*' || echo ' ')"
|
||||
printf " [%s] 8. Kiwix — Offline Wikipedia, Stack Overflow\n" "$($_K && echo '*' || echo ' ')"
|
||||
echo ""
|
||||
read -rp " Toggle [number] or Enter to confirm: " T
|
||||
case "$T" in
|
||||
1) $_W && _W=false || _W=true ;;
|
||||
2) $_R && _R=false || _R=true ;;
|
||||
3) $_M && _M=false || _M=true ;;
|
||||
4) $_S && _S=false || _S=true ;;
|
||||
5) $_G && _G=false || _G=true ;;
|
||||
6) $_I && _I=false || _I=true ;;
|
||||
7) $_P && _P=false || _P=true ;;
|
||||
8) $_K && _K=false || _K=true ;;
|
||||
"") break ;;
|
||||
esac
|
||||
done
|
||||
SVC_WEBUI=$_W; SVC_RAG=$_R; SVC_MCP=$_M; SVC_SEARXNG=$_S
|
||||
SVC_GITEA=$_G; SVC_INVOKEAI=$_I; SVC_PORTAINER=$_P; SVC_KIWIX=$_K
|
||||
fi
|
||||
|
||||
# Enforce dependencies: MCP needs RAG
|
||||
$SVC_MCP && ! $SVC_RAG && { warn "MCP Server requires RAG — enabling RAG+ChromaDB"; SVC_RAG=true; }
|
||||
fi
|
||||
|
||||
# ── Q1b: SSH keys from GitHub / Launchpad ─────────────────────────────────────
|
||||
SSH_IMPORT_IDS=() # list of "gh:username" or "lp:username" entries
|
||||
|
||||
if ! $IS_UPDATE || [[ ! -f "$HOME/.ssh/authorized_keys" ]]; then
|
||||
echo ""
|
||||
echo -e " ${BOLD}[SSH] Import SSH public keys? (for passwordless SSH into this machine)${NC}"
|
||||
echo " Pulls your public keys from GitHub or Launchpad and adds them to"
|
||||
echo " ~/.ssh/authorized_keys using ssh-import-id."
|
||||
echo ""
|
||||
echo " Examples: gh:yourusername lp:yourlaunchpadid"
|
||||
echo " Multiple: gh:alice lp:alice"
|
||||
echo ""
|
||||
read -rp " Usernames (or Enter to skip): " SSH_INPUT
|
||||
if [[ -n "$SSH_INPUT" ]]; then
|
||||
read -ra SSH_IMPORT_IDS <<< "$SSH_INPUT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Q2: Storage for Ollama models ─────────────────────────────────────────────
|
||||
OLLAMA_STORAGE="volume" # "volume" = Docker named volume, else a host path
|
||||
OLLAMA_HOST_PATH=""
|
||||
|
||||
if $INSTALL_AI; then
|
||||
echo ""
|
||||
echo -e " ${BOLD}[2/6] Where should Ollama models be stored?${NC}"
|
||||
echo " (Models are large — 5-50GB each. A fast SSD or large HDD is ideal.)"
|
||||
echo ""
|
||||
echo " Available mount points with >20GB free:"
|
||||
df -h --output=target,avail,fstype 2>/dev/null \
|
||||
| awk 'NR>1 && $2~/[0-9]/ {
|
||||
val=$2; unit=substr(val,length(val));
|
||||
num=substr(val,1,length(val)-1)+0;
|
||||
if ((unit=="G" && num>=20) || unit=="T") print " " $0
|
||||
}' | head -10
|
||||
echo ""
|
||||
echo " 1) Docker volume (default — stored in /var/lib/docker/volumes/)"
|
||||
echo " 2) Custom path (e.g. /mnt/ssd/ollama or /data/ollama)"
|
||||
echo ""
|
||||
read -rp " Choice [1]: " STORAGE_CHOICE
|
||||
STORAGE_CHOICE="${STORAGE_CHOICE:-1}"
|
||||
if [[ "$STORAGE_CHOICE" == "2" ]]; then
|
||||
read -rp " Enter full path for Ollama models: " OLLAMA_HOST_PATH
|
||||
OLLAMA_HOST_PATH="${OLLAMA_HOST_PATH%/}" # strip trailing slash
|
||||
[[ -z "$OLLAMA_HOST_PATH" ]] && die "No path entered."
|
||||
OLLAMA_STORAGE="bind"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Q3: Storage for Kiwix ZIMs ────────────────────────────────────────────────
|
||||
KIWIX_DIR="$BASE/kiwix" # default
|
||||
|
||||
if $SVC_KIWIX; then
|
||||
echo ""
|
||||
echo -e " ${BOLD}[3/6] Where should Kiwix ZIM files be stored?${NC}"
|
||||
echo " (ZIMs are large — Wikipedia alone is ~46GB. Total collection ~130GB.)"
|
||||
echo ""
|
||||
echo " Available mount points with >50GB free:"
|
||||
df -h --output=target,avail,fstype 2>/dev/null \
|
||||
| awk 'NR>1 && $2~/[0-9]/ {
|
||||
val=$2; unit=substr(val,length(val));
|
||||
num=substr(val,1,length(val)-1)+0;
|
||||
if ((unit=="G" && num>=50) || unit=="T") print " " $0
|
||||
}' | head -10
|
||||
echo ""
|
||||
echo " Default: $KIWIX_DIR"
|
||||
read -rp " Press Enter to use default, or type a different path: " KIWIX_INPUT
|
||||
if [[ -n "$KIWIX_INPUT" ]]; then
|
||||
KIWIX_DIR="${KIWIX_INPUT%/}"
|
||||
fi
|
||||
|
||||
# ── Q4: Download ZIMs now? ─────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e " ${BOLD}[4/6] Download ZIM files? (offline Wikipedia, Stack Overflow, etc.)${NC}"
|
||||
echo " Downloads run in the background — safe to start now and leave overnight."
|
||||
echo " Total: ~130GB (Wikipedia 46GB, Project Gutenberg 60GB, others smaller)"
|
||||
echo ""
|
||||
echo " 1) Yes — download all ZIMs overnight (~130GB)"
|
||||
echo " 2) Select — choose which ZIMs to download"
|
||||
echo " 3) No — skip for now (run ./kiwix_download.sh later)"
|
||||
echo ""
|
||||
read -rp " Choice [3]: " ZIM_CHOICE
|
||||
ZIM_CHOICE="${ZIM_CHOICE:-3}"
|
||||
else
|
||||
ZIM_CHOICE="3"
|
||||
fi
|
||||
|
||||
# ── Q4b: Firewall (LAN subnet) ────────────────────────────────────────────────
|
||||
LAN_SUBNET="192.168.1.0/24"
|
||||
if command -v ufw &>/dev/null && { [[ ! -f "$BASE/.ufw-done" ]] || $FORCE; }; then
|
||||
echo ""
|
||||
# Auto-detect likely subnet from current IP
|
||||
AUTO_SUBNET=$(echo "$LOCAL_IP" | awk -F. '{print $1"."$2"."$3".0/24"}')
|
||||
echo -e " ${BOLD}[4b/6] Firewall — allow LAN access to services${NC}"
|
||||
read -rp " LAN subnet [${AUTO_SUBNET}]: " LAN_INPUT
|
||||
LAN_SUBNET="${LAN_INPUT:-$AUTO_SUBNET}"
|
||||
[[ "$LAN_SUBNET" =~ /[0-9]+$ ]] || LAN_SUBNET="${LAN_SUBNET}/24"
|
||||
fi
|
||||
|
||||
# ── Q5: Model selection wizard ────────────────────────────────────────────────
|
||||
PULL_MODELS=false
|
||||
REASON_MODEL=""
|
||||
|
||||
if $INSTALL_AI; then
|
||||
|
||||
# speed estimate based on Q4 model size vs available VRAM
|
||||
# no hard limits — just honest labels so the user can choose
|
||||
speed_label() {
|
||||
local mgb="$1" # approximate Q4 size in GB
|
||||
if [[ "$VRAM_GB" -eq 0 ]]; then
|
||||
printf "CPU only — very slow"
|
||||
elif (( mgb <= VRAM_GB )); then
|
||||
printf "✓ fast — fully in VRAM"
|
||||
elif (( mgb <= VRAM_GB + 2 )); then
|
||||
printf "~ good — fits with small overhang (~reading speed)"
|
||||
elif (( mgb <= VRAM_GB + 8 )); then
|
||||
printf "✗ slow — partial CPU offload"
|
||||
else
|
||||
printf "✗ very slow — heavy CPU offload"
|
||||
fi
|
||||
}
|
||||
|
||||
echo ""
|
||||
echo -e " ${BOLD}[5/6] Model selection${NC}"
|
||||
echo " GPU: ${GPU_NAME:-None} (${VRAM_GB}GB VRAM)"
|
||||
echo ""
|
||||
echo " Origin preference:"
|
||||
echo " 1) Western-only — Codestral (Mistral 🇫🇷) · Phi4 (Microsoft 🇺🇸) · Mistral 7B"
|
||||
echo " 2) Performance-first — Qwen2.5 · Qwen2.5-Coder (Chinese, top benchmarks)"
|
||||
echo " 3) Mixed — Western for chat/reasoning, Qwen for coding only"
|
||||
echo " 4) Custom — enter model names manually"
|
||||
echo ""
|
||||
read -rp " Choice [1]: " MODEL_PREF
|
||||
MODEL_PREF="${MODEL_PREF:-1}"
|
||||
|
||||
# Q4 approximate sizes in GB: 7B=4, 13-15B=9, 22B=13, 32B=19, 70-72B=41
|
||||
echo ""
|
||||
echo " Size tier — estimated speed on your ${VRAM_GB}GB GPU:"
|
||||
echo " (No hard limits — Ollama uses all available VRAM automatically)"
|
||||
echo ""
|
||||
printf " %-8s %-42s %s\n" "Tier" "Models" "Speed on your system"
|
||||
printf " %-8s %-42s %s\n" "--------" "------------------------------------------" "--------------------"
|
||||
|
||||
case "$MODEL_PREF" in
|
||||
1) # Western
|
||||
printf " %-8s %-42s %s\n" "7B" "mistral:7b + codellama:7b" "$(speed_label 4)"
|
||||
printf " %-8s %-42s %s\n" "14B" "phi4:14b + starcoder2:15b" "$(speed_label 9)"
|
||||
printf " %-8s %-42s %s\n" "22B" "phi4:14b + codestral:22b" "$(speed_label 13)"
|
||||
printf " %-8s %-42s %s\n" "70B" "llama3.3:70b + codestral:22b" "$(speed_label 41)"
|
||||
;;
|
||||
2) # Performance
|
||||
printf " %-8s %-42s %s\n" "7B" "qwen2.5:7b + qwen2.5-coder:7b" "$(speed_label 4)"
|
||||
printf " %-8s %-42s %s\n" "14B" "qwen2.5:14b + qwen2.5-coder:14b" "$(speed_label 9)"
|
||||
printf " %-8s %-42s %s\n" "32B" "qwen2.5:14b + qwen2.5-coder:32b" "$(speed_label 19)"
|
||||
printf " %-8s %-42s %s\n" "72B" "qwen2.5:72b + qwen2.5-coder:32b" "$(speed_label 41)"
|
||||
;;
|
||||
3) # Mixed
|
||||
printf " %-8s %-42s %s\n" "7B" "mistral:7b + qwen2.5-coder:7b" "$(speed_label 4)"
|
||||
printf " %-8s %-42s %s\n" "14B" "phi4:14b + qwen2.5-coder:14b" "$(speed_label 9)"
|
||||
printf " %-8s %-42s %s\n" "32B" "phi4:14b + qwen2.5-coder:32b" "$(speed_label 19)"
|
||||
printf " %-8s %-42s %s\n" "70B" "llama3.3:70b + qwen2.5-coder:32b" "$(speed_label 41)"
|
||||
;;
|
||||
esac
|
||||
|
||||
# VRAM-based recommendation (soft — shown as suggestion only)
|
||||
if [[ "$VRAM_GB" -ge 20 ]]; then REC_TIER="32B"
|
||||
elif [[ "$VRAM_GB" -ge 12 ]]; then REC_TIER="22B"
|
||||
elif [[ "$VRAM_GB" -ge 6 ]]; then REC_TIER="14B"
|
||||
else REC_TIER="7B"
|
||||
fi
|
||||
# performance pref has no 22B tier
|
||||
[[ "$MODEL_PREF" == "2" && "$REC_TIER" == "22B" ]] && REC_TIER="32B"
|
||||
|
||||
echo ""
|
||||
|
||||
if [[ "$MODEL_PREF" == "4" ]]; then
|
||||
# Custom — free-form entry
|
||||
echo " Current defaults: fast=$FAST_MODEL chat=$CHAT_MODEL code=$CODE_MODEL"
|
||||
echo " Press Enter on any line to keep the default shown."
|
||||
echo ""
|
||||
read -rp " Fast/chat model [$FAST_MODEL]: " _in; FAST_MODEL="${_in:-$FAST_MODEL}"
|
||||
read -rp " Smart chat model [$CHAT_MODEL]: " _in; CHAT_MODEL="${_in:-$CHAT_MODEL}"
|
||||
read -rp " Code model [$CODE_MODEL]: " _in; CODE_MODEL="${_in:-$CODE_MODEL}"
|
||||
read -rp " Reasoning model (Enter to skip): " REASON_MODEL
|
||||
else
|
||||
read -rp " Choose tier [$REC_TIER]: " TIER_PICK
|
||||
TIER_PICK="${TIER_PICK:-$REC_TIER}"
|
||||
|
||||
case "${MODEL_PREF}:${TIER_PICK}" in
|
||||
# Western
|
||||
1:7B) FAST_MODEL="mistral:7b"; CHAT_MODEL="mistral:7b"; CODE_MODEL="codellama:7b"; REASON_MODEL="" ;;
|
||||
1:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="starcoder2:15b"; REASON_MODEL="phi4:14b" ;;
|
||||
1:22B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="codestral:22b"; REASON_MODEL="phi4:14b" ;;
|
||||
1:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="codestral:22b"; REASON_MODEL="llama3.3:70b" ;;
|
||||
# Performance-first
|
||||
2:7B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:7b"; CODE_MODEL="qwen2.5-coder:7b"; REASON_MODEL="" ;;
|
||||
2:14B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:14b"; REASON_MODEL="deepseek-r1:14b" ;;
|
||||
2:32B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:14b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="deepseek-r1:14b" ;;
|
||||
2:72B) FAST_MODEL="qwen2.5:7b"; CHAT_MODEL="qwen2.5:72b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="deepseek-r1:14b" ;;
|
||||
# Mixed
|
||||
3:7B) FAST_MODEL="mistral:7b"; CHAT_MODEL="mistral:7b"; CODE_MODEL="qwen2.5-coder:7b"; REASON_MODEL="" ;;
|
||||
3:14B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen2.5-coder:14b"; REASON_MODEL="phi4:14b" ;;
|
||||
3:32B) FAST_MODEL="mistral:7b"; CHAT_MODEL="phi4:14b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="phi4:14b" ;;
|
||||
3:70B) FAST_MODEL="mistral:7b"; CHAT_MODEL="llama3.3:70b"; CODE_MODEL="qwen2.5-coder:32b"; REASON_MODEL="llama3.3:70b" ;;
|
||||
*)
|
||||
warn "Unrecognised tier '$TIER_PICK' — keeping detected defaults"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo " Models selected:"
|
||||
printf " %-16s %s\n" "Fast chat:" "$FAST_MODEL"
|
||||
printf " %-16s %s\n" "Smart chat:" "$CHAT_MODEL"
|
||||
printf " %-16s %s\n" "Code:" "$CODE_MODEL"
|
||||
[[ -n "$REASON_MODEL" ]] && printf " %-16s %s\n" "Reasoning:" "$REASON_MODEL"
|
||||
printf " %-16s %s\n" "Embed (RAG):" "$EMBED_MODEL"
|
||||
echo ""
|
||||
read -rp " Download these models now? [Y/n]: " DO_PULL
|
||||
[[ "${DO_PULL,,}" != "n" ]] && PULL_MODELS=true
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo -e " ${BOLD}Select components to install:${NC}"
|
||||
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo -e "${BOLD} Setup plan — starting now:${NC}"
|
||||
echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
|
||||
echo ""
|
||||
echo " 1) Full stack — AI stack + Kiwix"
|
||||
echo " 2) AI stack — Ollama · WebUI · RAG · MCP · ChromaDB · SearXNG · Gitea · InvokeAI · Portainer"
|
||||
echo " 3) Kiwix only — Offline Wikipedia / docs (runs standalone)"
|
||||
$INSTALL_POSTINSTALL && echo " ✓ Full system setup (ubuntu-post-install.sh — interactive)"
|
||||
[[ "${#SSH_IMPORT_IDS[@]}" -gt 0 ]] && echo " ✓ SSH keys → ${SSH_IMPORT_IDS[*]}"
|
||||
if $INSTALL_AI; then
|
||||
_svcs="Ollama"
|
||||
$SVC_WEBUI && _svcs+=" · Open WebUI"
|
||||
$SVC_RAG && _svcs+=" · RAG+ChromaDB"
|
||||
$SVC_MCP && _svcs+=" · MCP"
|
||||
$SVC_SEARXNG && _svcs+=" · SearXNG"
|
||||
$SVC_GITEA && _svcs+=" · Gitea"
|
||||
$SVC_INVOKEAI && _svcs+=" · InvokeAI"
|
||||
$SVC_PORTAINER && _svcs+=" · Portainer"
|
||||
$SVC_KIWIX && _svcs+=" · Kiwix"
|
||||
echo " ✓ AI stack → $_svcs"
|
||||
fi
|
||||
if $INSTALL_AI; then
|
||||
if [[ "$OLLAMA_STORAGE" == "bind" ]]; then
|
||||
echo " ✓ Ollama models → $OLLAMA_HOST_PATH"
|
||||
else
|
||||
echo " ✓ Ollama models → Docker volume (default)"
|
||||
fi
|
||||
fi
|
||||
$SVC_KIWIX && echo " ✓ Kiwix ZIMs → $KIWIX_DIR"
|
||||
$PULL_MODELS && echo " ✓ Pull models : $EMBED_MODEL + $FAST_MODEL + $CHAT_MODEL + $CODE_MODEL${REASON_MODEL:+ + $REASON_MODEL}"
|
||||
[[ "$ZIM_CHOICE" == "1" ]] && echo " ✓ Download all ZIMs in background (~130GB)"
|
||||
[[ "$ZIM_CHOICE" == "2" ]] && echo " ✓ Select ZIMs to download (prompted after stack starts)"
|
||||
echo ""
|
||||
read -rp " Choice [1]: " MENU_CHOICE
|
||||
MENU_CHOICE="${MENU_CHOICE:-1}"
|
||||
case "$MENU_CHOICE" in
|
||||
1) INSTALL_AI=true; INSTALL_KIWIX=true ;;
|
||||
2) INSTALL_AI=true; INSTALL_KIWIX=false ;;
|
||||
3) INSTALL_AI=false; INSTALL_KIWIX=true ;;
|
||||
*) die "Invalid choice '$MENU_CHOICE' — run again and choose 1, 2, or 3" ;;
|
||||
esac
|
||||
read -rp " Proceed? [Y/n]: " CONFIRM
|
||||
[[ "${CONFIRM,,}" == "n" ]] && echo "Aborted." && exit 0
|
||||
echo ""
|
||||
|
||||
# ── helper: write only if missing (or --force) ────────────────────────────────
|
||||
@@ -79,24 +468,47 @@ write_if_new() {
|
||||
fi
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Run full system post-install first (sets up Docker, security, base services)
|
||||
# =============================================================================
|
||||
if $INSTALL_POSTINSTALL; then
|
||||
section "Full System Setup"
|
||||
if [[ ! -f "$POSTINSTALL_SCRIPT" ]]; then
|
||||
die "ubuntu-post-install.sh not found at $POSTINSTALL_SCRIPT"
|
||||
fi
|
||||
info "Launching ubuntu-post-install.sh in normal install mode..."
|
||||
echo ""
|
||||
bash "$POSTINSTALL_SCRIPT"
|
||||
echo ""
|
||||
ok "System setup complete — continuing with selected components..."
|
||||
echo ""
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "Prerequisites"
|
||||
# =============================================================================
|
||||
if ! $IS_UPDATE; then
|
||||
|
||||
# Docker
|
||||
if ! command -v docker &>/dev/null; then
|
||||
info "Installing Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker "$USER"
|
||||
ok "Docker installed — log out/in or run: newgrp docker"
|
||||
else
|
||||
ok "Docker: $(docker --version)"
|
||||
fi
|
||||
# ── Docker (always check — required whether new install or update) ─────────────
|
||||
if ! command -v docker &>/dev/null; then
|
||||
info "Installing Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker "$USER"
|
||||
warn "Added $USER to docker group — changes take effect on next login."
|
||||
warn "If docker commands fail, run: newgrp docker"
|
||||
else
|
||||
ok "Docker: $(docker --version | sed 's/Docker version //')"
|
||||
fi
|
||||
|
||||
# NVIDIA Container Toolkit
|
||||
if command -v nvidia-smi &>/dev/null && \
|
||||
! dpkg -l 2>/dev/null | grep -q nvidia-container-toolkit; then
|
||||
# Ensure Docker Compose plugin is present (included with modern Docker)
|
||||
if ! docker compose version &>/dev/null; then
|
||||
info "Installing docker-compose-plugin..."
|
||||
sudo apt-get install -y docker-compose-plugin
|
||||
fi
|
||||
ok "Docker Compose: $(docker compose version --short 2>/dev/null || echo 'ok')"
|
||||
|
||||
# ── NVIDIA Container Toolkit ───────────────────────────────────────────────────
|
||||
if command -v nvidia-smi &>/dev/null; then
|
||||
if ! dpkg -l 2>/dev/null | grep -q nvidia-container-toolkit; then
|
||||
info "Installing NVIDIA Container Toolkit..."
|
||||
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
|
||||
| sudo gpg --dearmor --yes \
|
||||
@@ -109,30 +521,46 @@ if ! $IS_UPDATE; then
|
||||
sudo nvidia-ctk runtime configure --runtime=docker
|
||||
sudo systemctl restart docker
|
||||
ok "NVIDIA Container Toolkit installed"
|
||||
elif command -v nvidia-smi &>/dev/null; then
|
||||
ok "NVIDIA Container Toolkit already present"
|
||||
else
|
||||
warn "No NVIDIA GPU detected — CPU-only (Ollama will use CPU)"
|
||||
ok "NVIDIA Container Toolkit: already present"
|
||||
fi
|
||||
|
||||
# ripgrep (used by MCP search_code tool)
|
||||
if ! command -v rg &>/dev/null; then
|
||||
sudo apt-get install -y ripgrep
|
||||
ok "ripgrep installed"
|
||||
fi
|
||||
|
||||
else
|
||||
ok "Skipping prereq checks (update mode)"
|
||||
info "No NVIDIA GPU detected — Ollama will run on CPU"
|
||||
fi
|
||||
|
||||
# ── ripgrep (used by MCP search_code tool) ─────────────────────────────────────
|
||||
if ! command -v rg &>/dev/null; then
|
||||
sudo apt-get install -y ripgrep
|
||||
ok "ripgrep installed"
|
||||
fi
|
||||
|
||||
# ── SSH key import from GitHub / Launchpad ─────────────────────────────────────
|
||||
if [[ "${#SSH_IMPORT_IDS[@]}" -gt 0 ]]; then
|
||||
section "SSH Keys"
|
||||
if ! command -v ssh-import-id &>/dev/null; then
|
||||
info "Installing ssh-import-id..."
|
||||
sudo apt-get install -y ssh-import-id
|
||||
fi
|
||||
mkdir -p "$HOME/.ssh"
|
||||
chmod 700 "$HOME/.ssh"
|
||||
for id in "${SSH_IMPORT_IDS[@]}"; do
|
||||
info "Importing SSH keys: $id"
|
||||
ssh-import-id "$id" && ok "Keys imported: $id" || warn "Failed to import: $id"
|
||||
done
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
section "Directories"
|
||||
# =============================================================================
|
||||
for d in papers repos workspace index searxng invokeai-data invokeai-outputs \
|
||||
kiwix gitea portainer-data logs; do
|
||||
gitea portainer-data logs; do
|
||||
mkdir -p "$BASE/$d"
|
||||
done
|
||||
ok "Directories ready under $BASE"
|
||||
# Kiwix ZIM dir — may be on a different drive
|
||||
mkdir -p "$KIWIX_DIR"
|
||||
# Ollama bind-mount dir (if using custom path)
|
||||
[[ "$OLLAMA_STORAGE" == "bind" ]] && mkdir -p "$OLLAMA_HOST_PATH"
|
||||
ok "Directories ready"
|
||||
|
||||
# =============================================================================
|
||||
if $INSTALL_AI; then
|
||||
@@ -209,6 +637,15 @@ fi
|
||||
# =============================================================================
|
||||
section "Docker Compose"
|
||||
# =============================================================================
|
||||
# Build ollama volume line based on storage choice
|
||||
if [[ "$OLLAMA_STORAGE" == "bind" ]]; then
|
||||
OLLAMA_VOLUME_LINE=" - ${OLLAMA_HOST_PATH}:/root/.ollama"
|
||||
OLLAMA_VOLUMES_DECL=""
|
||||
else
|
||||
OLLAMA_VOLUME_LINE=" - ollama-models:/root/.ollama"
|
||||
OLLAMA_VOLUMES_DECL=" ollama-models:"
|
||||
fi
|
||||
|
||||
# Always written — it's the stack definition and safe to update
|
||||
cat > "$BASE/docker-compose.yml" << COMPOSE
|
||||
# Local AI Stack — generated $(date '+%Y-%m-%d')
|
||||
@@ -225,7 +662,7 @@ services:
|
||||
ports:
|
||||
- "0.0.0.0:11434:11434"
|
||||
volumes:
|
||||
- ollama-models:/root/.ollama
|
||||
${OLLAMA_VOLUME_LINE}
|
||||
environment:
|
||||
- OLLAMA_NUM_GPU=999 # use all available VRAM (auto-detects GPU size)
|
||||
- OLLAMA_NUM_CTX=8192 # lower to 4096 if you hit OOM
|
||||
@@ -353,7 +790,6 @@ services:
|
||||
cap_add: [CHOWN, SETGID, SETUID]
|
||||
|
||||
# ── Kiwix — Offline Wikipedia/docs ─────────────────────────────────────────
|
||||
# Download ZIMs first: ./kiwix_download.sh
|
||||
kiwix:
|
||||
image: ghcr.io/kiwix/kiwix-serve:latest
|
||||
container_name: kiwix
|
||||
@@ -361,7 +797,7 @@ services:
|
||||
ports:
|
||||
- "0.0.0.0:8181:8080"
|
||||
volumes:
|
||||
- $BASE/kiwix:/data
|
||||
- $KIWIX_DIR:/data
|
||||
command: "*.zim"
|
||||
|
||||
# ── Gitea — Self-hosted Git ─────────────────────────────────────────────────
|
||||
@@ -419,7 +855,7 @@ services:
|
||||
- $BASE/portainer-data:/data
|
||||
|
||||
volumes:
|
||||
ollama-models:
|
||||
${OLLAMA_VOLUMES_DECL}
|
||||
open-webui-data:
|
||||
invokeai-models:
|
||||
COMPOSE
|
||||
@@ -430,10 +866,6 @@ section "Firewall (UFW)"
|
||||
# =============================================================================
|
||||
if command -v ufw &>/dev/null; then
|
||||
if [[ ! -f "$BASE/.ufw-done" ]] || $FORCE; then
|
||||
read -rp " LAN subnet for firewall [192.168.1.0/24]: " LAN_SUBNET
|
||||
LAN_SUBNET="${LAN_SUBNET:-192.168.1.0/24}"
|
||||
[[ "$LAN_SUBNET" =~ /[0-9]+$ ]] || LAN_SUBNET="${LAN_SUBNET}/24"
|
||||
|
||||
for port_comment in \
|
||||
"3000:Open WebUI" "11434:Ollama" "8001:RAG Server" \
|
||||
"8002:MCP Server" "8000:ChromaDB" "8888:SearXNG" \
|
||||
@@ -458,6 +890,23 @@ fi
|
||||
section "Helper Scripts"
|
||||
# =============================================================================
|
||||
|
||||
# Build URL list for start.sh (only include selected services)
|
||||
_START_URLS=""
|
||||
$SVC_WEBUI && _START_URLS+=$'echo " Open WebUI → http://'"$LOCAL_IP"$':3000"\n'
|
||||
$SVC_INVOKEAI && _START_URLS+=$'echo " InvokeAI → http://'"$LOCAL_IP"$':9090"\n'
|
||||
$SVC_SEARXNG && _START_URLS+=$'echo " SearXNG → http://'"$LOCAL_IP"$':8888"\n'
|
||||
$SVC_GITEA && _START_URLS+=$'echo " Gitea → http://'"$LOCAL_IP"$':3001"\n'
|
||||
$SVC_RAG && _START_URLS+=$'echo " RAG Health → http://'"$LOCAL_IP"$':8001/health"\n'
|
||||
$SVC_MCP && _START_URLS+=$'echo " MCP SSE → http://'"$LOCAL_IP"$':8002/sse"\n'
|
||||
$SVC_PORTAINER && _START_URLS+=$'echo " Portainer → https://'"$LOCAL_IP"$':9443"\n'
|
||||
$SVC_KIWIX && _START_URLS+=$'echo " Kiwix → http://'"$LOCAL_IP"$':8181"\n'
|
||||
_START_MCP=""
|
||||
$SVC_MCP && _START_MCP=$'echo ""\necho " Claude Code MCP:"\necho " claude mcp add local http://'"$LOCAL_IP"$':8002/sse"\n'
|
||||
_START_PDFS=""
|
||||
$SVC_RAG && _START_PDFS=$'echo " Drop PDFs → '"$BASE"$'/papers/"\n'
|
||||
_START_IMGS=""
|
||||
$SVC_INVOKEAI && _START_IMGS=$'echo " Images out → '"$BASE"$'/invokeai-outputs/"\n'
|
||||
|
||||
cat > "$BASE/start.sh" << STARTSH
|
||||
#!/bin/bash
|
||||
cd "$BASE"
|
||||
@@ -465,21 +914,9 @@ echo "Pulling latest images..."
|
||||
docker compose pull --quiet
|
||||
docker compose up -d
|
||||
echo ""
|
||||
echo " Open WebUI → http://$LOCAL_IP:3000"
|
||||
echo " InvokeAI → http://$LOCAL_IP:9090"
|
||||
echo " SearXNG → http://$LOCAL_IP:8888"
|
||||
echo " Kiwix → http://$LOCAL_IP:8181"
|
||||
echo " Gitea → http://$LOCAL_IP:3001"
|
||||
echo " RAG Health → http://$LOCAL_IP:8001/health"
|
||||
echo " MCP SSE → http://$LOCAL_IP:8002/sse"
|
||||
echo " Portainer → https://$LOCAL_IP:9443"
|
||||
echo ""
|
||||
$_START_URLS
|
||||
echo " Workspace → $BASE/workspace/"
|
||||
echo " Drop PDFs → $BASE/papers/"
|
||||
echo " Images out → $BASE/invokeai-outputs/"
|
||||
echo ""
|
||||
echo " Claude Code MCP:"
|
||||
echo " claude mcp add local http://$LOCAL_IP:8002/sse"
|
||||
$_START_PDFS$_START_IMGS$_START_MCP
|
||||
STARTSH
|
||||
chmod +x "$BASE/start.sh"
|
||||
ok "start.sh"
|
||||
@@ -525,25 +962,37 @@ ok "status.sh"
|
||||
|
||||
cat > "$BASE/pull-models.sh" << PULLSH
|
||||
#!/bin/bash
|
||||
# Models chosen at install time — re-run setup to change selection
|
||||
EMBED_MODEL="$EMBED_MODEL"
|
||||
FAST_MODEL="$FAST_MODEL"
|
||||
CHAT_MODEL="$CHAT_MODEL"
|
||||
CODE_MODEL="$CODE_MODEL"
|
||||
REASON_MODEL="$REASON_MODEL"
|
||||
|
||||
echo "Waiting for Ollama..."
|
||||
until docker exec ollama ollama list &>/dev/null; do sleep 3; done
|
||||
echo "Ollama ready."
|
||||
|
||||
echo ""
|
||||
echo "Pulling embed model (needed for RAG)..."
|
||||
docker exec ollama ollama pull $EMBED_MODEL
|
||||
echo "Pulling embed model (RAG — required)..."
|
||||
docker exec ollama ollama pull "\$EMBED_MODEL"
|
||||
|
||||
echo "Pulling fast chat model (~5GB)..."
|
||||
docker exec ollama ollama pull $FAST_MODEL
|
||||
echo "Pulling fast chat model..."
|
||||
docker exec ollama ollama pull "\$FAST_MODEL"
|
||||
|
||||
echo "Pulling smart chat model (~9GB)..."
|
||||
docker exec ollama ollama pull $CHAT_MODEL
|
||||
echo "Pulling smart chat model..."
|
||||
[[ "\$CHAT_MODEL" != "\$FAST_MODEL" ]] && docker exec ollama ollama pull "\$CHAT_MODEL"
|
||||
|
||||
echo "Pulling code model (~5GB)..."
|
||||
docker exec ollama ollama pull $CODE_MODEL
|
||||
echo "Pulling code model..."
|
||||
docker exec ollama ollama pull "\$CODE_MODEL"
|
||||
|
||||
if [[ -n "\$REASON_MODEL" && "\$REASON_MODEL" != "\$CHAT_MODEL" ]]; then
|
||||
echo "Pulling reasoning model..."
|
||||
docker exec ollama ollama pull "\$REASON_MODEL"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done. Models:"
|
||||
echo "Done. Installed models:"
|
||||
docker exec ollama ollama list
|
||||
PULLSH
|
||||
chmod +x "$BASE/pull-models.sh"
|
||||
@@ -599,8 +1048,17 @@ cd "$BASE"
|
||||
info "Pulling images (this takes a few minutes on first run)..."
|
||||
|
||||
COMPOSE_SERVICES=""
|
||||
$INSTALL_AI && COMPOSE_SERVICES+=" ollama open-webui chromadb rag-server mcp-server searxng gitea invokeai portainer"
|
||||
$INSTALL_KIWIX && COMPOSE_SERVICES+=" kiwix"
|
||||
if $INSTALL_AI; then
|
||||
COMPOSE_SERVICES="ollama"
|
||||
$SVC_WEBUI && COMPOSE_SERVICES+=" open-webui"
|
||||
$SVC_RAG && COMPOSE_SERVICES+=" chromadb rag-server"
|
||||
$SVC_MCP && COMPOSE_SERVICES+=" mcp-server"
|
||||
$SVC_SEARXNG && COMPOSE_SERVICES+=" searxng"
|
||||
$SVC_GITEA && COMPOSE_SERVICES+=" gitea"
|
||||
$SVC_INVOKEAI && COMPOSE_SERVICES+=" invokeai"
|
||||
$SVC_PORTAINER && COMPOSE_SERVICES+=" portainer"
|
||||
$SVC_KIWIX && COMPOSE_SERVICES+=" kiwix"
|
||||
fi
|
||||
|
||||
# shellcheck disable=SC2086
|
||||
docker compose pull --quiet $COMPOSE_SERVICES
|
||||
@@ -608,14 +1066,153 @@ docker compose pull --quiet $COMPOSE_SERVICES
|
||||
docker compose up -d $COMPOSE_SERVICES
|
||||
ok "Stack started"
|
||||
|
||||
if $INSTALL_AI && ! $IS_UPDATE; then
|
||||
echo ""
|
||||
read -rp "Pull Ollama models now? (~20GB total, takes 10-40 min) [Y/n]: " DO_PULL
|
||||
if [[ "${DO_PULL,,}" != "n" ]]; then
|
||||
bash "$BASE/pull-models.sh"
|
||||
else
|
||||
info "Run later: bash $BASE/pull-models.sh"
|
||||
# ── Pull Ollama models (answer was captured upfront) ─────────────────────────
|
||||
if $INSTALL_AI && $PULL_MODELS; then
|
||||
section "Pulling Ollama Models"
|
||||
info "Waiting for Ollama to be ready..."
|
||||
until docker exec ollama ollama list &>/dev/null; do sleep 3; done
|
||||
ok "Ollama ready."
|
||||
|
||||
info "Pulling embed model (required for RAG)..."
|
||||
docker exec ollama ollama pull "$EMBED_MODEL"
|
||||
|
||||
info "Pulling fast chat model..."
|
||||
docker exec ollama ollama pull "$FAST_MODEL"
|
||||
|
||||
info "Pulling smart chat model..."
|
||||
docker exec ollama ollama pull "$CHAT_MODEL"
|
||||
|
||||
info "Pulling code model..."
|
||||
docker exec ollama ollama pull "$CODE_MODEL"
|
||||
|
||||
if [[ -n "$REASON_MODEL" ]]; then
|
||||
info "Pulling reasoning model ($REASON_MODEL)..."
|
||||
docker exec ollama ollama pull "$REASON_MODEL"
|
||||
fi
|
||||
|
||||
ok "All models pulled."
|
||||
echo ""
|
||||
docker exec ollama ollama list
|
||||
elif $INSTALL_AI; then
|
||||
info "Skipping model pull — run later: bash $BASE/pull-models.sh"
|
||||
fi
|
||||
|
||||
# ── Start ZIM downloads (answer was captured upfront) ────────────────────────
|
||||
if $SVC_KIWIX && [[ "$ZIM_CHOICE" != "3" ]]; then
|
||||
section "Starting ZIM Downloads"
|
||||
LOG="$BASE/logs/kiwix-download.log"
|
||||
mkdir -p "$(dirname "$LOG")"
|
||||
|
||||
info "Checking mirrors for latest ZIM filenames..."
|
||||
|
||||
MIRROR="https://ftp.fau.de/kiwix/zim"
|
||||
MIRROR2="https://download.kiwix.org/zim"
|
||||
|
||||
latest_zim() {
|
||||
local base_url="$1" pattern="$2"
|
||||
curl -s "$base_url/" | grep -oP "${pattern}_[0-9-]+\.zim" | sort -u | tail -1
|
||||
}
|
||||
|
||||
dl_zim() {
|
||||
local cat="$1" file="$2" desc="$3" mirror="${4:-$MIRROR}"
|
||||
local dest="$KIWIX_DIR/$file"
|
||||
if [[ -z "$file" ]]; then warn "Could not find $desc — skipping"; return; fi
|
||||
if [[ -f "$dest" ]]; then ok "$desc already downloaded"; return; fi
|
||||
info "Starting: $desc"
|
||||
nohup wget -c "$mirror/$cat/$file" -O "$dest" >> "$LOG" 2>&1 &
|
||||
echo $! >> "$KIWIX_DIR/.download_pids"
|
||||
ok "Download started (PID $!) — $desc"
|
||||
}
|
||||
|
||||
WIKI=$(latest_zim "$MIRROR/wikipedia" "wikipedia_en_all_nopic")
|
||||
SO=$(latest_zim "$MIRROR/stack_exchange" "stackoverflow.com_en_all")
|
||||
ARCH=$(latest_zim "$MIRROR/other" "archlinux_en_all_maxi")
|
||||
WIKT=$(latest_zim "$MIRROR/wiktionary" "wiktionary_en_all_nopic")
|
||||
WIKB=$(latest_zim "$MIRROR/wikibooks" "wikibooks_en_all_nopic")
|
||||
WIKS=$(latest_zim "$MIRROR/wikisource" "wikisource_en_all_nopic")
|
||||
WIKV=$(latest_zim "$MIRROR/wikivoyage" "wikivoyage_en_all_nopic")
|
||||
WIKUNI=$(latest_zim "$MIRROR/wikiversity" "wikiversity_en_all_nopic")
|
||||
WIKNEWS=$(latest_zim "$MIRROR/wikinews" "wikinews_en_all_nopic")
|
||||
WIKQ=$(latest_zim "$MIRROR/wikiquote" "wikiquote_en_all_nopic")
|
||||
VIKIDIA=$(latest_zim "$MIRROR/vikidia" "vikidia_en_all_nopic")
|
||||
TED=$(latest_zim "$MIRROR/ted" "ted_mul_youth")
|
||||
PHET=$(latest_zim "$MIRROR/phet" "phet_en_all")
|
||||
DEVDOCS=$(latest_zim "$MIRROR/devdocs" "devdocs_en_zig")
|
||||
FCC=$(latest_zim "$MIRROR/freecodecamp" "freecodecamp_en_all")
|
||||
IFIX=$(latest_zim "$MIRROR/ifixit" "ifixit_en_all")
|
||||
LIBRE=$(latest_zim "$MIRROR/libretexts" "libretexts.org_en_workforce")
|
||||
GUT=$(latest_zim "$MIRROR2/gutenberg" "gutenberg_en_all")
|
||||
|
||||
if [[ "$ZIM_CHOICE" == "2" ]]; then
|
||||
# Let user select which ZIMs
|
||||
echo ""
|
||||
echo " Select ZIMs to download (enter numbers separated by spaces):"
|
||||
echo " 1) Wikipedia ~46GB ${WIKI:-NOT FOUND}"
|
||||
echo " 2) Stack Overflow ~3GB ${SO:-NOT FOUND}"
|
||||
echo " 3) Arch Linux Wiki ~30MB ${ARCH:-NOT FOUND}"
|
||||
echo " 4) Wiktionary ~2GB ${WIKT:-NOT FOUND}"
|
||||
echo " 5) Wikibooks ~500MB ${WIKB:-NOT FOUND}"
|
||||
echo " 6) Wikisource ~4GB ${WIKS:-NOT FOUND}"
|
||||
echo " 7) Wikivoyage ~200MB ${WIKV:-NOT FOUND}"
|
||||
echo " 8) Wikiversity ~500MB ${WIKUNI:-NOT FOUND}"
|
||||
echo " 9) WikiNews ~300MB ${WIKNEWS:-NOT FOUND}"
|
||||
echo " 10) Wikiquote ~300MB ${WIKQ:-NOT FOUND}"
|
||||
echo " 11) Vikidia (kids K-8) ~66MB ${VIKIDIA:-NOT FOUND}"
|
||||
echo " 12) TED Talks ~5GB ${TED:-NOT FOUND}"
|
||||
echo " 13) PhET Simulations ~500MB ${PHET:-NOT FOUND}"
|
||||
echo " 14) DevDocs ~1GB ${DEVDOCS:-NOT FOUND}"
|
||||
echo " 15) FreeCodeCamp ~small ${FCC:-NOT FOUND}"
|
||||
echo " 16) iFixit ~2GB ${IFIX:-NOT FOUND}"
|
||||
echo " 17) LibreTexts ~varies ${LIBRE:-NOT FOUND}"
|
||||
echo " 18) Project Gutenberg ~60GB ${GUT:-NOT FOUND}"
|
||||
echo ""
|
||||
read -rp " Your choices (e.g. 1 2 3): " ZIM_PICKS
|
||||
rm -f "$KIWIX_DIR/.download_pids"
|
||||
for n in $ZIM_PICKS; do
|
||||
case "$n" in
|
||||
1) dl_zim "wikipedia" "$WIKI" "Wikipedia" ;;
|
||||
2) dl_zim "stack_exchange" "$SO" "Stack Overflow" ;;
|
||||
3) dl_zim "other" "$ARCH" "Arch Linux Wiki" ;;
|
||||
4) dl_zim "wiktionary" "$WIKT" "Wiktionary" ;;
|
||||
5) dl_zim "wikibooks" "$WIKB" "Wikibooks" ;;
|
||||
6) dl_zim "wikisource" "$WIKS" "Wikisource" ;;
|
||||
7) dl_zim "wikivoyage" "$WIKV" "Wikivoyage" ;;
|
||||
8) dl_zim "wikiversity" "$WIKUNI" "Wikiversity" ;;
|
||||
9) dl_zim "wikinews" "$WIKNEWS" "WikiNews" ;;
|
||||
10) dl_zim "wikiquote" "$WIKQ" "Wikiquote" ;;
|
||||
11) dl_zim "vikidia" "$VIKIDIA" "Vikidia (kids K-8)" ;;
|
||||
12) dl_zim "ted" "$TED" "TED Talks" ;;
|
||||
13) dl_zim "phet" "$PHET" "PhET Simulations" ;;
|
||||
14) dl_zim "devdocs" "$DEVDOCS" "DevDocs" ;;
|
||||
15) dl_zim "freecodecamp" "$FCC" "FreeCodeCamp" ;;
|
||||
16) dl_zim "ifixit" "$IFIX" "iFixit" ;;
|
||||
17) dl_zim "libretexts" "$LIBRE" "LibreTexts" ;;
|
||||
18) dl_zim "gutenberg" "$GUT" "Project Gutenberg" "$MIRROR2" ;;
|
||||
esac
|
||||
done
|
||||
else
|
||||
# Download all
|
||||
rm -f "$KIWIX_DIR/.download_pids"
|
||||
dl_zim "wikipedia" "$WIKI" "Wikipedia"
|
||||
dl_zim "stack_exchange" "$SO" "Stack Overflow"
|
||||
dl_zim "other" "$ARCH" "Arch Linux Wiki"
|
||||
dl_zim "wiktionary" "$WIKT" "Wiktionary"
|
||||
dl_zim "wikibooks" "$WIKB" "Wikibooks"
|
||||
dl_zim "wikisource" "$WIKS" "Wikisource"
|
||||
dl_zim "wikivoyage" "$WIKV" "Wikivoyage"
|
||||
dl_zim "wikiversity" "$WIKUNI" "Wikiversity"
|
||||
dl_zim "wikinews" "$WIKNEWS" "WikiNews"
|
||||
dl_zim "wikiquote" "$WIKQ" "Wikiquote"
|
||||
dl_zim "vikidia" "$VIKIDIA" "Vikidia (kids K-8)"
|
||||
dl_zim "ted" "$TED" "TED Talks"
|
||||
dl_zim "phet" "$PHET" "PhET Simulations"
|
||||
dl_zim "devdocs" "$DEVDOCS" "DevDocs"
|
||||
dl_zim "freecodecamp" "$FCC" "FreeCodeCamp"
|
||||
dl_zim "ifixit" "$IFIX" "iFixit"
|
||||
dl_zim "libretexts" "$LIBRE" "LibreTexts"
|
||||
dl_zim "gutenberg" "$GUT" "Project Gutenberg" "$MIRROR2"
|
||||
fi
|
||||
ok "ZIM downloads running in background — monitor: tail -f $LOG"
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
@@ -623,28 +1220,27 @@ echo ""
|
||||
echo -e "${GREEN}${BOLD}━━━ Done! ━━━${NC}"
|
||||
echo ""
|
||||
if $INSTALL_AI; then
|
||||
echo -e " ${CYAN}Open WebUI${NC} → http://$LOCAL_IP:3000"
|
||||
echo -e " ${CYAN}InvokeAI${NC} → http://$LOCAL_IP:9090"
|
||||
echo -e " ${CYAN}SearXNG${NC} → http://$LOCAL_IP:8888"
|
||||
echo -e " ${CYAN}Gitea${NC} → http://$LOCAL_IP:3001"
|
||||
echo -e " ${CYAN}RAG Health${NC} → http://$LOCAL_IP:8001/health"
|
||||
echo -e " ${CYAN}MCP SSE${NC} → http://$LOCAL_IP:8002/sse"
|
||||
echo -e " ${CYAN}Portainer${NC} → https://$LOCAL_IP:9443"
|
||||
fi
|
||||
if $INSTALL_KIWIX; then
|
||||
echo -e " ${CYAN}Kiwix${NC} → http://$LOCAL_IP:8181 (needs ZIMs — run kiwix_download.sh)"
|
||||
$SVC_WEBUI && echo -e " ${CYAN}Open WebUI${NC} → http://$LOCAL_IP:3000"
|
||||
$SVC_INVOKEAI && echo -e " ${CYAN}InvokeAI${NC} → http://$LOCAL_IP:9090"
|
||||
$SVC_SEARXNG && echo -e " ${CYAN}SearXNG${NC} → http://$LOCAL_IP:8888"
|
||||
$SVC_GITEA && echo -e " ${CYAN}Gitea${NC} → http://$LOCAL_IP:3001"
|
||||
$SVC_RAG && echo -e " ${CYAN}RAG Health${NC} → http://$LOCAL_IP:8001/health"
|
||||
$SVC_MCP && echo -e " ${CYAN}MCP SSE${NC} → http://$LOCAL_IP:8002/sse"
|
||||
$SVC_PORTAINER && echo -e " ${CYAN}Portainer${NC} → https://$LOCAL_IP:9443"
|
||||
$SVC_KIWIX && echo -e " ${CYAN}Kiwix${NC} → http://$LOCAL_IP:8181"
|
||||
fi
|
||||
echo ""
|
||||
if $INSTALL_AI; then
|
||||
echo -e " ${YELLOW}Add MCP to Claude Code:${NC}"
|
||||
echo " claude mcp add local http://$LOCAL_IP:8002/sse"
|
||||
echo ""
|
||||
$SVC_MCP && { echo -e " ${YELLOW}Add MCP to Claude Code:${NC}"; echo " claude mcp add local http://$LOCAL_IP:8002/sse"; echo ""; }
|
||||
echo -e " ${YELLOW}Add API tokens to:${NC} $BASE/.env"
|
||||
echo -e " ${YELLOW}Drop PDFs into:${NC} $BASE/papers/"
|
||||
$SVC_RAG && echo -e " ${YELLOW}Drop PDFs into:${NC} $BASE/papers/"
|
||||
echo -e " ${YELLOW}Your workspace:${NC} $BASE/workspace/"
|
||||
fi
|
||||
if $INSTALL_KIWIX; then
|
||||
echo -e " ${YELLOW}ZIM downloads:${NC} ./kiwix_download.sh"
|
||||
if $SVC_KIWIX && [[ "$ZIM_CHOICE" == "3" ]]; then
|
||||
echo -e " ${YELLOW}ZIM downloads:${NC} ./kiwix_download.sh (not started)"
|
||||
elif $SVC_KIWIX; then
|
||||
echo -e " ${YELLOW}ZIM progress:${NC} tail -f $BASE/logs/kiwix-download.log"
|
||||
echo -e " ${YELLOW}ZIM location:${NC} $KIWIX_DIR/"
|
||||
fi
|
||||
echo ""
|
||||
$IS_UPDATE && echo -e " ${GREEN}Update complete.${NC}" \
|
||||
|
||||
Reference in New Issue
Block a user