Files
ubuntu-post-install/ai-stack/configure-storage.sh
T
Claude c6576178b4 ai-stack: vendor the local-ai full AI stack as a new service
Vendor the functional source of github.com/outis1one/local-ai into
./ai-stack (22 files) and add services/ai-stack.sh, which copies the
source to ~/docker/ai-stack and hands off to the app's VRAM-aware
installer (local-ai-setup.sh). The stack bundles Ollama, Open WebUI,
RAG + MCP servers, ChromaDB, SearXNG, Kiwix, Gitea, InvokeAI, ComfyUI
and Portainer.

Cloud LLM providers (Groq/DeepInfra/OpenAI/OpenRouter) are optionally
wired into Open WebUI via the plural OPENAI_API_BASE_URLS list, with the
local RAG connection kept as the first entry so RAG keeps working. Open
WebUI ships built-in auth, so Caddy is configured without Authelia.

Excludes the upstream's two bundled copies of this very project
(ubuntu-post-install.sh, ubuntu-post-install-main.zip) — stale and
circular. Coexists with the existing ai-gpu service.

Also fix the install-function names for ai-gpu and ai-stack: the
dispatcher calls install_<raw-name>, so the function must be
install_ai-gpu / install_ai-stack (hyphen), matching the working
mail-archiver / wg-easy services. ai-gpu was previously uninstallable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Nb2vJ8W7bHKx1JXVvpCraH
2026-06-26 12:41:44 +00:00

240 lines
9.2 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# configure-storage.sh — interactively assign drives for ZIM and model storage
# then patches docker-compose.yml in place
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${CYAN}[..]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[!!]${NC} $*"; }
die() { echo -e "${RED}[ERR]${NC} $*"; exit 1; }
BASE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
COMPOSE="$BASE/docker-compose.yml"
[[ -f "$COMPOSE" ]] || die "docker-compose.yml not found at $BASE — run local-ai-setup.sh first"
# ── collect drives ─────────────────────────────────────────────────────────────
echo -e "\n${BOLD}━━━ Detected Drives ━━━${NC}\n"
# build array of mountpoints excluding tiny/system ones
mapfile -t MOUNTS < <(
df -h --output=target,size,avail,pcent 2>/dev/null \
| tail -n +2 \
| grep -v -E '^\s*(/$|/boot|/sys|/proc|/dev|/run|/snap|/var/lib/docker|/etc|tmpfs)' \
| awk '$3 ~ /[0-9]/ {print}' \
| sort -u
)
if [[ ${#MOUNTS[@]} -eq 0 ]]; then
warn "No additional mounted drives found beyond system disk."
warn "Mount your drives first (e.g. /mnt/storage), then re-run this script."
echo ""
echo "Quick mount example:"
echo " sudo mkdir -p /mnt/storage"
echo " sudo mount /dev/sdb1 /mnt/storage"
echo " # To make permanent, add to /etc/fstab"
exit 0
fi
# display numbered list
echo -e " ${BOLD}# Mount Point Size Free Used${NC}"
echo " ─────────────────────────────────────────────────────"
IDX=0
declare -a MOUNT_PATHS
for line in "${MOUNTS[@]}"; do
mp=$(echo "$line" | awk '{print $1}')
sz=$(echo "$line" | awk '{print $2}')
av=$(echo "$line" | awk '{print $3}')
pc=$(echo "$line" | awk '{print $4}')
printf " ${CYAN}%-3d${NC} %-30s %-7s %-7s %s\n" "$IDX" "$mp" "$sz" "$av" "$pc"
MOUNT_PATHS[$IDX]="$mp"
(( IDX++ ))
done
echo ""
pick_drive() {
local purpose="$1"
local varname="$2"
local skip_ok="${3:-true}"
echo -e "${BOLD}$purpose storage${NC}"
if $skip_ok; then
read -rp " Enter drive number (or Enter to skip): " choice
else
read -rp " Enter drive number: " choice
fi
if [[ -z "$choice" ]]; then
eval "$varname=''"
info "Skipping $purpose storage."
elif [[ "$choice" =~ ^[0-9]+$ ]] && [[ "$choice" -lt "$IDX" ]]; then
eval "$varname='${MOUNT_PATHS[$choice]}'"
ok "Selected: ${MOUNT_PATHS[$choice]}"
else
warn "Invalid choice — skipping $purpose."
eval "$varname=''"
fi
echo ""
}
# ── ZIM storage ────────────────────────────────────────────────────────────────
echo -e "${BOLD}━━━ Kiwix ZIM Storage ━━━${NC}\n"
echo " ZIM files can be large (Wikipedia alone is ~100GB)."
echo " Pick a drive with plenty of space."
echo ""
pick_drive "ZIM" ZIM_DRIVE
ZIM_PATH=""
if [[ -n "$ZIM_DRIVE" ]]; then
read -rp " Folder name on that drive [zims]: " ZIM_FOLDER
ZIM_FOLDER="${ZIM_FOLDER:-zims}"
ZIM_PATH="$ZIM_DRIVE/$ZIM_FOLDER"
mkdir -p "$ZIM_PATH"
ok "ZIM path: $ZIM_PATH"
echo ""
fi
# ── Model storage ──────────────────────────────────────────────────────────────
echo -e "${BOLD}━━━ Ollama Model Storage ━━━${NC}\n"
echo " Models range from 4GB (7B) to 30GB+ (70B)."
echo " Pick a fast drive (SSD preferred) with 50200GB free."
echo ""
pick_drive "Model" MODEL_DRIVE
MODEL_PATH=""
if [[ -n "$MODEL_DRIVE" ]]; then
read -rp " Folder name on that drive [ollama-models]: " MODEL_FOLDER
MODEL_FOLDER="${MODEL_FOLDER:-ollama-models}"
MODEL_PATH="$MODEL_DRIVE/$MODEL_FOLDER"
mkdir -p "$MODEL_PATH"
ok "Model path: $MODEL_PATH"
echo ""
fi
# ── bail if nothing selected ───────────────────────────────────────────────────
if [[ -z "$ZIM_PATH" && -z "$MODEL_PATH" ]]; then
info "Nothing selected — no changes made."
exit 0
fi
# ── summary + confirm ──────────────────────────────────────────────────────────
echo -e "${BOLD}━━━ Planned Changes ━━━${NC}\n"
[[ -n "$ZIM_PATH" ]] && echo " Kiwix ZIMs → $ZIM_PATH"
[[ -n "$MODEL_PATH" ]] && echo " Ollama models → $MODEL_PATH"
echo ""
read -rp "Apply these changes to docker-compose.yml? [Y/n]: " CONFIRM
[[ "${CONFIRM,,}" == "n" ]] && { info "Aborted — no changes made."; exit 0; }
# ── backup ────────────────────────────────────────────────────────────────────
cp "$COMPOSE" "$COMPOSE.bak"
info "Backed up to docker-compose.yml.bak"
# ── patch kiwix volumes ────────────────────────────────────────────────────────
if [[ -n "$ZIM_PATH" ]]; then
# Replace the kiwix volumes line to add extra ZIM mounts
# Current: volumes: [$BASE/kiwix:/data]
# New: volumes:
# - $BASE/kiwix:/data
# - /mnt/drive/zims:/data/external
python3 - "$COMPOSE" "$BASE" "$ZIM_PATH" << 'PY'
import sys, re
compose_file = sys.argv[1]
base = sys.argv[2]
zim_path = sys.argv[3]
text = open(compose_file).read()
# Find the kiwix service volumes line (single-line array style)
old = f" volumes: [{base}/kiwix:/data]"
new = (
f" volumes:\n"
f" - {base}/kiwix:/data\n"
f" - {zim_path}:/data/external"
)
if old in text:
text = text.replace(old, new)
open(compose_file, "w").write(text)
print(f"[OK] Kiwix volumes updated")
else:
# Already multi-line — append the new mount if not already there
marker = "container_name: kiwix"
if marker in text and zim_path not in text:
# find the volumes block under kiwix and append
lines = text.splitlines()
in_kiwix = False
in_vols = False
insert_after = -1
for i, line in enumerate(lines):
if marker in line:
in_kiwix = True
if in_kiwix and "volumes:" in line:
in_vols = True
if in_vols and line.strip().startswith("- ") and ":/data" in line:
insert_after = i
if in_vols and insert_after > 0 and not line.strip().startswith("- "):
break
if insert_after > 0:
lines.insert(insert_after + 1, f" - {zim_path}:/data/external")
open(compose_file, "w").write("\n".join(lines) + "\n")
print(f"[OK] Kiwix extra ZIM volume appended")
else:
print(f"[!!] Could not patch kiwix volumes — edit manually: add - {zim_path}:/data/external")
else:
print(f"[!!] Kiwix volumes line not found in expected format — edit manually")
PY
fi
# ── patch ollama volumes ───────────────────────────────────────────────────────
if [[ -n "$MODEL_PATH" ]]; then
python3 - "$COMPOSE" "$MODEL_PATH" << 'PY'
import sys
compose_file = sys.argv[1]
model_path = sys.argv[2]
text = open(compose_file).read()
# Replace Docker-managed volume with host path
old = " volumes: [ollama-models:/root/.ollama]"
new = f" volumes: [{model_path}:/root/.ollama]"
if old in text:
text = text.replace(old, new)
# Remove ollama-models from the top-level volumes section if present
text = text.replace(" ollama-models:\n", "")
open(compose_file, "w").write(text)
print(f"[OK] Ollama volume → {model_path}")
else:
print(f"[!!] ollama volumes line not in expected format — edit manually:")
print(f" change ollama-models:/root/.ollama to {model_path}:/root/.ollama")
PY
fi
# ── restart if running ─────────────────────────────────────────────────────────
echo ""
if docker compose -f "$COMPOSE" ps --quiet 2>/dev/null | grep -q .; then
read -rp "Stack is running — restart now to apply changes? [Y/n]: " RESTART
if [[ "${RESTART,,}" != "n" ]]; then
cd "$BASE"
docker compose down
docker compose up -d
ok "Stack restarted"
else
warn "Remember to restart: cd $BASE && docker compose down && docker compose up -d"
fi
else
info "Stack not running — changes will apply on next start."
fi
echo ""
echo -e "${GREEN}${BOLD}Done!${NC}"
[[ -n "$ZIM_PATH" ]] && echo " Drop .zim files into: $ZIM_PATH"
[[ -n "$MODEL_PATH" ]] && echo " Ollama models stored in: $MODEL_PATH"
echo ""
echo " Undo: cp $COMPOSE.bak $COMPOSE"