LocalAI/scripts/start.sh
2026-10-17 23:15:26 +02:00

186 lines
8.5 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
# Startet den LocalAI-Stack, wartet bis er bereit ist und zeigt die Verbindungsdaten.
#
# ./scripts/start.sh starten + Chat-Modell vorladen
# ./scripts/start.sh --no-preload nur starten
# ./scripts/start.sh --model glm-4.7-flash anderes Chat-Modell vorladen
# ./scripts/start.sh --show-key API-Key im Klartext ausgeben
# ./scripts/start.sh --pull Basis-Images aktualisieren und neu bauen
set -euo pipefail
cd "$(dirname "$(readlink -f "$0")")/.."
PRELOAD=1
MODEL="qwen3.5-35b-a3b"
SHOW_KEY=0
PULL=0
while [[ $# -gt 0 ]]; do
case "$1" in
--no-preload) PRELOAD=0 ;;
--model) MODEL="$2"; shift ;;
--show-key) SHOW_KEY=1 ;;
--pull) PULL=1 ;;
-h|--help) sed -n '2,9p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "Unbekannte Option: $1" >&2; exit 2 ;;
esac
shift
done
if [[ -t 1 ]]; then B=$'\e[1m'; G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; N=$'\e[0m'; else B= G= Y= R= N=; fi
ok() { echo "${G}✔${N} $*"; }
warn() { echo "${Y}!${N} $*"; }
die() { echo "${R}✘${N} $*" >&2; exit 1; }
step() { echo; echo "${B}== $*${N}"; }
# ---------------------------------------------------------------- Prüfungen
step "Prüfungen"
command -v docker >/dev/null || die "docker nicht installiert"
docker info >/dev/null 2>&1 || die "Docker-Daemon läuft nicht oder keine Rechte (Gruppe 'docker'?)"
docker compose version >/dev/null 2>&1 || die "docker compose (v2) fehlt"
ok "Docker $(docker version --format '{{.Server.Version}}')"
[[ -e /dev/kfd && -d /dev/dri ]] || die "/dev/kfd oder /dev/dri fehlt – AMD-GPU-Treiber (amdgpu/ROCm) prüfen"
ok "GPU-Geräte vorhanden"
if [[ ! -f .env ]]; then
warn ".env fehlt – wird aus .env.example erzeugt"
cp .env.example .env
sed -i "s/^LLM_API_KEY=.*/LLM_API_KEY=$(openssl rand -hex 32)/" .env
sed -i "s/^WEBUI_SECRET_KEY=.*/WEBUI_SECRET_KEY=$(openssl rand -hex 32)/" .env
sed -i "s/^HOST_UID=.*/HOST_UID=$(id -u)/; s/^HOST_GID=.*/HOST_GID=$(id -g)/" .env
rg=$(getent group render | cut -d: -f3 || true); vg=$(getent group video | cut -d: -f3 || true)
[[ -n "$rg" ]] && sed -i "s/^RENDER_GID=.*/RENDER_GID=$rg/" .env
[[ -n "$vg" ]] && sed -i "s/^VIDEO_GID=.*/VIDEO_GID=$vg/" .env
chmod 600 .env
warn "Bitte ALLOWED_NETS in .env prüfen (LAN-/NetBird-Netz)"
fi
# ältere .env ohne Web-Chat-Einträge ergänzen
if ! grep -q '^WEBUI_SECRET_KEY=' .env; then
printf '\nWEBUI_PORT=8801\nWEBUI_BACKEND_PORT=3000\nWEBUI_SECRET_KEY=%s\n' "$(openssl rand -hex 32)" >> .env
warn "Web-Chat-Einträge (WEBUI_*) in .env ergänzt"
fi
set -a; . ./.env; set +a
LISTEN_PORT="${LISTEN_PORT:-8800}"
BACKEND_PORT="${BACKEND_PORT:-8080}"
WEBUI_PORT="${WEBUI_PORT:-8801}"
WEBUI_BACKEND_PORT="${WEBUI_BACKEND_PORT:-3000}"
[[ -n "${LLM_API_KEY:-}" && "$LLM_API_KEY" != "CHANGE_ME" ]] || die "LLM_API_KEY in .env setzen (openssl rand -hex 32)"
[[ -n "${WEBUI_SECRET_KEY:-}" && "$WEBUI_SECRET_KEY" != "CHANGE_ME" ]] || die "WEBUI_SECRET_KEY in .env setzen (openssl rand -hex 32)"
ok ".env geladen"
# Ports dürfen nur von unserem eigenen Stack belegt sein
running=$(docker compose ps --status running -q 2>/dev/null | wc -l)
if [[ "$running" -eq 0 ]]; then
for p in "$LISTEN_PORT" "$BACKEND_PORT" "$WEBUI_PORT" "$WEBUI_BACKEND_PORT"; do
if ss -Htln "sport = :$p" | grep -q .; then
die "Port $p ist bereits belegt: $(ss -Htlnp "sport = :$p" | awk '{print $NF}' | head -1) – in .env ändern"
fi
done
ok "Ports $LISTEN_PORT, $BACKEND_PORT, $WEBUI_PORT und $WEBUI_BACKEND_PORT frei"
fi
if systemctl is-active --quiet ollama 2>/dev/null; then
warn "Host-Ollama läuft noch (belegt ggf. VRAM, lauscht auf dem Netz) → sudo systemctl disable --now ollama"
fi
if command -v ufw >/dev/null && systemctl is-active --quiet ufw 2>/dev/null; then
for p in "$LISTEN_PORT" "$WEBUI_PORT"; do
if [[ -r /etc/ufw/user.rules ]] && ! grep -q "dport $p" /etc/ufw/user.rules; then
warn "Keine ufw-Freigabe für Port $p gefunden → sudo ./scripts/firewall-ufw.sh"
fi
done
fi
mkdir -p models
free_gb=$(df -BG --output=avail models | tail -1 | tr -dc '0-9')
(( free_gb < 30 )) && warn "Nur ${free_gb} GB frei – Modell-Downloads brauchen bis zu 25 GB"
# ------------------------------------------------------------------- Start
step "Starte Container"
if (( PULL )); then
docker compose pull --ignore-buildable
docker compose build --pull
fi
# baut das llama-swap-Image (ROCm + whisper/sd-server), falls es noch fehlt
docker compose up -d --build --remove-orphans
step "Warte auf Bereitschaft"
for i in $(seq 1 60); do
curl -fsS -m 2 "http://127.0.0.1:$BACKEND_PORT/health" >/dev/null 2>&1 && break
(( i == 60 )) && die "llama-swap antwortet nicht – docker compose logs llama-swap"
sleep 1
done
ok "llama-swap bereit"
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -m 2 -w '%{http_code}' "http://127.0.0.1:$LISTEN_PORT/v1/models" || true)
[[ "$code" == "401" ]] && break
(( i == 30 )) && die "Caddy antwortet nicht wie erwartet (HTTP $code) – docker compose logs caddy"
sleep 1
done
code=$(curl -s -o /dev/null -m 5 -w '%{http_code}' -H "Authorization: Bearer $LLM_API_KEY" "http://127.0.0.1:$LISTEN_PORT/v1/models")
[[ "$code" == "200" ]] || die "API mit Key liefert HTTP $code"
ok "Caddy bereit (Auth aktiv)"
# Open WebUI braucht beim ersten Start etwas länger (Datenbank anlegen)
for i in $(seq 1 120); do
curl -fsS -m 2 "http://127.0.0.1:$WEBUI_PORT/health" >/dev/null 2>&1 && break
(( i == 120 )) && die "Web-Chat antwortet nicht – docker compose logs open-webui"
sleep 1
done
ok "Web-Chat bereit"
# ---------------------------------------------------------------- Preload
if (( PRELOAD )); then
step "Lade Modelle vor (erster Start lädt ggf. von Hugging Face herunter)"
preload() { # $1 = Modell, $2 = Endpoint, Rest = curl-Argumente für den Body
local m=$1 ep=$2 t0=$SECONDS code; shift 2
printf ' %-22s … ' "$m"
code=$(curl -s -o /dev/null -m 3600 -w '%{http_code}' "http://127.0.0.1:$BACKEND_PORT$ep" "$@" || true)
if [[ "$code" == "200" ]]; then echo "${G}ok${N} ($((SECONDS - t0)) s)"; else echo "${R}HTTP $code${N}"; fi
}
json=(-H 'Content-Type: application/json' -d)
preload qwen2.5-coder-1.5b /v1/completions "${json[@]}" '{"model":"qwen2.5-coder-1.5b","prompt":"x","max_tokens":1}'
preload nomic-embed /v1/embeddings "${json[@]}" '{"model":"nomic-embed","input":"x"}'
if [[ -f models/whisper/ggml-large-v3-turbo-q8_0.bin ]]; then
wav=$(mktemp --suffix=.wav)
python3 -c 'import wave,sys; w=wave.open(sys.argv[1],"wb"); w.setnchannels(1); w.setsampwidth(2); w.setframerate(16000); w.writeframes(b"\0\0"*16000)' "$wav"
preload whisper /v1/audio/transcriptions -F model=whisper -F "file=@$wav"
rm -f "$wav"
else
warn "Whisper-Modell fehlt (models/whisper/) – siehe README"
fi
preload "$MODEL" /v1/chat/completions "${json[@]}" "{\"model\":\"$MODEL\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":1}"
fi
# ------------------------------------------------------------------ Status
step "Status"
docker compose ps --format '{{.Service}}: {{.Status}}' | sed 's/^/ /'
loaded=$(curl -s -m 5 "http://127.0.0.1:$BACKEND_PORT/running" \
| python3 -c 'import sys,json; print(", ".join(m["model"] for m in json.load(sys.stdin)["running"]) or "-")' 2>/dev/null || echo "?")
echo " geladen: $loaded"
command -v rocm-smi >/dev/null && rocm-smi --showmeminfo vram 2>/dev/null \
| awk '/GPU\[0\].*Total Memory/{t=$NF} /GPU\[0\].*Used Memory/{u=$NF} END{if(t) printf " VRAM: %.1f / %.1f GB\n", u/1e9, t/1e9}'
step "Verbindung"
lan_ip=$(ip -4 -o addr show scope global | awk '$2 !~ /^(wt|docker|virbr|br-|veth)/ {split($4,a,"/"); print a[1]; exit}')
nb_ip=$(ip -4 -o addr show wt0 2>/dev/null | awk '{split($4,a,"/"); print a[1]}')
nb_fqdn=$(netbird status 2>/dev/null | awk '/^FQDN:/{print $2}' || true)
echo " API lokal: http://127.0.0.1:$LISTEN_PORT/v1"
[[ -n "$lan_ip" ]] && echo " API LAN: http://$lan_ip:$LISTEN_PORT/v1"
[[ -n "$nb_ip" ]] && echo " API NetBird: http://$nb_ip:$LISTEN_PORT/v1${nb_fqdn:+ (http://$nb_fqdn:$LISTEN_PORT/v1)}"
echo " Chat lokal: http://127.0.0.1:$WEBUI_PORT"
[[ -n "$lan_ip" ]] && echo " Chat LAN: http://$lan_ip:$WEBUI_PORT"
[[ -n "$nb_ip" ]] && echo " Chat NetBird: http://$nb_ip:$WEBUI_PORT${nb_fqdn:+ (http://$nb_fqdn:$WEBUI_PORT)}"
if (( SHOW_KEY )); then
echo " API-Key: $LLM_API_KEY"
else
echo " API-Key: ${LLM_API_KEY:0:6}… (vollständig: --show-key)"
fi
echo " llama-swap: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)"
echo
echo " Clients einrichten: docs/opencode.md, docs/continue.md, docs/webchat.md"