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

151 lines
6.6 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 Images vorher aktualisieren
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/^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
set -a; . ./.env; set +a
LISTEN_PORT="${LISTEN_PORT:-8800}"
BACKEND_PORT="${BACKEND_PORT:-8080}"
[[ -n "${LLM_API_KEY:-}" && "$LLM_API_KEY" != "CHANGE_ME" ]] || die "LLM_API_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"; 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 und $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
if [[ -r /etc/ufw/user.rules ]] && ! grep -q "dport $LISTEN_PORT" /etc/ufw/user.rules; then
warn "Keine ufw-Freigabe für Port $LISTEN_PORT gefunden → sudo ./scripts/firewall-ufw.sh"
fi
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"
(( PULL )) && docker compose pull
docker compose up -d --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)"
# ---------------------------------------------------------------- Preload
if (( PRELOAD )); then
step "Lade Modelle vor (erster Start lädt ggf. von Hugging Face herunter)"
preload() { # $1 = Modell, $2 = Endpoint, $3 = JSON-Body
local t0=$SECONDS code
printf ' %-22s … ' "$1"
code=$(curl -s -o /dev/null -m 3600 -w '%{http_code}' -H 'Content-Type: application/json' \
"http://127.0.0.1:$BACKEND_PORT$2" -d "$3" || true)
if [[ "$code" == "200" ]]; then echo "${G}ok${N} ($((SECONDS - t0)) s)"; else echo "${R}HTTP $code${N}"; fi
}
preload qwen2.5-coder-1.5b /v1/completions '{"model":"qwen2.5-coder-1.5b","prompt":"x","max_tokens":1}'
preload nomic-embed /v1/embeddings '{"model":"nomic-embed","input":"x"}'
preload "$MODEL" /v1/chat/completions "{\"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 " lokal: http://127.0.0.1:$LISTEN_PORT/v1"
[[ -n "$lan_ip" ]] && echo " LAN: http://$lan_ip:$LISTEN_PORT/v1"
[[ -n "$nb_ip" ]] && echo " NetBird: http://$nb_ip:$LISTEN_PORT/v1${nb_fqdn:+ (http://$nb_fqdn:$LISTEN_PORT/v1)}"
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 " Web-UI: http://127.0.0.1:$BACKEND_PORT/ui (nur lokal)"
echo
echo " Clients einrichten: docs/opencode.md, docs/continue.md"