#!/usr/bin/env bash # Reproducible bootstrap for a fresh Debian 12/13 AI host. set -Eeuo pipefail umask 077 ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CONFIG="" STACK_DIR=/opt/mike-ai/stack SECRETS_DIR=/etc/mike-ai STATE_DIR=/srv/mike-ai log() { printf '\n==> %s\n' "$*"; } die() { printf 'FEHLER: %s\n' "$*" >&2; exit 1; } usage() { cat <<'EOF' Aufruf: sudo ./install.sh --config /root/mike-ai-install.env Das Skript ist idempotent und darf nach einem Treiber-Reboot erneut ausgeführt werden. Es formatiert keine Datenträger und löscht keine Modelle oder Volumes. EOF } while [[ $# -gt 0 ]]; do case "$1" in --config) CONFIG="${2:-}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) die "Unbekanntes Argument: $1" ;; esac done [[ $EUID -eq 0 ]] || die "Bitte als root ausführen." [[ -n "$CONFIG" && -f "$CONFIG" ]] || die "Konfigurationsdatei fehlt (--config)." config_mode=$(stat -c '%a' "$CONFIG") if (( (8#$config_mode & 077) != 0 )); then die "Konfigurationsdatei darf nicht für Gruppe/Andere schreib- oder lesbar sein (chmod 600)." fi # shellcheck disable=SC1090 source "$CONFIG" required=(AI_HOSTNAME ADMIN_USER AI_BIND_ADDRESS MODEL_DIR FAST_MODEL_FILE FAST_MODEL_URL FAST_MODEL_SHA256 MEDIUM_MODEL_FILE MEDIUM_MODEL_URL MEDIUM_MODEL_SHA256 LONG_MODEL_FILE LONG_MODEL_URL LONG_MODEL_SHA256 EXPERIMENTAL_MODEL_FILE EXPERIMENTAL_MODEL_URL EXPERIMENTAL_MODEL_SHA256 VISION_PROJECTOR_FILE VISION_PROJECTOR_URL VISION_PROJECTOR_SHA256) for name in "${required[@]}"; do [[ -n "${!name:-}" ]] || die "Pflichtwert $name fehlt." done source /etc/os-release [[ ${ID:-} == debian ]] || die "Unterstützt wird Debian, gefunden: ${ID:-unbekannt}." [[ ${VERSION_ID%%.*} == 12 || ${VERSION_ID%%.*} == 13 ]] || \ die "Unterstützt werden Debian 12 und 13." [[ $(dpkg --print-architecture) == amd64 ]] || die "Dieser Stack erwartet amd64." id "$ADMIN_USER" >/dev/null 2>&1 || die "ADMIN_USER $ADMIN_USER existiert nicht." install_base_packages() { log "Basispakete installieren" apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates curl git gnupg jq openssl wireguard-tools iptables \ iproute2 pciutils rsync unattended-upgrades } install_docker() { log "Docker CE aus dem offiziellen Repository installieren" install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg \ -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc cat >/etc/apt/sources.list.d/docker.sources </dev/null 2>&1; then [[ ${INSTALL_NVIDIA_DRIVER:-false} == true ]] || \ die "NVIDIA-Treiber fehlt. Installiere ihn oder setze INSTALL_NVIDIA_DRIVER=true." log "Aktuellen NVIDIA-Treiber aus dem offiziellen NVIDIA-Repository installieren" DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ "linux-headers-$(uname -r)" local distro="debian${VERSION_ID%%.*}" local keyring_url="https://developer.download.nvidia.com/compute/cuda/repos/${distro}/x86_64/cuda-keyring_1.1-1_all.deb" local keyring_deb keyring_deb=$(mktemp /tmp/cuda-keyring.XXXXXX.deb) curl --fail --location --retry 5 --output "$keyring_deb" "$keyring_url" dpkg -i "$keyring_deb" rm -f "$keyring_deb" apt-get update if [[ -n ${NVIDIA_DRIVER_BRANCH:-} ]]; then local pinning="nvidia-driver-pinning-${NVIDIA_DRIVER_BRANCH}" apt-cache show "$pinning" >/dev/null 2>&1 || \ die "Gewuenschter NVIDIA-Treiberzweig fehlt im offiziellen Repository: $pinning" DEBIAN_FRONTEND=noninteractive apt-get install -y "$pinning" fi # RTX 30/50 werden beide von NVIDIAs offenen Kernelmodulen unterstuetzt. # Compute-only vermeidet X11-, Desktop- und Vulkan-Pakete auf dem Headless-Host. DEBIAN_FRONTEND=noninteractive apt-get install -y \ nvidia-driver-cuda nvidia-kernel-open-dkms printf '\nNVIDIA-Treiber installiert. Jetzt neu starten und danach denselben Installer erneut ausführen.\n' exit 20 fi nvidia-smi >/dev/null || die "nvidia-smi ist vorhanden, aber der Treiber funktioniert nicht." local driver_major min_driver_major driver_major=$(nvidia-smi --query-gpu=driver_version --format=csv,noheader | head -n1 | cut -d. -f1) min_driver_major=${NVIDIA_MIN_DRIVER_MAJOR:-570} [[ $driver_major =~ ^[0-9]+$ ]] || die "NVIDIA-Treiberversion konnte nicht gelesen werden." (( driver_major >= min_driver_major )) || \ die "NVIDIA-Treiber $driver_major ist zu alt; mindestens $min_driver_major wird verlangt." log "NVIDIA Container Toolkit installieren" curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \ gpg --dearmor --yes -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg curl -fsSL https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \ sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' \ >/etc/apt/sources.list.d/nvidia-container-toolkit.list apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y nvidia-container-toolkit nvidia-ctk runtime configure --runtime=docker systemctl restart docker docker run --rm --gpus all nvidia/cuda:12.8.1-base-ubuntu24.04 nvidia-smi >/dev/null } setup_wireguard() { [[ ${WIREGUARD_ENABLE:-false} == true ]] || return 0 for name in WG_INTERFACE WG_ADDRESS WG_HOME_SUBNET WG_PEER_PUBLIC_KEY WG_PEER_ENDPOINT; do [[ -n "${!name:-}" && ${!name} != REPLACE_* ]] || die "WireGuard-Wert $name fehlt." done log "WireGuard als ausgehenden Heimnetz-Tunnel konfigurieren" install -d -m 0700 /etc/wireguard local key_file="/etc/wireguard/${WG_INTERFACE}.key" [[ -s $key_file ]] || wg genkey >"$key_file" chmod 0600 "$key_file" local private_key private_key=$(<"$key_file") local allowed_ips="$WG_HOME_SUBNET" [[ ${WG_ROUTE_AI_INTERNET:-true} == true ]] && allowed_ips="0.0.0.0/0, ::/0" { printf '[Interface]\nAddress = %s\nPrivateKey = %s\nTable = off\n' "$WG_ADDRESS" "$private_key" printf '\n[Peer]\nPublicKey = %s\nEndpoint = %s\nAllowedIPs = %s\nPersistentKeepalive = 25\n' \ "$WG_PEER_PUBLIC_KEY" "$WG_PEER_ENDPOINT" "$allowed_ips" if [[ -n ${WG_PEER_PRESHARED_KEY_FILE:-} ]]; then [[ -s $WG_PEER_PRESHARED_KEY_FILE ]] || die "WireGuard-Preshared-Key-Datei fehlt." printf 'PresharedKey = %s\n' "$(<"$WG_PEER_PRESHARED_KEY_FILE")" fi } >"/etc/wireguard/${WG_INTERFACE}.conf" chmod 0600 "/etc/wireguard/${WG_INTERFACE}.conf" systemctl enable --now "wg-quick@${WG_INTERFACE}" ip address show "$WG_INTERFACE" >/dev/null local wg_host=${WG_ADDRESS%/*} [[ $AI_BIND_ADDRESS == "$wg_host" ]] || \ die "AI_BIND_ADDRESS muss der WireGuard-Adresse ohne Präfix entsprechen ($wg_host)." printf 'WireGuard-Public-Key des KI-Hosts: %s\n' "$(wg pubkey <"$key_file")" } install_stack_files() { log "Stackdateien installieren" install -d -m 0755 "$STACK_DIR" "$MODEL_DIR" "$STATE_DIR/backups" rsync -a --delete --exclude .git --exclude '*.local.*' \ --exclude config/install.env "$ROOT_DIR/" "$STACK_DIR/" install -d -m 0700 "$SECRETS_DIR" [[ -s $SECRETS_DIR/router-api-key ]] || openssl rand -base64 48 >$SECRETS_DIR/router-api-key [[ -s $SECRETS_DIR/controller-token ]] || openssl rand -base64 48 >$SECRETS_DIR/controller-token [[ -s $SECRETS_DIR/webui-secret ]] || openssl rand -base64 48 >$SECRETS_DIR/webui-secret chmod 0600 "$SECRETS_DIR"/* local searx="$STACK_DIR/platform/web-search/searxng-settings.yml" if [[ ! -s $searx ]]; then cp "$STACK_DIR/platform/web-search/searxng-settings.example.yml" "$searx" sed -i "s/CHANGE_ME_GENERATE_RANDOM_SECRET/$(openssl rand -hex 32)/" "$searx" fi # The official image reads this as its unprivileged uid (977). chown root:977 "$searx" chmod 0640 "$searx" cat >$SECRETS_DIR/stack.env </dev/null 2>&1; then printf 'Vorhanden und geprüft: %s\n' "$relative" return fi log "Modell laden: $relative" curl --fail --location --continue-at - --retry 5 --retry-all-errors \ --output "$target.partial" "$url" printf '%s %s\n' "$expected" "$target.partial" | sha256sum -c - mv "$target.partial" "$target" } download_models() { [[ ${SKIP_MODEL_DOWNLOADS:-false} == true ]] && return 0 declare -A seen=() local row relative url hash while IFS='|' read -r relative url hash; do [[ -n ${seen[$relative]:-} ]] && continue seen[$relative]=1 download_one "$relative" "$url" "$hash" done </usr/local/sbin/mike-ai-network-guard </dev/null || true done ip route replace \"\$HOME_NET\" dev \"\$WG\" ip route replace \"\$HOME_NET\" dev \"\$WG\" table \"\$TABLE\" ip route replace blackhole default metric 32767 table \"\$TABLE\" EOF if [[ ${WG_ROUTE_AI_INTERNET:-true} == true ]]; then printf 'ip route replace default dev "$WG" metric 10 table "$TABLE"\n' >>/usr/local/sbin/mike-ai-network-guard fi cat >>/usr/local/sbin/mike-ai-network-guard <<'EOF' # Explicit forwarding policy: AI containers may use wg0; WireGuard clients may # reach the two published UI/API ports. Neither side may use this host as a # general university<->home router. The blackhole route above prevents a # fail-open to the university gateway when wg0 loses its peer/default route. add_rule() { iptables -C DOCKER-USER "$@" 2>/dev/null || iptables -I DOCKER-USER 1 "$@"; } add_rule -o "$WG" -j DROP add_rule -i "$WG" -j DROP add_rule -s 172.30.0.0/16 -o "$WG" -j ACCEPT add_rule -i "$WG" -d 172.30.10.0/24 -p tcp -m multiport --dports 8080,8081 -j ACCEPT add_rule -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT EOF chmod 0755 /usr/local/sbin/mike-ai-network-guard cat >/etc/systemd/system/mike-ai-network-guard.service <