Für KING_P · Take 00 bis 12

Das Heimstudio

Du hast TrueNAS und Tailscale. Was fehlt, ist die Band. Und die passt – entgegen dem, was du denkst – schon heute in deine Kiste. Kein Lottogewinn nötig.

Beelink ME-Serie · Intel N150 TrueNAS 25.10 Ollama 0.32.5 Qdrant 1.18.3 cloudfrei Stand Juli 2026
Signalkette durch das Studio00BriefingWas KING_Pvorhat01SoundcheckKisteausmessen02BühneDatasets& Ordner03BacklineRechte,Snapshots04RackOllamalaeuft05MusikerModelle &Presets06LibraryQdrantIndex07SongbookObsidian+ Sync08TagebuchGit &Forgejo09MischpultWebUI &Agenten10BackstageTailscalevon unterwegs11Soundcheck 2Test &Betrieberledigtdieser Takekommt noch
TAKE 00

Briefing

Was du eigentlich vorhast – und warum du dafür nicht im Lotto gewinnen musst.

Du hast geschrieben: „Jetzt erstmal unabhängig von Cloud Anbietern.“ Und gleich danach: „Wenn ich mal im Lotto gewinne, wird das ganze noch mit einem lokalen GPU Server verknüpft und mit meiner eigenen KI erweitert.“

Beides zusammen ergibt ein klares Bild. Mit TrueNAS und Tailscale hast du die Datensouveränität gelöst: der Speicher gehört dir, der Zugang gehört dir, niemand liest mit. Was noch fehlt, ist die Verarbeitungssouveränität – dass auch das Denken über deine Daten auf deiner Hardware passiert.

Die Umdeutung

Ein 70B-Modell auf deinem Gerät wäre wie ein Konzertflügel im Bad: technisch reinbekommbar, aber niemand hat etwas davon. Was tatsächlich funktioniert, sind viele kleine Spezialisten – jeder kann eine Sache richtig gut, ist in Sekunden da und braucht zwei Gigabyte.

Was du dir vorstellstWas tatsächlich läuftErgebnis
Ein großes Modell für allesFünf kleine mit klaren Rollenschneller, nachvollziehbar, wartbar
GPU-Server für 2000 €Der Beelink, der schon da steht0 € Mehrkosten, ~15 W
Warten bis Hardware reichtHeute Abend anfangenin zwei Stunden läuft die erste Band

Das Bild, an dem wir uns festhalten

Dein Gerät sieht nicht zufällig aus wie ein Verstärker – Rezensenten vergleichen das Gehäuse wörtlich mit einem Marshall-Amp. Also bauen wir kein Rechenzentrum, sondern ein Heimstudio. Das ist keine Spielerei: die Metapher hält, weil beide dieselben Probleme haben. Begrenzter Platz im Rack, begrenzte Kanäle, und alles muss eingepegelt sein, bevor es gut klingt.

TechnikIm StudioWas es tut
Der BeelinkDer Ampdie Maschine, auf der alles steht
ZFS-DatasetsDie Bühnewer wo steht, und wer schreiben darf
OllamaDas Instrumenten-Rackhält die Modelle spielbereit
Modelle & ModelfilesDie Session-Musikerje einer für eine Aufgabe
QdrantDie Sample-Libraryfindet in Sekunden, was du mal notiert hast
Obsidian-VaultDas Songbookdein Wissen, als schlichtes Markdown
Git & ForgejoDas Bandtagebuchjeder Take archiviert, nichts geht verloren
Open WebUIDas Mischpultwo du die Regler in der Hand hast
TailscaleDer Backstage-Passvon überall rein, ohne offene Tür
Zielzustand: das HeimstudioBeelinkTrueNAS 25.10 · ZFS16 GB, 4 KerneRackOllama3 Musiker warmLibraryQdrantVektorindexSongbookObsidian-Vault+ SyncthingMischpultOpen WebUIModell-PresetsWerkzeugeBandtagebuchGit + Forgejojeder Take gesichertBackstageTailscalevon ueberall,kein offener Port114346333indexcommitSechs Container. Ihr Zustand liegt in Dateien und schlanken SQLite-Dateien -kein einziger zusaetzlicher Datenbankserver. Genau das haelt es wartungsarm.
Der Zielzustand. Sechs Container, vier davon brauchen nur eine Datei als Zustand – keine einzige zusätzliche Datenbank.

Was dich das kostet

TAKE 01

Soundcheck

Bevor irgendetwas installiert wird: die Kiste ausmessen. Fünf Minuten, die dir später Stunden sparen.

Auf dem Foto steht ein Beelink-Würfel aus der ME-Serie. Welcher genau, lässt sich von außen nicht sicher sagen – die vollflächige Mesh-Front mit Schriftzug spricht für den ME Pro, das interne Netzteil eher für den ME mini. Das ist keine Haarspalterei: davon hängt ab, wie viele Steckplätze du hast und ob ein bekannter Kernel-Fehler dich betrifft.

Soundcheck – ein Befehl, alle Antworten bash
# In der TrueNAS-Shell ausfuehren (System Settings > Shell)
{
echo "=== Geraet ==="
sudo dmidecode -s system-product-name 2>/dev/null || echo "  (dmidecode nicht verfuegbar)"
sudo dmidecode -s baseboard-product-name 2>/dev/null
echo
echo "=== Prozessor ==="
lscpu | grep -E "Model name|^CPU\(s\)|Thread|Core"
echo
echo "=== Speicher (das entscheidende Mass) ==="
free -h | head -2
sudo dmidecode -t memory 2>/dev/null | grep -E "Size:|Speed:|Locator:" | grep -v "No Module"
echo
echo "=== Netz ==="
ip -br link | grep -v lo
lspci -nn | grep -i ethernet
echo
echo "=== Platten ==="
lsblk -o NAME,SIZE,MODEL,TRAN 2>/dev/null | grep -v loop
echo
echo "=== Grafik (fuer den Entscheidungspunkt in Take 04) ==="
lspci -k | grep -A3 -Ei 'vga|display' || echo "  keine dedizierte Grafik"
ls -l /dev/dri/ 2>/dev/null || echo "  /dev/dri fehlt"
echo
echo "=== TrueNAS ==="
cat /etc/version 2>/dev/null
uname -r
echo
echo "=== ZFS ==="
zpool list
cat /sys/module/zfs/parameters/zfs_arc_max 2>/dev/null | awk '{printf "ARC-Deckel: %.1f GB\n", $1/1073741824}'
} | tee ~/soundcheck-$(date +%F).txt

Was du in der Ausgabe suchst

ZeileWas sie dir sagtWarum es zählt
Model name: Intel N150 oder N95welche CPU-Klassebestimmt, welche Modellgrößen Sinn ergeben
free -h → 12 oder 16 GBdein hartes Budgetverlötet. Es gibt kein Upgrade.
Realtek RTL8126 in lspci5-GbE-Port vorhanden→ es ist ein ME Pro
zweimal Intel I226, sonst nichtszwei 2,5-GbE-Ports→ es ist ein ME mini
ASMedia ASM2824PCIe-Switch für 6 Steckplätze→ ME mini, und du bist vom NVMe-Fehler betroffen
/dev/dri/renderD128 vorhandeniGPU ist ansprechbarrelevant für Take 04 und für Videotranscoding
uname -r ≥ 6.12Kernel neu genugunter 6.9 fehlt /dev/dri auf dieser CPU komplett

Die eine Zahl, die alles bestimmt

Nicht die Kerne. Nicht die Taktrate. Die Speicherbandbreite. Ein Sprachmodell muss für jedes einzelne Wort seine kompletten Gewichte einmal durch den Speicher schieben. Deine CPU hat einen Speicherkanal – theoretisch 38 GB/s, real eher 20 bis 28.

Daraus folgt die ganze Architektur dieses Guides: Ein 2-GB-Modell schafft bei kurzem Kontext grob acht bis zwölf Wörter pro Sekunde. Ein 5-GB-Modell zwei bis drei. Ein 9-GB-Modell passt neben TrueNAS gar nicht mehr rein. Mehr Kerne würden daran nichts ändern, eine schnellere iGPU auch nicht – beide hängen am selben Kanal.

RAM-Budget als Faderbank16 GB verloetet – so wird eingepegeltTrueNAS2 GBZFS-ARC4 GBOllama6.5 GBOpen WebUI0.9 GBQdrant0.3 GBForgejo0.3 GBSyncthing0.05 GBRest1.95 GBZFS-ARC steht ab Werk auf 8 GB. Auf 4 GB deckeln – dann bleibt dem Rack genug Luft.Der Fader, auf den es ankommt, ist die Modellgroesse. Nicht die Zahl der Container.
16 GB verlötet, aufgeteilt wie eine Faderbank. Der einzige Regler, der wirklich etwas bewegt, ist die Modellgröße.

Zwei bekannte Fallen dieser Geräte

ME mini, nur 4 von 6 NVMe sichtbar. Ein Kernel-Fehler am PCIe-Switch: der Steckplatz meldet sich, schließt das Link-Training aber nicht ab. Unter Windows und Unraid laufen alle sechs, unter TrueNAS SCALE nicht. Kein Fix in Sicht. Prüfen mit nvme list gegen die Zahl der bestückten Slots.

Intel i226-V fällt auf 100 Mbit/s. Nach Stunden oder Tagen, ohne Vorwarnung, bis zum Neustart. Bekannt seit dem Vorgängerchip, kein offizieller Fix. Wenn dein NAS plötzlich zäh wird, ist das der erste Verdacht:

Netzwerk-Degradation entschärfen bash
# Aktuellen Zustand pruefen
ethtool enp1s0 | grep -E "Speed|Duplex"

# Energiesparmodus abschalten - das ist die Massnahme, die wirkt.
sudo ethtool --set-eee enp1s0 eee off

# Feste Geschwindigkeit erzwingen funktioniert bei diesem Chip NICHT:
# der igc-Treiber unterstuetzt kein Force-Speed, und 2,5GBASE-T verlangt
# ohnehin Autonegotiation. Der Befehl quittiert mit "Invalid argument".
#   sudo ethtool -s enp1s0 autoneg off speed 2500 duplex full   # <- geht nicht

# Ueberlebt keinen Neustart. Dauerhaft machen:
#   System Settings > Advanced > Init/Shutdown Scripts
#   Type: Command  |  When: Post Init
#   Command: /usr/sbin/ethtool --set-eee enp1s0 eee off
# Bleibt das Problem, hilft manchmal noch:
#   sudo ethtool -K enp1s0 tso off gso off
TAKE 02

Bühne

Die Ordnerstruktur. Klingt langweilig, ist aber die Entscheidung, die du am häufigsten bereust, wenn du sie falsch triffst.

Weggabelung

Weggabelung 1 · Direkt auf TrueNAS oder in einer VM?

Bevor du die erste Zeile schreibst: Sollen die Container unmittelbar auf TrueNAS laufen, oder in einer virtuellen Maschine mit eigenem Betriebssystem? Die Frage klingt nach Geschmack, hat aber handfeste Folgen für Speicher, Snapshots und dafür, wie oft dich Updates ärgern.

Variante A: direkt auf TrueNASEMPFOHLENTrueNAS 25.10ZFS-Pool, Docker,Apps > Install via YAMLOllamabind-mountdirekt aufs DatasetQdrantlokales POSIX-FSPflichtOpen WebUIDatasetsrecordsize je Zwecksnapshots je DatasetSnapshotsRollback inSekundenContainer sehen die ZFS-Datasets unmittelbar. Kein virtuelles Blockgeraet dazwischen,keine doppelte Speicherbuchhaltung, kein zweiter Kernel. Snapshot und Rollbackgelten fuer echte Dateien - man kann eine einzelne Notiz zurueckholen.Das Docker-Subsystem von TrueNAS ist seit 24.10 stabil. Das VM-Backend wurde zweimal getauscht.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Ein Bind-Mount hängt ein ZFS-Dataset direkt in den Container. Kein virtuelles Blockgerät dazwischen, keine doppelte Speicherbuchhaltung, kein zweiter Kernel.

Der praktische Gewinn zeigt sich beim Zurückholen: Snapshots gelten für echte Dateien. Du kannst eine einzelne Notiz aus dem Stand von vorgestern holen, ohne eine virtuelle Platte zu mounten.

Und ein Argument aus der Beobachtung: Das Docker-Teilsystem von TrueNAS läuft seit 24.10 unverändert stabil. Das VM-Teilsystem wurde im selben Zeitraum zweimal ausgetauscht – erst von libvirt weg zu Incus, in 25.10 wieder zurück zu libvirt. Wer dort seine Arbeitslasten hatte, durfte zweimal umziehen.

Direkt – Datasets als Bind-Mount yaml
# So sieht ein Volume aus, wenn TrueNAS das Dateisystem stellt:
volumes:
  - type: bind
    source: /mnt/tank/studio/library/storage   # echtes ZFS-Dataset
    target: /qdrant/storage
    read_only: false

# Was du damit gewinnst:
sudo zfs snapshot tank/songbook/vault@jetzt
ls /mnt/tank/songbook/vault/.zfs/snapshot/jetzt/     # einfach hineinsehen
cp /mnt/tank/songbook/vault/.zfs/snapshot/jetzt/Projekte/notiz.md ~/   # eine Datei zurueck

# Eigenschaften nachtraeglich aendern - wirkt fuer neu geschriebene Bloecke
sudo zfs set recordsize=16K tank/studio/library/storage
zfs get -r recordsize,compression tank/studio
Variante B: virtualisiert in einer VMNUR BEI BEDARFTrueNAS 25.10stellt nur nochein zvol bereitVM · Debianeigener Kernel,eigenes Docker,fester RAM-BlockOllamaQdrantim virtuellenDateisystemOpen WebUIzvolSnapshot = ganzeDisk, nicht DateizvolSinnvoll nur, wenn du ein fremdes Betriebssystem oder eigene Kernelmodule brauchst.Preis: RAM wird fest reserviert und fehlt dem ZFS-Cache. Ein Snapshot sichert diegesamte virtuelle Platte - einzelne Dateien holt man daraus nicht zurueck.Bei 16 GB verloetetem Speicher ist das der teuerste Weg zum gleichen Ziel.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Es gibt gute Gründe dafür – aber sie sind spezifisch: Du brauchst ein fremdes Betriebssystem, eigene Kernelmodule, oder du willst eine Docker-Spielwiese, die den Host-Daemon von TrueNAS nicht anfasst. Auch harte Isolation ist ein Argument, wenn du Fremdcode laufen lässt.

Was es kostet: Der Speicher wird fest reserviert und fehlt dem ZFS-Lesecache – bei 16 GB verlötetem RAM ist das schmerzhaft. Ein Snapshot sichert die gesamte virtuelle Platte; einzelne Dateien holt man daraus nicht mehr heraus. Und die Voraussetzung: VT-x muss im BIOS aktiv sein.

Eine iGPU an eine VM durchzureichen lohnt hier ohnehin nicht – TrueNAS braucht selbst eine Grafikeinheit, und du verlierst damit die Beschleunigung für alle Container.

VM – Vorbereitung auf TrueNAS 25.10 yaml
# 1. Voraussetzung pruefen: Virtualisierung muss im BIOS an sein
grep -o -m1 -E 'vmx|svm' /proc/cpuinfo || echo "VT-x/AMD-V im BIOS aktivieren!"

# 2. Virtualization > Virtual Machines > Add
#    Guest OS: Linux  |  Kerne: 3 von 4  |  RAM: 8 GB (fest reserviert!)
#    Disk: neues zvol, 64 GB  |  Netz: Bridge

# 3. In der VM: Docker installieren und den Stack normal fahren
curl -fsSL https://get.docker.com | sh

# 4. Damit die VM an die NAS-Daten kommt, brauchst du eine Freigabe -
#    und genau hier faengt der Aerger an:
sudo mkdir -p /mnt/songbook
sudo mount -t nfs 192.168.1.10:/mnt/tank/songbook /mnt/songbook

# ACHTUNG: Qdrant verweigert auf NFS den Dienst. Seine Daten muessen
# auf der virtuellen Platte liegen - und sind damit nur noch als
# Ganzes sicherbar, nicht mehr dateiweise.

Der Rest dieses Takes geht vom direkten Weg aus. In einer VM ist alles gleich – nur dass die Pfade dann innerhalb der VM liegen und du auf Qdrants Dateisystem-Anforderung besonders achten musst.

Auf ZFS ist ein Dataset kein Ordner. Ein Ordner erbt alles vom Elternteil. Ein Dataset hat eigene Blockgröße, eigene Kompression, eigene Snapshots, eigene Aufbewahrungsregeln. Genau das brauchst du, denn deine drei Datenarten könnten unterschiedlicher kaum sein:

WasWie es sich verhältWas es braucht
GGUF-Modellewenige Dateien, mehrere GB, sequenziell gelesenrecordsize=1M, Kompression aus
Qdrant-Segmentewahlfreier Zugriff mitten in großen Dateienrecordsize=16K, wie eine Datenbank
Markdown-Notizentausende winzige Textdateienrecordsize=128K, zstd-3
Buehnenplan: die OrdnerstrukturBuehnenplan – wer wo steht und wer schreiben darfstudio/alles was rechnetrack/Ollama · Modelle & Presets1M · lz4library/Qdrant · Vektorindex16K · lz4desk/WebUI · Indexer · Sync128K · lz4backstage/Tailscale-Zustand128K · lz4stagebox/Secrets · Modus 700128K · lz4logs/Betriebsprotokolle128K · zstdsongbook/alles was Wissen istvault/Obsidian · nur lesen128K · zstdinbox/Agenten duerfen schreiben128K · zstdattachments/Bilder & PDFs1M · offtapes/alles was Geschichte istsongbook.gitbare-Repo · Audit-Log128K · zstdforgejo/Weboberflaeche128K · zstdNur zwei Orte sind beschreibbar: songbook/inbox fuer die Agenten und studio/stagebox fuer dich. Alles andere ist Lesestoff.
Der Bühnenplan. Zwei Orte sind beschreibbar, alles andere ist Lesestoff.

Der Grundriss

Drei Bereiche, klar getrennt. studio/ ist alles, was rechnet. songbook/ ist alles, was Wissen ist. tapes/ ist alles, was Geschichte ist. Die Trennung zahlt sich sofort aus: jeder Bereich bekommt die Blockgröße und die Kompression, die zu ihm passt – und lässt sich später für sich behandeln, ohne die anderen anzufassen.

Bühne aufbauen – Datasets anlegen bash
# In der TrueNAS-Shell. Pool heisst hier "tank" - passe das an.

# --- Das Rack: Ollama. Grosse Dateien, sequenziell gelesen. ---
sudo zfs create -o recordsize=128K -o compression=lz4    -o atime=off tank/studio
sudo zfs create -o recordsize=1M   -o compression=lz4    -o atime=off tank/studio/rack
sudo zfs create -o recordsize=1M   -o compression=off    -o atime=off tank/studio/rack/models
sudo zfs create -o recordsize=128K -o compression=zstd-3 -o atime=off tank/studio/rack/presets

# --- Die Library: Qdrant. Wahlfreier Zugriff, wie eine Datenbank. ---
sudo zfs create -o recordsize=16K  -o compression=lz4    -o atime=off tank/studio/library
sudo zfs create -o recordsize=16K  -o compression=lz4    -o atime=off tank/studio/library/storage
sudo zfs create -o recordsize=1M   -o compression=lz4    -o atime=off tank/studio/library/snapshots

# --- Das Mischpult und der Rest ---
sudo zfs create -o recordsize=128K -o compression=lz4    -o atime=off tank/studio/desk
sudo zfs create -o recordsize=128K -o compression=lz4    -o atime=off tank/studio/backstage
sudo zfs create -o recordsize=128K -o compression=lz4    -o atime=off tank/studio/stagebox
sudo zfs create -o recordsize=128K -o compression=zstd-3 -o atime=off tank/studio/logs

# --- Das Songbook: viele kleine Textdateien, hoch komprimierbar ---
sudo zfs create -o recordsize=128K -o compression=zstd-3 -o atime=off tank/songbook
sudo zfs create -o recordsize=128K -o compression=zstd-3 -o atime=off tank/songbook/vault
sudo zfs create -o recordsize=128K -o compression=zstd-3 -o atime=off tank/songbook/inbox
sudo zfs create -o recordsize=1M   -o compression=off    -o atime=off tank/songbook/attachments

# --- Das Bandtagebuch ---
sudo zfs create -o recordsize=128K -o compression=zstd-3 -o atime=off tank/tapes

# --- Kontrolle ---
zfs list -o name,used,avail,recordsize,compression,atime -r tank/studio tank/songbook tank/tapes

Ein Skript, das alles auf einmal macht

Wenn du es lieber in einem Rutsch hast: Das Skript unten legt die komplette Struktur an, setzt die Rechte, initialisiert das Bandtagebuch und schreibt eine maschinenlesbare Karte der Struktur weg. Es hat einen Schutzwall – es weigert sich, in einem Ordner zu arbeiten, in dem bereits fremde Daten liegen, und zeigt dir, was es gefunden hat.

studio-bootstrap.sh – erst trocken, dann scharf bash
# Erst schauen, was passieren wuerde - schreibt nichts
./studio-bootstrap.sh --root /mnt/tank --mode zfs --dry-run

# Wenn die Ausgabe passt: scharf schalten
sudo ./studio-bootstrap.sh --root /mnt/tank --mode zfs --pool tank

# Kontrolle
zfs list -o name,recordsize,compression,atime -r tank/studio tank/songbook tank/tapes
cat /mnt/tank/studio/studio-map.json | jq '.paths | length'
TAKE 03

Backline

Wer darf wohin schreiben? Der Take, den alle überspringen – und der als einziger erklärt, warum später Container nicht starten.

TrueNAS-Apps laufen als Benutzerkennung 568. Wenn ein Container in ein Verzeichnis schreiben soll, das root gehört, bekommst du permission denied – und weil Container-Logs oft nur „failed to start“ sagen, suchst du an der falschen Stelle.

Rechte setzen und prüfen bash
# Der Regelfall: alles, was Apps beschreiben duerfen
sudo chown -R 568:568 /mnt/tank/studio /mnt/tank/songbook /mnt/tank/tapes

# Verzeichnisse 775, Dateien 664 - ein pauschales "chmod -R 775" wuerde
# jede Textdatei ausfuehrbar machen und die feineren Rechte darunter
# gleich wieder ueberschreiben.
sudo find /mnt/tank/studio /mnt/tank/songbook -type d -exec chmod 775 {} +
sudo find /mnt/tank/studio /mnt/tank/songbook -type f -exec chmod 664 {} +

# Die Ausnahmen - hier wird die Regel bewusst gebrochen:
sudo chmod 700 /mnt/tank/studio/stagebox    # Secrets: nur du
sudo chmod 755 /mnt/tank/songbook/vault     # Vault: Indexer liest nur
sudo chmod 775 /mnt/tank/songbook/inbox     # Inbox: Agenten duerfen schreiben

# Kontrolle - das -n ist wichtig, es zeigt die Zahlen statt der Namen
ls -ln /mnt/tank/studio/
ls -ld /mnt/tank/studio/stagebox /mnt/tank/songbook/vault /mnt/tank/songbook/inbox

Die Schreibordnung

Eine Regel, die sich durch das ganze Studio zieht und die du dir merken solltest: Es gibt genau zwei Orte, an denen automatisch geschrieben wird.

OrtWer schreibtRegel
songbook/inbox/Agenten, AutomatikAlles Maschinengemachte landet hier. Du sichtest und verschiebst.
studio/stagebox/nur duSchlüssel und Passwörter. Modus 700. Kein Container greift hier hinein.
songbook/vault/nur SyncthingIndexer und Agenten lesen. Nie schreiben.
tapes/nur git pushHistorie ist unveränderlich. Das ist der ganze Punkt.

Der Grund ist nicht Pedanterie. Wenn ein Agent direkt in deinen Vault schreiben darf, hast du nach drei Wochen Notizen, von denen du nicht mehr weißt, ob du sie geschrieben hast oder eine Maschine. Die Inbox löst das: Maschinengeschriebenes ist erkennbar, bis du es bewusst übernimmst.

Ein Sicherheitsnetz beim Einrichten

Bevor du in den nächsten Takes anfängst, Container zu installieren und Konfigurationen zu ändern: ein einzelner Handgriff, mit dem du jede Änderung zurückdrehen kannst. Das kostet keinen Platz und keine Zeit – ZFS legt nur einen Merkpunkt an.

Vor jeder größeren Änderung bash
# Merkpunkt setzen, rekursiv ueber alles was zaehlt
sudo zfs snapshot -r tank/studio@vor-$(date +%Y%m%d-%H%M)

# Nachsehen, was da ist
zfs list -t snapshot -r tank/studio | tail -5

# In einen Merkpunkt hineinsehen, ohne irgendetwas zurueckzudrehen:
ls /mnt/tank/songbook/vault/.zfs/snapshot/
cp /mnt/tank/songbook/vault/.zfs/snapshot/<name>/Projekte/notiz.md ~/

# Wenn alles gut lief, wieder aufraeumen.
# IMMER erst mit -n trocken laufen lassen und die Ausgabe lesen:
# ein vergessenes "@..." macht daraus "zfs destroy -r tank/studio"
# und loescht das gesamte Studio mit allen Kindern.
sudo zfs destroy -rn tank/studio@vor-20260729-2100   # nur anzeigen
sudo zfs destroy -r  tank/studio@vor-20260729-2100   # wirklich loeschen
TAKE 04

Rack

Ollama einbauen. Und die Frage, die im Netz fast immer falsch beantwortet wird: rechnet die CPU oder die Grafikeinheit?

Ollama ist dein Instrumenten-Rack. Es hält Modelle spielbereit, lädt sie bei Bedarf nach, wirft sie raus, wenn der Platz knapp wird, und spricht eine Schnittstelle, die praktisch jedes Werkzeug versteht. Es hat allerdings keine Anmeldung – wer den Port erreicht, kann Modelle löschen und die Maschine in den Speichernotstand fahren. Deshalb bekommt es keinen Port.

Weggabelung

Weggabelung 2 · Wer rechnet?

Dein Gerät hat eine integrierte Grafikeinheit. Die Frage, ob sie beim Rechnen hilft, wird im Netz gern mit „klar, GPU ist schneller“ beantwortet. Hier ist sie es nicht – und der Grund ist lehrreich genug, dass du ihn einmal gesehen haben solltest.

Variante A: CPU-onlyEMPFOHLENDer Amp · N1504 Kerne, 1 Speicherkanal~20 GB/s nettoOllama · CPUOLLAMA_IGPU_ENABLEnicht gesetztiGPU · 24 EUbleibt frei fuerJellyfin-Transcoding3 Musiker warmgranite 3B · qwen 2Bembeddinggemma8-12 tok/svorhersagbar,korrektWarum: Tokens erzeugen ist bandbreitenbegrenzt, nicht rechenbegrenzt.CPU und iGPU teilen sich denselben einen Speicherkanal - die iGPU kanngar nicht schneller sein. Gemessen: 2,9 tok/s Vulkan gegen 2,1 tok/s CPU.Und: auf genau dieser Grafikgeneration ist stiller Datenmuell dokumentiert.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Wörter entstehen so schnell, wie die Gewichte durch den Speicher passen – nicht so schnell, wie gerechnet wird. CPU und iGPU hängen am selben einen Speicherkanal. Die iGPU kann strukturell nicht schneller sein.

Gemessen an vergleichbarer Hardware, mit einem großen 7B-Modell: 2,9 Wörter/s über Vulkan gegen 2,1 über die CPU. Das ist keine Größenordnung, das ist Rauschen – und bei der Vorverarbeitung liegt die CPU sogar vorn.

Der Nebeneffekt ist der eigentliche Gewinn: die Grafikeinheit bleibt frei für das, worin sie tatsächlich gut ist – Videotranscoding für Jellyfin, Objekterkennung für Frigate.

compose.yml – CPU (Ausschnitt) yaml
  ollama:
    image: ollama/ollama:0.32.5
    container_name: studio-rack
    restart: unless-stopped
    ports:
      - "127.0.0.1:11434:11434"   # NUR Loopback - erreichbar vom NAS, nicht aus dem LAN
    volumes:
      - type: bind
        source: /mnt/tank/studio/rack
        target: /root/.ollama
    environment:
      OLLAMA_VULKAN: "0"          # iGPU ausdruecklich abschalten, siehe Variante B
      OLLAMA_IGPU_ENABLE: "0"
      OLLAMA_MAX_LOADED_MODELS: "3"
      OLLAMA_NUM_PARALLEL: "1"
      OLLAMA_CONTEXT_LENGTH: "4096"
      OLLAMA_KEEP_ALIVE: "30m"
      OLLAMA_LOAD_TIMEOUT: "20m"
      OLLAMA_FLASH_ATTENTION: "1"
      OLLAMA_KV_CACHE_TYPE: "q8_0"
      OLLAMA_NO_CLOUD: "1"
    deploy:
      resources:
        limits: { cpus: "3.5", memory: 9G }
Variante B: iGPU / VulkanNUR MIT TESTDer Amp · N1504 Kerne, 1 Speicherkanal~20 GB/s nettoOllama · VulkanOLLAMA_IGPU_ENABLE=1/dev/dri gemountetiGPU · 24 EUjetzt belegt -Transcoding konkurriert3 Musiker warmgleiche Modelle,gleiche Groesse~3 tok/sund teilsGarbage-Outputshared memDer Scheduler verwirft die iGPU ohne OLLAMA_IGPU_ENABLE=1 stillschweigend -das steht in keiner Doku, nur im Quelltext. Selbst mit dem Schalter bleibtdie Bandbreite gleich; Vulkan meldet ohne Sonderrechte falschen freien Speicher.Risiko: falsche Antworten ohne Fehlermeldung. Nur mit Qualitaetstest fahren.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Wenn du es trotzdem sehen willst: die beiden Schalter unten sind der Hebel. Ihr Standardverhalten hat sich zwischen den Ollama-Versionen mehrfach geändert – mal wurde die integrierte Grafik stillschweigend verworfen, mal war Vulkan von sich aus an und ließ sich schwer abschalten. Deshalb steht in beiden Varianten dieses Guides ein ausdrücklicher Wert: ollama ps in der Spalte PROCESSOR ist die einzige verlässliche Kontrolle darüber, was tatsächlich rechnet.

Das ernstere Problem: Für genau diese Grafikgeneration ist stiller Datenmüll gemeldet – korrekt aussehende Antworten, die inhaltlich Unsinn sind, ohne jede Fehlermeldung im Protokoll. Wenn du testest, dann mit einer Frage, deren Antwort du kennst. Ein Geschwindigkeitstest deckt diesen Fehler nicht auf.

compose.yml – iGPU/Vulkan (Ausschnitt) yaml
  ollama:
    image: ollama/ollama:0.32.5
    container_name: studio-rack
    restart: unless-stopped
    ports:
      - "127.0.0.1:11434:11434"
    devices:
      - /dev/dri:/dev/dri      # NUR /dev/dri - /dev/kfd waere AMD
    group_add:
      - "44"                   # video  -> stat -c '%g' /dev/dri/card0
      - "104"                  # render -> stat -c '%g' /dev/dri/renderD128
    volumes:
      - type: bind
        source: /mnt/tank/studio/rack
        target: /root/.ollama
    environment:
      OLLAMA_VULKAN: "1"
      OLLAMA_IGPU_ENABLE: "1"  # beide ausdruecklich setzen, nie auf Standard verlassen
      OLLAMA_MAX_LOADED_MODELS: "2"
      OLLAMA_NUM_PARALLEL: "1"
      OLLAMA_CONTEXT_LENGTH: "4096"
      OLLAMA_FLASH_ATTENTION: "1"
      OLLAMA_KV_CACHE_TYPE: "q8_0"
      OLLAMA_NO_CLOUD: "1"
    deploy:
      resources:
        limits: { memory: 9G }

# Danach ZWINGEND pruefen - nicht die Geschwindigkeit, die Richtigkeit:
#   docker exec studio-rack ollama ps        # Spalte PROCESSOR
#   und eine Frage stellen, deren Antwort du kennst.

Einbauen

Der Weg über die Oberfläche ist Apps → Discover → Install via YAML. Das ist wichtig: über die Shell gestartete Container kennt die TrueNAS-Verwaltung nicht – sie tauchen nicht unter Apps auf, landen nicht im Konfigurationsbackup und starten nach einem Neustart nicht zuverlässig wieder.

Vorher: Zugangsdaten in die Stagebox bash
sudo mkdir -p /mnt/tank/studio/stagebox
sudo tee /mnt/tank/studio/stagebox/.env >/dev/null <<EOF
QDRANT_API_KEY=$(openssl rand -hex 24)
QDRANT_RO_KEY=$(openssl rand -hex 24)     # fuer nur-lesende Zugriffe
WEBUI_SECRET=$(openssl rand -hex 32)      # wird zu WEBUI_SECRET_KEY im Pult
EOF
sudo chmod 600 /mnt/tank/studio/stagebox/.env
sudo chown 568:568 /mnt/tank/studio/stagebox/.env

# Zum Einsetzen ins YAML anzeigen (Install via YAML liest keine .env-Datei):
sudo cat /mnt/tank/studio/stagebox/.env

Die vollständige Compose-Datei

Bis hier hast du Ausschnitte gesehen. Das hier ist das Ganze – alle sechs Dienste, einmal einfügen unter Apps → Discover → Install via YAML, Name studio. Die späteren Takes bauen nur noch darauf auf, sie ersetzen nichts.

compose.yml – das komplette Studio yaml
services:

  # ============ DAS RACK: Ollama ============
  ollama:
    image: ollama/ollama:0.32.5
    container_name: studio-rack
    restart: unless-stopped
    ports:
      - "127.0.0.1:11434:11434"     # nur Loopback - Ollama hat keine Anmeldung
    volumes:
      - type: bind
        source: /mnt/tank/studio/rack
        target: /root/.ollama
    environment:
      TZ: Europe/Berlin
      OLLAMA_VULKAN: "0"
      OLLAMA_IGPU_ENABLE: "0"
      OLLAMA_MAX_LOADED_MODELS: "3"
      OLLAMA_NUM_PARALLEL: "1"
      OLLAMA_MAX_QUEUE: "64"
      OLLAMA_CONTEXT_LENGTH: "4096"
      OLLAMA_KEEP_ALIVE: "30m"
      OLLAMA_LOAD_TIMEOUT: "20m"
      OLLAMA_FLASH_ATTENTION: "1"
      OLLAMA_KV_CACHE_TYPE: "q8_0"
      OLLAMA_NO_CLOUD: "1"
      OLLAMA_NOHISTORY: "1"
    deploy:
      resources:
        limits: { cpus: "3.5", memory: 9G }
    networks: [studio]

  # ============ DIE LIBRARY: Qdrant ============
  qdrant:
    image: qdrant/qdrant:v1.18.3-unprivileged
    container_name: studio-library
    restart: unless-stopped
    ports:
      - "127.0.0.1:6333:6333"       # nur Loopback, fuer curl und das Dashboard
    volumes:
      - type: bind
        source: /mnt/tank/studio/library/storage
        target: /qdrant/storage
      - type: bind
        source: /mnt/tank/studio/library/snapshots
        target: /qdrant/snapshots
    environment:
      TZ: Europe/Berlin
      QDRANT__SERVICE__API_KEY: "HIER-DEN-QDRANT_API_KEY-EINSETZEN"
      QDRANT__SERVICE__READ_ONLY_API_KEY: "HIER-DEN-QDRANT_RO_KEY-EINSETZEN"
      QDRANT__SERVICE__ENABLE_TLS: "false"
      QDRANT__STORAGE__ON_DISK_PAYLOAD: "true"
      QDRANT__STORAGE__PERFORMANCE__OPTIMIZER_CPU_BUDGET: "-2"
      QDRANT__STORAGE__OPTIMIZERS__MAX_OPTIMIZATION_THREADS: "1"
      QDRANT__STORAGE__HNSW_INDEX__MAX_INDEXING_THREADS: "2"
      QDRANT__TELEMETRY_DISABLED: "true"
    ulimits:
      nofile: { soft: 10000, hard: 10000 }
    deploy:
      resources:
        limits: { memory: 1G }
    networks: [studio]

  # ============ DAS MISCHPULT: Open WebUI ============
  webui:
    image: ghcr.io/open-webui/open-webui:0.11.0
    container_name: studio-desk
    restart: unless-stopped
    ports:
      - "31028:8080"                # das einzige, was ins LAN darf
    volumes:
      - type: bind
        source: /mnt/tank/studio/desk/webui
        target: /app/backend/data
      - type: bind
        source: /mnt/tank/songbook/vault
        target: /vault
        read_only: true             # das Songbook wird gelesen, nie beschrieben
    environment:
      TZ: Europe/Berlin
      OLLAMA_BASE_URL: "http://ollama:11434"
      WEBUI_SECRET_KEY: "HIER-DEN-WEBUI_SECRET-EINSETZEN"
      WEBUI_AUTH: "true"
      ENABLE_SIGNUP: "false"
      VECTOR_DB: "qdrant"
      QDRANT_URI: "http://qdrant:6333"
      QDRANT_API_KEY: "HIER-DEN-QDRANT_API_KEY-EINSETZEN"
      RAG_EMBEDDING_ENGINE: "ollama"
      RAG_EMBEDDING_MODEL: "qwen3-embedding:0.6b"
      RAG_OLLAMA_BASE_URL: "http://ollama:11434"
    depends_on: [ollama, qdrant]
    deploy:
      resources:
        limits: { memory: 1500M }
    networks: [studio]

  # ============ DAS BANDTAGEBUCH: Forgejo ============
  forgejo:
    image: codeberg.org/forgejo/forgejo:16.0.1
    container_name: studio-tapes
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "222:22"
    volumes:
      - type: bind
        source: /mnt/tank/tapes/forgejo
        target: /data
    environment:
      USER_UID: "568"
      USER_GID: "568"
      FORGEJO__database__DB_TYPE: "sqlite3"
      FORGEJO__service__DISABLE_REGISTRATION: "true"
      FORGEJO__security__INSTALL_LOCK: "true"
      FORGEJO__actions__ENABLED: "false"
      FORGEJO__indexer__REPO_INDEXER_ENABLED: "true"
    deploy:
      resources:
        limits: { memory: 512M }
    networks: [studio]

  # ============ DER ROADIE: Syncthing ============
  syncthing:
    image: syncthing/syncthing:2.1.2
    container_name: studio-roadie
    hostname: studio-nas
    restart: unless-stopped
    ports:
      - "8384:8384"
      - "22000:22000/tcp"
      - "22000:22000/udp"
      - "21027:21027/udp"
    volumes:
      - type: bind
        source: /mnt/tank/studio/desk/syncthing
        target: /var/syncthing/config
      - type: bind
        source: /mnt/tank/songbook
        target: /var/syncthing/songbook
    environment:
      PUID: "568"
      PGID: "568"
      TZ: Europe/Berlin
    deploy:
      resources:
        limits: { memory: 256M }
    networks: [studio]

  # ============ DER AUFNAHMELEITER: Warmup ============
  warmup:
    image: curlimages/curl:8.11.1
    container_name: studio-warmup
    restart: on-failure
    depends_on: [ollama]
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        until curl -sf http://ollama:11434/api/version >/dev/null; do sleep 3; done
        curl -s http://ollama:11434/api/generate -H 'Content-Type: application/json' \
             -d '{"model":"granite4.1:3b-q4_K_M","keep_alive":-1}' >/dev/null
        curl -s http://ollama:11434/api/embed -H 'Content-Type: application/json' \
             -d '{"model":"qwen3-embedding:0.6b","input":"warmup","keep_alive":-1}' >/dev/null
        echo "Generalist und Embedder stehen bereit."
    networks: [studio]

networks:
  studio:
    name: studio
    driver: bridge
Prüfen, ob das Rack steht bash
docker ps --filter name=studio-rack
docker logs studio-rack 2>&1 | tail -20

# Antwortet es?
docker exec studio-rack ollama --version
curl -s http://localhost:11434/api/version   # nur vom NAS aus, kein Port nach aussen

# Die Diagnosezeilen, die wirklich etwas verraten:
docker logs studio-rack 2>&1 | grep -Ei "vram-based default|disabling mmap|dropping integrated|max runners"

Zwei Dinge, die kaum jemand weiß

Der Kontext ist immer 4096. Die Automatik von Ollama richtet sich ausschließlich nach erkanntem Grafikspeicher. Bei reiner CPU-Rechnung ist der null – also greift immer die unterste Stufe, egal ob die Maschine 16 oder 96 GB hat. Größere Kontexte gibt es nur pro Anfrage oder pro Preset.

Memory-Mapping ist bei CPU-Rechnung standardmäßig aus. Die Gewichte liegen dann fest im Speicher statt als Datei-Abbild. Für selten gebrauchte Spezialisten lohnt es sich, das wieder einzuschalten – dann darf der Kernel die Seiten verwerfen, statt dass dir der Speicher ausgeht. Im Preset: PARAMETER use_mmap true.

TAKE 05

Musiker

Fünf Spezialisten statt eines Alleskönners – und wie du dir aus einem Modell beliebig viele baust.

Jetzt kommt der Teil, der Spaß macht. Du stellst deine Band zusammen. Und weil deine Kiste keine Diva verträgt, nimmst du fünf Musiker, die je eine Sache können – aber die richtig.

RolleModellGrößeKannImmer warm?
Hausmeistergranite4.1:3b-q4_K_M2,1 GBChat, Werkzeuge, JSON, Deutschja
Zweitmeinungqwen3.5:2b-q4_K_M1,9 GBChat, Bilder, Nachdenken, 256K30 Min
Notenschreiberqwen3-embedding:0.6b0,64 GBEinbettungen für die Sucheja
Setzerqwen2.5-coder:3b1,9 GBCode, Vervollständigung30 Min
Notenleserglm-ocr:q8_01,6 GBPapier lesen, Tabellennein

Warum granite als Hausmeister? Weil IBM als einziger Hersteller in dieser Größenklasse Deutsch namentlich als unterstützte Sprache führt – nicht „multilingual“ im Kleingedruckten, sondern explizit. Dazu Apache-2.0 und ein Training, das ausdrücklich auf Werkzeugaufrufe und strukturierte Ausgaben zielt. Genau das brauchst du für Agenten.

Die Band engagieren bash
# Hausmeister: Generalist, Deutsch, Werkzeuge, JSON        2,1 GB
docker exec studio-rack ollama pull granite4.1:3b-q4_K_M

# Zweitmeinung: 256K Kontext, Bilder, Nachdenken           1,9 GB
docker exec studio-rack ollama pull qwen3.5:2b-q4_K_M

# Notenschreiber: Einbettungen fuer die Suche              639 MB
docker exec studio-rack ollama pull qwen3-embedding:0.6b

# Setzer: Code                                             1,9 GB
docker exec studio-rack ollama pull qwen2.5-coder:3b

# Notenleser: OCR, nur bei Bedarf                          1,6 GB
docker exec studio-rack ollama pull glm-ocr:q8_0

# Wer ist da?
docker exec studio-rack ollama list

Presets – aus einem Modell werden fünf

Hier wird es elegant. Ein Modelfile ist ein Preset: dasselbe Basismodell, andere Regler, andere Anweisung. Und weil Ollama die Gewichte nach Inhalt adressiert, kostet ein Preset ein paar Kilobyte – die 2,1 GB werden nicht kopiert.

Der Archivar – reißt Struktur aus Text dockerfile
# /mnt/tank/studio/rack/presets/Modelfile.archivar
FROM granite4.1:3b-q4_K_M
PARAMETER temperature 0
PARAMETER top_p 1.0
PARAMETER top_k 1
PARAMETER num_ctx 8192
PARAMETER num_predict 512
PARAMETER num_thread 3
PARAMETER repeat_penalty 1.0
SYSTEM """Du bist ein Extraktionsdienst. Du gibst AUSSCHLIESSLICH gueltiges JSON
zurueck, das exakt dem vom Aufrufer gelieferten Schema entspricht. Kein Fliesstext,
keine Codefences, keine Erklaerung. Fehlende Werte sind null - niemals erfunden."""
MESSAGE user Schema: {"titel":"string","tags":["string"],"jahr":"number"} -- Text: Notiz ueber TrueNAS-Setup von 2026.
MESSAGE assistant {"titel":"TrueNAS-Setup","tags":["truenas","homeserver"],"jahr":2026}
Der Kurator – fasst deutsche Fachtexte zusammen dockerfile
# /mnt/tank/studio/rack/presets/Modelfile.kurator
FROM granite4.1:3b-q4_K_M
PARAMETER temperature 0.3
PARAMETER top_p 0.9
PARAMETER num_ctx 16384
PARAMETER num_predict 900
PARAMETER num_batch 256
PARAMETER use_mmap true
SYSTEM """Du fasst deutsche Fachtexte zusammen. Antworte auf Deutsch, hoechstens
fuenf Stichpunkte, sachlich, ohne Einleitungsfloskel. Was du nicht im Text findest,
erfindest du nicht - du schreibst 'steht nicht im Text'."""
Presets bauen und ausprobieren bash
# Die Presets liegen auf dem Host unter /mnt/tank/studio/rack/presets.
# Im Container sind sie damit unter /root/.ollama/presets sichtbar -
# denn genau dorthin ist /mnt/tank/studio/rack gemountet.
docker exec studio-rack ollama create archivar -f /root/.ollama/presets/Modelfile.archivar
docker exec studio-rack ollama create kurator  -f /root/.ollama/presets/Modelfile.kurator
docker exec studio-rack ollama list

# Der Archivar bei der Arbeit - mit erzwungenem Schema
curl -s http://localhost:11434/api/chat -H 'Content-Type: application/json' -d '{
  "model": "archivar",
  "messages": [{"role":"user","content":"Notiz: TrueNAS auf dem Beelink eingerichtet, Juli 2026, laeuft mit Tailscale."}],
  "stream": false, "think": false,
  "options": { "temperature": 0 },
  "format": {
    "type": "object",
    "properties": {
      "titel": {"type":"string"},
      "tags":  {"type":"array","items":{"type":"string"}},
      "jahr":  {"type":"integer"}
    },
    "required": ["titel","tags","jahr"]
  }
}' | jq -r '.message.content' | jq .

Warm halten und einpegeln

Wer steht auf der Bühne? bash
# Zwei Musiker dauerhaft pinnen (-1 = bleibt, bis du ihn gehen laesst)
curl -s http://localhost:11434/api/generate -H 'Content-Type: application/json' \
     -d '{"model":"granite4.1:3b-q4_K_M","keep_alive":-1}'
curl -s http://localhost:11434/api/embed -H 'Content-Type: application/json' \
     -d '{"model":"qwen3-embedding:0.6b","input":"warmup","keep_alive":-1}'

# Den Notenleser nach Gebrauch sofort wieder wegschicken
docker exec studio-rack ollama stop glm-ocr:q8_0

# Der Blick aufs Pult
docker exec studio-rack ollama ps
# NAME                     ID            SIZE     PROCESSOR   CONTEXT   UNTIL
# granite4.1:3b-q4_K_M     a1b2c3d4e5f6  2.6 GB   100% CPU    4096      Forever

Die Spalte CONTEXT ist deine Kontrolle: sie zeigt, wie viel Kontext tatsächlich reserviert wurde. Steht dort ein Vielfaches von 4096, hat jemand OLLAMA_NUM_PARALLEL hochgedreht – und dein Speicher geht dafür drauf.

TAKE 06

Library

Qdrant. Damit dein Server findet, was du vor zwei Jahren notiert hast – auch wenn du andere Wörter benutzt.

Eine Volltextsuche findet, was du wörtlich geschrieben hast. Eine Vektorsuche findet, was du gemeint hast. Du suchst nach „Server von unterwegs erreichbar machen“ und bekommst deine Notiz über Tailscale – obwohl darin weder „unterwegs“ noch „erreichbar“ steht.

Qdrant ist dafür überraschend genügsam. Im Test mit 200 Einträgen: 37 MB Arbeitsspeicher. Auch mit hunderttausend Textstücken bleibt es bei rund 140 MB, wenn man es richtig einstellt. Qdrant ist nicht dein Engpass – deine Modelle sind es.

Die Library einrichten bash
# Nach dem ersten Start ZUERST das hier pruefen:
docker logs studio-library 2>&1 | grep -i "filesystem"
# Keine Ausgabe = alles gut.
# Steht dort etwas, hat Qdrant den Start verweigert - dann NICHT mit
# irgendwelchen Schaltern erzwingen, sondern die Ablage aendern.

# Antwortet sie?
K=$(sudo grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2)
curl -s -H "api-key: $K" http://localhost:6333/healthz
curl -s -H "api-key: $K" http://localhost:6333/ | jq

# Ohne Schluessel muss 401 kommen - sonst steht die Tuer offen:
curl -s -o /dev/null -w "ohne Schluessel: HTTP %{http_code}\n" http://localhost:6333/collections
Die Sammlung anlegen bash
K=$(sudo grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2)

curl -X PUT http://localhost:6333/collections/songbook \
  -H "api-key: $K" -H 'Content-Type: application/json' -d '{
  "vectors": {
    "size": 1024,
    "distance": "Cosine",
    "on_disk": true
  },
  "quantization_config": {
    "scalar": { "type": "int8", "quantile": 0.99, "always_ram": true }
  },
  "on_disk_payload":   true,
  "hnsw_config":       { "m": 16, "ef_construct": 100 },
  "optimizers_config": { "indexing_threshold": 20000 }
}'

# Filterfelder indizieren - sonst wird jede gefilterte Suche zur Schleppnetzfahrt
for f in path:keyword tags:keyword mtime:integer; do
  curl -s -X PUT http://localhost:6333/collections/songbook/index \
    -H "api-key: $K" -H 'Content-Type: application/json' \
    -d "{\"field_name\":\"${f%%:*}\",\"field_schema\":\"${f##*:}\"}"
done

Die drei Einstellungen, die den Unterschied machen

EinstellungWas sie tutWas sie bringt
on_disk: trueVektoren als Datei-Abbild statt im SpeicherSpeicherbedarf sinkt drastisch
scalar int8Vektoren auf ein Viertel eingedampftFaktor 4 weniger, ~1 % Fehler
on_disk_payloadNutztext auf der Platteder Text zählt nicht gegen den Speicher

Von binärer Quantisierung – Faktor 32 – lässt du hier die Finger. Sie ist erst deutlich oberhalb von tausend Dimensionen zuverlässig; die dokumentierten Erfolgsfälle liegen alle bei 1536 und mehr. Bei 768 oder 1024 ist sie bestenfalls grenzwertig.

Ausprobieren

Ein Textstück ablegen und wiederfinden bash
K=$(sudo grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2)

# 1. Text einbetten lassen
V=$(curl -s http://localhost:11434/api/embed -H 'Content-Type: application/json' -d '{
  "model": "qwen3-embedding:0.6b",
  "input": "Tailscale macht den Server von unterwegs erreichbar, ohne einen Port zu oeffnen.",
  "truncate": false, "keep_alive": "24h"
}' | jq -c '.embeddings[0]')

# 2. Ablegen
curl -s -X PUT "http://localhost:6333/collections/songbook/points?wait=true" \
  -H "api-key: $K" -H 'Content-Type: application/json' -d "{
  \"points\": [{ \"id\": 1, \"vector\": $V,
    \"payload\": {\"path\":\"Projekte/homeserver.md\",\"tags\":[\"netzwerk\"],\"text\":\"Tailscale ...\"} }]}"

# 3. Mit ANDEREN Worten suchen
Q=$(curl -s http://localhost:11434/api/embed -H 'Content-Type: application/json' \
    -d '{"model":"qwen3-embedding:0.6b","input":"wie komme ich von aussen auf mein NAS"}' | jq -c '.embeddings[0]')

curl -s -X POST http://localhost:6333/collections/songbook/points/query \
  -H "api-key: $K" -H 'Content-Type: application/json' \
  -d "{\"query\": $Q, \"limit\": 3, \"with_payload\": [\"path\",\"tags\"]}" | jq '.result.points'

Sichern

Ein ZFS-Snapshot kann Qdrant mitten im Schreiben erwischen. Die eigene Snapshot-Schnittstelle liefert dagegen einen in sich stimmigen Stand mit Prüfsumme. Beides zusammen ist die richtige Antwort.

Qdrant-Snapshot als Cron-Aufgabe bash
#!/usr/bin/env bash
# TrueNAS: System Settings > Advanced > Cron Jobs, taeglich 03:10
set -euo pipefail
K=$(grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2)

curl -sf -X POST "http://localhost:6333/collections/songbook/snapshots" -H "api-key: $K" \
  | tee -a /mnt/tank/studio/logs/qdrant-snapshots.log

# Aelter als 14 Tage wegraeumen - Snapshots sind Vollkopien
find /mnt/tank/studio/library/snapshots -name '*.snapshot' -mtime +14 -delete
TAKE 07

Songbook

Obsidian auf dem NAS – und die Falle, in die fast jeder einmal tappt.

Die richtige Aufteilung ist einfach: Der Vault lebt auf deinem Gerät. Das NAS ist ein gleichberechtigter Sync-Partner, keine Arbeitskopie. Von dieser NAS-Kopie liest der Indexer, und in sie committet Git – beides nur lesend beziehungsweise nebenläufig.

Weggabelung

Weggabelung 3 · Wie kommen die Notizen aufs NAS?

Zwei Wege, und die Entscheidung hängt an genau einer Frage: Ist ein iPhone oder iPad im Spiel?

Variante A: Syncthing + GitEMPFOHLENLaptopVault liegt lokalAndroidVault liegt lokalSyncthing18 MB Image~40 MB RAMNAS-KopieSync-Peer,keine ArbeitskopieIndexerliest NURGitHistorie &Audit-Logread-onlycommitDateien bleiben ueberall schlichtes Markdown. Faellt der Sync aus, hast du auf jedemGeraet weiterhin lesbare Notizen - kein Datenbankformat, aus dem man erst wiederherausmuss. Historie und Konfliktloesung liefert Git, das du ohnehin willst.Einziger Haken: iOS unterstuetzt Syncthing nicht.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Syncthing gleicht Ordner zwischen Geräten ab. Kein Server, keine Datenbank, kein Konto. Auf jedem Gerät liegt am Ende dasselbe schlichte Markdown.

Das ist der eigentliche Vorteil: Wenn Syncthing morgen verschwindet, hast du auf jedem Gerät weiterhin lesbare Textdateien. Kein Exportformat, aus dem du erst wieder herausmusst.

Historie und Konfliktlösung liefert Git – das brauchst du für den Prüfpfad ohnehin. Zwei Werkzeuge, jedes gut in einer Sache.

Der Haken: Auf iOS läuft Syncthing nicht. Apple lässt Hintergrunddienste dieser Art nicht zu.

Syncthing – compose-Ausschnitt yaml
  syncthing:
    image: syncthing/syncthing:2.1.2
    container_name: studio-roadie
    hostname: studio-nas
    restart: unless-stopped
    ports:
      - "8384:8384"          # Oberflaeche, nur im LAN
      - "22000:22000/tcp"
      - "22000:22000/udp"
      - "21027:21027/udp"    # Geraeteerkennung
    volumes:
      - type: bind
        source: /mnt/tank/studio/desk/syncthing
        target: /var/syncthing/config
      - type: bind
        source: /mnt/tank/songbook
        target: /var/syncthing/songbook
    environment:
      PUID: "568"
      PGID: "568"
      TZ: Europe/Berlin

# Danach: http://<nas>:8384 oeffnen, Ordner /var/syncthing/songbook/vault
# anlegen, Geraete koppeln. Ignore-Patterns nicht vergessen:
#   .obsidian/workspace.json
#   .obsidian/workspace-mobile.json
#   .obsidian/plugins/*/data.json
#   .trash
#   .stversions
Variante B: Self-hosted LiveSyncNUR FUER iOSLaptopObsidian-PluginiPhone / iPadObsidian-PluginCouchDBeigene Datenbank~250 MB RAMChunk-Storekein Klartext-Markdown mehrExport noetigfuer Indexer undGit erst ausleitenDer einzige Grund fuer diesen Weg heisst iOS. Dafuer handelst du dir eine Datenbank ein,die eingerichtet, abgesichert und gesichert werden will - und die Notizen liegenauf dem NAS nicht mehr als lesbare Dateien, sondern als Chunks.Das Projekt warnt selbst: vor Installation und vor jedem Update den Vault sichern.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Self-hosted LiveSync ist ein Obsidian-Plugin, das gegen eine CouchDB repliziert. Es läuft auf iOS, weil es innerhalb von Obsidian arbeitet statt als Systemdienst.

Es gleicht auf Textstück-Ebene ab, also auch bei geöffneter Datei, und bringt eine Oberfläche zum Zusammenführen von Konflikten mit. Für mehrere Personen an einem Vault wäre das die richtige Wahl.

Der Preis: eine zusätzliche Datenbank, die eingerichtet, abgesichert und gesichert werden will – die Einrichtung braucht sogar eine eigene Laufzeitumgebung. Und auf dem NAS liegen dann keine lesbaren Notizen mehr, sondern Chunks. Für Indexer und Git musst du erst wieder ausleiten.

Das Projekt selbst warnt: vor Installation und vor jedem Update den Vault sichern.

CouchDB für LiveSync – compose-Ausschnitt yaml
  couchdb:
    image: couchdb:3.5.2
    container_name: studio-couch
    user: "5984:5984"        # WICHTIG - sonst gehoert das Verzeichnis falsch
    restart: unless-stopped
    ports:
      - "5984:5984"
    volumes:
      - type: bind
        source: /mnt/tank/studio/desk/couchdb/data
        target: /opt/couchdb/data
      - type: bind
        source: /mnt/tank/studio/desk/couchdb/etc
        target: /opt/couchdb/etc/local.d
    environment:
      COUCHDB_USER: "kingp"
      COUCHDB_PASSWORD: "HIER-EIN-LANGES-ZUFALLSPASSWORT"
    deploy:
      resources:
        limits: { memory: 512M }

# Vorbereitung ZWINGEND vor dem ersten Start:
#   sudo mkdir -p /mnt/tank/studio/desk/couchdb/{data,etc}
#   sudo chown -R 5984:5984 /mnt/tank/studio/desk/couchdb
# Danach Initialisierung nach Anleitung des Plugins, dann im Plugin verbinden.

Der Vault als Struktur

Ordnerstruktur ist Geschmackssache – aber zwei Dinge zahlen sich handfest aus, weil der Indexer sie nutzt.

Frontmatter: der Kopf jeder Notiz markdown
---
tags: [homeserver, truenas, ki]
datum: 2026-07-29
status: aktiv
---

# Homeserver-Aufbau

## Speicher

ZFS mit sechs Steckplaetzen. Die recordsize haengt vom Zweck ab.

## Netzwerk

Tailscale sorgt fuer den Zugang von unterwegs, ohne einen Port zu oeffnen.
Siehe auch [[Tailscale-Einrichtung]].

Obsidian mit dem eigenen Rack verbinden

Es gibt Plugins, die direkt mit Ollama sprechen. Die Adresse ist dann dein NAS statt einer Cloud – aber ein Vorbehalt: dein Rack hat keine Anmeldung. Wenn du es aus Obsidian erreichbar machst, dann über Tailscale (Take 10), nicht über eine Portfreigabe im Router.

TAKE 08

Tagebuch

Git als Prüfpfad. Jede Änderung nachvollziehbar – auch die, die du längst wieder gelöscht hast.

Syncthing gleicht ab, aber es erinnert sich nicht. Snapshots erinnern sich, aber sie erklären nichts. Git kann beides: es weiß, was sich wann geändert hat – und es findet Inhalte, die es heute gar nicht mehr gibt.

Das ist besonders wertvoll, sobald Agenten mitschreiben. Wenn eine Notiz plötzlich anders aussieht, willst du in zehn Sekunden sehen: warst du das, oder war es die Maschine?

Das Bandtagebuch anlegen bash
VAULT=/mnt/tank/songbook/vault
BARE=/mnt/tank/tapes/songbook.git

# Ein nacktes Repo als Ziel
sudo -u apps git init --bare -b main "$BARE"

# Die Arbeitskopie
cd "$VAULT"
sudo -u apps git init -b main
sudo -u apps git config user.name  "studio-bot"
sudo -u apps git config user.email "studio@localhost"
sudo -u apps git config gc.auto 256
sudo -u apps git remote add origin "$BARE"

# WICHTIG: Git verweigert den Dienst, wenn Repo und aufrufender Nutzer
# nicht zusammenpassen. Erst initialisieren, dann uebereignen - und
# sicherheitshalber ausdruecklich freigeben:
sudo git config --global --add safe.directory "$VAULT"
.gitignore – was nicht in die Historie gehört bash
cat > /mnt/tank/songbook/vault/.gitignore <<'EOF'
# geraete-/sitzungsspezifisch - nie committen
.obsidian/workspace.json
.obsidian/workspace-mobile.json
.obsidian/cache/
# ACHTUNG: hier liegen API-Schluessel von LLM-Plugins im KLARTEXT.
# Der Kommentar MUSS in einer eigenen Zeile stehen - .gitignore kennt
# keine Kommentare am Zeilenende, sonst wird er Teil des Musters und
# die Datei landet trotzdem im Repo.
.obsidian/plugins/*/data.json

# Plugin-Caches, die gross werden
.smart-env/
.smart-connections/

# Sync-Artefakte
.stfolder/
.stversions/
*.sync-conflict-*

# Betriebssystem
.DS_Store
Thumbs.db
desktop.ini
.trash/

# Secrets
.env
*.key
EOF

Die restliche .obsidian-Konfiguration – Tastenkürzel, Erscheinungsbild – solltest du versionieren. Dann ist ein frisch aufgesetzter Rechner nach einem git clone sofort so eingerichtet, wie du es gewohnt bist.

Das Tagebuch schreibt sich selbst

vault-autocommit.sh bash
#!/usr/bin/env bash
# TrueNAS: System Settings > Advanced > Cron Jobs, alle 30 Minuten
set -euo pipefail
VAULT="${1:-/mnt/tank/songbook/vault}"
LOG="/mnt/tank/studio/logs/vault-git.log"
G(){ git -c "safe.directory=$VAULT" -C "$VAULT" "$@"; }

# Nicht mitten in einen Syncthing-Schreibvorgang hineincommitten:
# warten, bis 20 Sekunden lang nichts mehr angefasst wurde.
for _ in 1 2 3 4 5; do
  RECENT=$(find "$VAULT" -newermt '-20 seconds' -type f -not -path '*/.git/*' | head -1)
  [ -z "$RECENT" ] && break
  sleep 10
done

G add -A
G diff --cached --quiet && exit 0        # nichts passiert, nichts zu tun

STAT=$(G diff --cached --shortstat)
COUNT=$(G diff --cached --name-only | wc -l)
FILES=$(G diff --cached --name-only | head -3 | xargs -I{} basename {} | paste -sd, - | sed 's/,/, /g')
[ "$COUNT" -gt 3 ] && FILES="$FILES (+$((COUNT-3)) weitere)"

G commit -q -m "songbook: $FILES" -m "$STAT"
G push -q --follow-tags origin HEAD 2>>"$LOG" || echo "$(date '+%F %T') push fehlgeschlagen" >> "$LOG"
echo "$(date '+%F %T') commit: $FILES | $STAT" >> "$LOG"

# Wochenmarke als Ankerpunkt in der Historie
WEEK="w$(date +%G-%V)"
G rev-parse "$WEEK" >/dev/null 2>&1 || G tag -a "$WEEK" -m "Wochenstand KW $(date +%V)"

Was du damit beantworten kannst

Das Tagebuch befragen bash
cd /mnt/tank/songbook/vault
G(){ git -c safe.directory=$PWD "$@"; }

# Was hat sich diese Woche getan?
G log --since="1 week ago" --name-only --pretty=format:'%C(yellow)%h%C(reset) %ad %s' --date=short

# Welche Notizen aendern sich am haeufigsten? Das sind deine Baustellen.
G log --pretty=format: --name-only | grep -v '^$' | sort | uniq -c | sort -rn | head -20

# Der eigentliche Trumpf: Volltextsuche ueber die GESCHICHTE.
# Findet Inhalte, die es heute nicht mehr gibt.
G log -S "Tailscale" --oneline --name-only

# Eine einzelne Notiz ueber die Zeit
G log -p --follow -- "Projekte/homeserver.md"

# Was wurde jemals geloescht?
G log --diff-filter=D --name-only --pretty=format:'%ad %h' --date=short

# Wieviel ist in 30 Tagen dazugekommen?
G diff --stat "@{30 days ago}" HEAD | tail -1

git log -S ist der Grund für den ganzen Aufwand. Es beantwortet die Frage „Ich hatte das doch mal aufgeschrieben und dann rausgeworfen“ – und das kann dir weder Syncthing noch ein Snapshot noch dein Vektorindex beantworten.

Forgejo – die Oberfläche dazu

Optional, aber angenehm: Forgejo gibt dir Diffs im Browser, Suche über den gesamten Vault und eine Zeitleiste. Mit SQLite statt einer eigenen Datenbank und abgeschalteten Actions liegt es bei rund 150 MB Speicher.

Forgejo – compose-Ausschnitt yaml
  forgejo:
    image: codeberg.org/forgejo/forgejo:16.0.1
    container_name: studio-tapes
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "222:22"
    volumes:
      - type: bind
        source: /mnt/tank/tapes/forgejo
        target: /data
    environment:
      USER_UID: "568"
      USER_GID: "568"
      FORGEJO__database__DB_TYPE: "sqlite3"
      FORGEJO__service__DISABLE_REGISTRATION: "true"
      FORGEJO__service__REQUIRE_SIGNIN_VIEW: "true"
      FORGEJO__security__INSTALL_LOCK: "true"
      FORGEJO__actions__ENABLED: "false"          # CI auf 4 Kernen waere Unfug
      FORGEJO__indexer__REPO_INDEXER_ENABLED: "true"   # Volltextsuche ueber den Vault
    deploy:
      resources:
        limits: { memory: 512M }
Forgejo-Zugang anlegen bash
docker exec -u git studio-tapes forgejo admin user create \
  --admin --username kingp --email kingp@localhost \
  --password "EIN-LANGES-PASSWORT" \
  --config /data/gitea/conf/app.ini

# Danach anmelden unter http://<nas-ip>:3000
# Dort ein leeres Repository "songbook" anlegen und den Vault dorthin schieben:
cd /mnt/tank/songbook/vault
git -c safe.directory=$PWD remote set-url origin http://<nas-ip>:3000/kingp/songbook.git
git -c safe.directory=$PWD push -u origin main
TAKE 09

Mischpult

Open WebUI als Oberfläche – und die ehrliche Antwort darauf, was „Agenten“ auf dieser Hardware bedeutet.

Open WebUI ist deine Oberfläche – und zusammen mit Forgejo, Syncthing und (falls gewählt) n8n eines der wenigen Stücke, die überhaupt einen Port bekommen. Rack und Library bekommen keinen, die sind nur im internen Container-Netz erreichbar. Es bringt Chat, Wissensanbindung, Modell-Voreinstellungen, Werkzeuge und MCP-Unterstützung in einem Container mit – ein Werkzeug statt vier.

Das Pult in Betrieb nehmen bash
# Nach dem Start: http://<nas-ip>:31028
# Der ERSTE angelegte Zugang wird automatisch Verwalter. Also gleich selbst anlegen,
# bevor jemand anderes im LAN schneller ist.

docker logs studio-desk 2>&1 | tail -20
curl -s -o /dev/null -w "WebUI: HTTP %{http_code}\n" http://localhost:31028

# Sieht das Pult das Rack?
docker exec studio-desk curl -s http://ollama:11434/api/tags | head -c 300

Modell-Voreinstellungen: dieselbe Idee wie Presets, nur im Browser

Unter Workspace → Models baust du dir Arbeitsplätze. Ein Modell, ein Systemprompt, eine Auswahl an Werkzeugen, eine Wissenssammlung. Das ergibt Einträge in der Modellauswahl, zwischen denen du wechselst wie zwischen Kanälen.

ArbeitsplatzBasisAngebunden anWofür
Hausmeistergranite4.1:3bSongbook-Wissen„was habe ich zu X notiert?“
Kuratorgranite4.1:3blangen Text zu fünf Punkten eindampfen
Setzerqwen2.5-coder:3bSkripte, Compose-Dateien
Notenleserglm-ocr:q8_0Rechnung abfotografiert, Text raus
Weggabelung

Weggabelung 4 · Wer füllt die Library?

Irgendetwas muss regelmäßig durch den Vault gehen, geänderte Notizen zerlegen, einbetten und nach Qdrant schieben. Zwei Wege – der Unterschied ist Sichtbarkeit gegen Schlankheit.

Variante B: schlankes Python-SkriptSCHLANKSongbookMarkdownindex.py~200 Zeilen,nur Standardbibliothek,laeuft im CronOllama /embedembeddinggemmaQdrantupsertLogdateigrep stattKlickstreckeNull zusaetzliche Container, null RAM im Leerlauf, und der ganze Ablauf steht ineiner Datei, die du liest und veraenderst. Der Zustand liegt in einer kleinenMarken-Datei mit Zeitstempeln - nur Geaendertes wird neu eingelesen.Preis: Wiederholung und Fehlerbehandlung schreibst du selbst.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

Ein Skript, nur Standardbibliothek, läuft in der Cron-Verwaltung von TrueNAS. Kein pip, keine Umgebung, kein Container.

Es merkt sich Änderungszeiten in einer kleinen Datei und liest nur neu ein, was sich wirklich geändert hat. Gelöschte Notizen fliegen aus dem Index. Es hat einen Trockenlauf und bricht sauber ab, wenn Einbettungsmodell und Sammlung nicht zusammenpassen.

Und: der ganze Ablauf steht in einer Datei, die du liest und änderst. Kein Klicken durch eine Oberfläche, um herauszufinden, was passiert.

Indexer als Cron-Aufgabe yaml
# 1. Ablegen unter /mnt/tank/studio/desk/indexer/index_songbook.py

# 2. Erst trocken laufen lassen - schreibt nichts
export QDRANT_API_KEY=$(sudo grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2)
python3 /mnt/tank/studio/desk/indexer/index_songbook.py \
  --vault /mnt/tank/songbook/vault --dry-run

# 3. Scharf
python3 /mnt/tank/studio/desk/indexer/index_songbook.py \
  --vault  /mnt/tank/songbook/vault \
  --qdrant http://localhost:6333 \
  --ollama http://localhost:11434 \
  --model  qwen3-embedding:0.6b --dim 1024

# 4. TrueNAS: System Settings > Advanced > Cron Jobs
#    Schedule: */30 * * * *   |  Run as: apps
#    Command:
#      QDRANT_API_KEY=$(grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2) \
#      python3 /mnt/tank/studio/desk/indexer/index_songbook.py \
#        --vault /mnt/tank/songbook/vault --model qwen3-embedding:0.6b --dim 1024 \
#        >> /mnt/tank/studio/logs/indexer.log 2>&1
Variante A: n8n als IndexerKOMFORTSongbookMarkdownn8nCron, Retry,Fehlerprotokoll,sichtbarer AblaufOllama /embedembeddinggemmaQdrantupsertWeboberflaechesehen, wasschiefgingWenn etwas nicht indexiert wird, siehst du im Browser welcher Schritt gescheitert istund kannst ihn einzeln erneut anstossen. Wiederholungen, Zeitplan und Protokollsind eingebaut statt selbstgebaut.Preis: ein weiterer Container, ~350 MB RAM.
So sieht dein Studio aus, wenn du dich hierfür entscheidest.

n8n ist ein Ablauf-Baukasten mit fertigen Bausteinen für Ollama und Qdrant. Du siehst im Browser, welcher Schritt gescheitert ist, und kannst ihn einzeln erneut anstoßen.

Zeitplan, Wiederholung bei Fehlern und Protokoll sind eingebaut statt selbstgebaut. Wenn deine Pipeline irgendwann mehr macht als indexieren – Mails einsortieren, Feeds ziehen, Dateien umbenennen – ist n8n der richtige Ort dafür.

Der Preis: ein weiterer Container, rund 350 MB im Leerlauf. Bei 16 GB verlötetem Speicher ist das eine bewusste Entscheidung, keine Nebensache.

n8n – compose-Ausschnitt yaml
  n8n:
    image: docker.n8n.io/n8nio/n8n:2.33.0
    container_name: studio-indexer
    restart: unless-stopped
    ports:
      - "5678:5678"
    volumes:
      - type: bind
        source: /mnt/tank/studio/desk/indexer
        target: /home/node/.n8n
      - type: bind
        source: /mnt/tank/songbook/vault
        target: /vault
        read_only: true          # der Vault wird gelesen, nie beschrieben
    environment:
      GENERIC_TIMEZONE: "Europe/Berlin"
      TZ: "Europe/Berlin"
      N8N_RUNNERS_ENABLED: "true"
      N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS: "true"
      N8N_SECURE_COOKIE: "false"     # nur im LAN ohne HTTPS
    deploy:
      resources:
        limits: { memory: 900M }

# Der Ablauf danach:
#   Schedule Trigger (alle 30 Min)
#     -> Read/Write Files from Disk  (/vault/**/*.md)
#     -> Code  (Frontmatter lesen, an Ueberschriften teilen)
#     -> Embeddings Ollama  (qwen3-embedding:0.6b)
#     -> Qdrant Vector Store  (Sammlung: songbook, upsert)

Agenten – und was das hier wirklich heißt

Jetzt der Teil, bei dem andere Anleitungen anfangen zu schwärmen. Ich mache es anders, weil du sonst zwei Wochenenden verlierst.

Was hier funktioniert: ein Aufruf, ein Modell, guter Kontext aus der Library, erzwungenes Ausgabeschema. Wenn du wirklich mehrere Schritte brauchst, dann als feste Kette – „lies, fasse zusammen, verschlagworte, schreib in die Inbox“ – mit klaren Übergaben. Nicht als Runde, in der Agenten sich gegenseitig befragen.

Ein Agenten-Werkzeug, das trägt python
"""Open WebUI: Workspace > Tools > neues Tool.
Sucht in der Library und liefert dem Modell fertigen Kontext.
Ein Aufruf, keine Kaskade."""
import json, os, urllib.request

class Tools:
    def __init__(self):
        self.qdrant = "http://qdrant:6333"
        self.ollama = "http://ollama:11434"
        self.key    = os.environ.get("QDRANT_API_KEY", "")
        if not self.key:
            # Ohne Schluessel antwortet Qdrant mit 401 - lieber hier sauber melden
            # als spaeter mit einer nackten HTTPError im Chat landen.
            print("WARNUNG: QDRANT_API_KEY ist im Container nicht gesetzt.")
        self.model  = "qwen3-embedding:0.6b"

    def _post(self, url, body, hdr=None):
        h = {"Content-Type": "application/json"}
        h.update(hdr or {})
        req = urllib.request.Request(url, json.dumps(body).encode(), h, method="POST")
        with urllib.request.urlopen(req, timeout=120) as r:
            return json.load(r)

    def songbook_durchsuchen(self, frage: str, treffer: int = 5, tag: str = "") -> str:
        """Durchsucht das persoenliche Songbook nach Inhalten zu einer Frage.

        :param frage: Wonach gesucht wird, in eigenen Worten.
        :param treffer: Wieviele Fundstellen zurueckkommen sollen.
        :param tag: Optionaler Filter auf ein Schlagwort.
        """
        vec = self._post(f"{self.ollama}/api/embed",
                         {"model": self.model, "input": frage,
                          "truncate": False, "keep_alive": "24h"})["embeddings"][0]
        body = {"query": vec, "limit": treffer,
                "with_payload": ["path", "heading", "text"]}
        if tag:
            body["filter"] = {"must": [{"key": "tags", "match": {"value": tag}}]}
        pts = self._post(f"{self.qdrant}/collections/songbook/points/query",
                         body, {"api-key": self.key})["result"]["points"]
        if not pts:
            return "Im Songbook steht dazu nichts."
        out = []
        for p in pts:
            pl = p.get("payload") or {}
            kopf = pl.get("path", "(ohne Pfad)")
            if pl.get("heading"):
                kopf += " > " + pl["heading"]
            out.append(f"### {kopf}  (Naehe {p['score']:.2f})\n{pl.get('text','')[:900]}")
        return "\n\n".join(out)

Dieses Werkzeug im Arbeitsplatz „Hausmeister“ aktivieren – und das Modell holt sich selbst den Kontext aus deinem Wissen, bevor es antwortet. Das ist der ganze Zauber. Kein Framework nötig.

TAKE 10

Backstage

Von unterwegs auf dein Studio – ohne einen einzigen Port im Router zu öffnen.

Tailscale hast du schon. Deshalb hier nur die Dinge, die im Zusammenspiel mit diesem Aufbau zählen – plus zwei Einstellungen, die man leicht falsch macht.

EinstellungWertWarum
Host Networkanalle App-Ports sind sofort über das Tailnet erreichbar
Userspace Modeausim Userspace fehlt der Kernel-Tunnel – manches funktioniert dann nicht
State StorageHost Pathsonst verliert der Knoten seine Identität
Accept DNSandamit MagicDNS-Namen funktionieren
Advertise Routesdein LANnur wenn du das ganze Heimnetz erreichen willst

Mit Host Network und MagicDNS erreichst du danach alles über sprechende Namen:

Die Adressen deines Studios bash
http://truenas.<dein-tailnet>.ts.net:31028   # Mischpult
http://truenas.<dein-tailnet>.ts.net:3000    # Bandtagebuch
http://truenas.<dein-tailnet>.ts.net:8384    # Syncthing
https://truenas.<dein-tailnet>.ts.net        # TrueNAS selbst

# Von jedem Geraet im Tailnet. Kein Port im Router. Kein dynamisches DNS.
# Der Verkehr ist durch WireGuard verschluesselt - auch ohne HTTPS.

Sauber: echtes HTTPS ohne Reverse Proxy

Tailscale kann selbst als HTTPS-Endpunkt auftreten, mit gültigem Zertifikat und ohne Portnummer. Die TrueNAS-App bietet dafür keinen Schalter, aber es geht über eine Konfigurationsdatei.

Tailscale Serve dauerhaft einrichten bash
# 1. Im Tailscale-Admin: HTTPS-Zertifikate und MagicDNS aktivieren
# 2. Dataset fuer die Konfiguration
sudo mkdir -p /mnt/tank/studio/backstage/config
sudo tee /mnt/tank/studio/backstage/config/serve.json >/dev/null <<'EOF'
{
  "TCP": { "443": { "HTTPS": true } },
  "Web": {
    "truenas.DEIN-TAILNET.ts.net:443": {
      "Handlers": { "/": { "Proxy": "http://127.0.0.1:31028" } }
    }
  }
}
EOF
sudo chown -R 568:568 /mnt/tank/studio/backstage/config

# 3. In der Tailscale-App:
#    Host Path /mnt/tank/studio/backstage/config -> /config
#    Umgebungsvariable TS_SERVE_CONFIG=/config/serve.json
# 4. App neu starten. Danach:
#    https://truenas.DEIN-TAILNET.ts.net   (ohne Portnummer, gueltiges Zertifikat)

# Zum Testen ohne Dauerhaftigkeit:
sudo docker exec -it $(docker ps --format '{{.Names}}' | grep -i tailscale) \
  tailscale serve --bg --https=443 http://127.0.0.1:31028

Zugriffsregeln – wer darf was

Tailscale-Regelwerk json
{
  "tagOwners": { "tag:studio": ["autogroup:admin"] },
  "acls": [
    {
      "action": "accept",
      "src":    ["autogroup:member"],
      "dst":    ["tag:studio:443", "tag:studio:31028", "tag:studio:3000"]
    },
    {
      "action": "accept",
      "src":    ["autogroup:admin"],
      "dst":    ["tag:studio:*"]
    }
  ]
}

Den NAS-Knoten im Admin-Panel mit tag:studio versehen. Danach kommen normale Mitglieder nur noch an Mischpult, Tagebuch und Weboberfläche – nicht an SSH, nicht an die Verwaltung. Wenn du irgendwann jemandem Zugang zu einem Dienst geben willst, ist das der Ort dafür.

TAKE 11

Soundcheck 2

Alles steht. Jetzt geht es darum, dass es auch morgen noch steht.

Ein Studio, das nur am Aufbautag funktioniert, ist kein Studio. Dieser letzte Take gibt dir zwei Dinge: einen Befehl, der dir sonntags in dreißig Sekunden sagt, ob alles in Ordnung ist – und eine Tabelle für den Moment, in dem es das nicht ist.

Der wöchentliche Blick

Als Cron-Aufgabe einrichten unter System Settings → Advanced → Cron Jobs, sonntags. Die Ausgabe kannst du dir per E-Mail schicken lassen oder ins Protokoll schreiben.

studio-check.sh bash
#!/usr/bin/env bash
set -uo pipefail          # nicht -e: ein toter Dienst soll den Bericht nicht abbrechen
echo "===== STUDIO-CHECK  $(date '+%F %T') ====="

echo; echo "--- Container ---"
docker ps --filter name=studio- --format 'table {{.Names}}	{{.Status}}	{{.Image}}'

echo; echo "--- Speicher ---"
docker stats --no-stream --format 'table {{.Name}}	{{.MemUsage}}	{{.CPUPerc}}' \
  | grep -E 'NAME|studio-'
free -h | head -2

echo; echo "--- Wer steht auf der Buehne ---"
docker exec studio-rack ollama ps 2>/dev/null || echo "  Rack antwortet nicht"

echo; echo "--- Library ---"
K=$(grep QDRANT_API_KEY /mnt/tank/studio/stagebox/.env | cut -d= -f2)
curl -s -H "api-key: $K" http://localhost:6333/collections/songbook \
  | jq '{punkte:.result.points_count, indiziert:.result.indexed_vectors_count, status:.result.status}' \
  2>/dev/null || echo "  Library antwortet nicht"

echo; echo "--- Pool ---"
zpool list -o name,size,alloc,free,cap,health
zpool status -x

echo; echo "--- Bandtagebuch ---"
git -c safe.directory=/mnt/tank/songbook/vault -C /mnt/tank/songbook/vault \
    log --oneline -5 2>/dev/null || echo "  kein Repo"

echo; echo "--- Netz (die i226-Falle) ---"
for i in $(ls /sys/class/net | grep -v lo); do
  echo "  $i: $(ethtool $i 2>/dev/null | grep Speed | tr -d '\t')"
done

echo; echo "--- Was zuletzt schiefging ---"
tail -5 /mnt/tank/studio/logs/indexer.log 2>/dev/null
tail -3 /mnt/tank/studio/logs/vault-git.log 2>/dev/null

Drei Zeilen daraus verdienen deine Aufmerksamkeit: PROCESSOR bei ollama ps muss 100% CPU sagen. health beim Pool muss ONLINE sein. Und die Netzgeschwindigkeit muss bei 2500 stehen – steht dort 100, hat der Netzwerkchip wieder zugeschlagen.

Wenn es klemmt

SymptomErster VerdachtHandgriff
Antworten dauern plötzlich ewigModell wurde entladen und lädt neuollama ps – steht dort nichts, mit keep_alive:-1 pinnen
Container startet nicht, Log sagt wenigRechte auf dem Bind-Mountdocker exec -it <c> id gegen ls -ln auf dem Host halten
Qdrant bricht beim Start abAblage auf Netzwerk- oder FUSE-Dateisystemdocker logs studio-library | grep -i filesystem
Suche findet nichts, obwohl Notizen da sindDimension passt nicht zum EinbettungsmodellDer Indexer nennt die Zahl. Sammlung neu anlegen und einmal voll einlesen.
Filter nach Tags findet nichts, Volltext schonByte-Order-Mark vor dem Frontmatterhead -c3 notiz.md | xxd – steht da efbbbf, Datei ohne Marke neu speichern
curl localhost:11434 sagt connection refusedPort nicht veröffentlichtIm Compose muss ports: 127.0.0.1:11434:11434 stehen, nicht expose:
NAS ist zäh, Übertragung kriechtNetzwerkchip auf 100 Mbit gefallenethtool <if> | grep Speed – siehe Take 01
Speicher voll, der Kernel schießt Prozesse abLesecache und Modelle konkurrierenzfs_arc_max auf 4 GB, OLLAMA_MAX_LOADED_MODELS auf 2
Antwort sieht richtig aus, ist aber UnsinnGrafikeinheit rechnet mitollama ps, Spalte PROCESSOR. Zurück auf CPU.
git sagt not in a git directoryRepo gehört einer anderen Kennunggit config --global --add safe.directory <pfad>
Forgejo lässt dich nicht anmeldenKein Zugang angelegtDer Befehl in Take 08 legt ihn an – der Assistent ist absichtlich übersprungen.

Die drei Regeln für den Dauerbetrieb

TAKE 12

Konfiguration

Der komplette Sollzustand in einer Datei. Maschinenlesbar, fortschreibbar, und mit Begründungen versehen.

Diese Datei ist die einzige Wahrheit über dein Studio. Skripte lesen sie, Agenten lesen sie, und in einem halben Jahr liest du sie und weißt wieder, warum etwas so eingestellt ist. Sie gehört nach /mnt/tank/studio/studio.config.json und ins Bandtagebuch.

Wie du sie fortschreibst

  1. Feld ändern – etwa ein Modell tauschen oder eine Snapshot-Haltedauer anpassen.
  2. Prüfen drücken. Rot heißt: Syntaxfehler, mit Zeilenangabe.
  3. Herunterladen und nach /mnt/tank/studio/studio.config.json legen.
  4. Committen: git -C /mnt/tank/studio add studio.config.json && git commit -m "config: ..."

Sie maschinell auslesen

Die Konfiguration auswerten bash
# Alle Datasets mit ihren Eigenschaften anlegen - direkt aus der Konfiguration
jq -r '.pfade.datasets[] | "zfs create -o recordsize=\(.recordsize) -o compression=\(.compression) -o atime=\(.atime) tank/\(.pfad)"' \
  /mnt/tank/studio/studio.config.json

# Alle Modelle holen
jq -r '.musiker[].tag' /mnt/tank/studio/studio.config.json \
  | xargs -I{} docker exec studio-rack ollama pull {}

# Die Umgebungsvariablen fuers Rack als .env ausgeben
jq -r '.rack.environment | to_entries[] | "\(.key)=\(.value)"' \
  /mnt/tank/studio/studio.config.json

# Was habe ich damals entschieden - und warum?
jq -r '.entscheidungen | to_entries[] | "\(.key): \(.value.gewaehlt)\n   \(.value.begruendung)\n"' \
  /mnt/tank/studio/studio.config.json

# Passt der Speicher noch? Summe aller Modellgroessen
jq '[.musiker[].groesse_gb] | add' /mnt/tank/studio/studio.config.json

Was darin steht

AbschnittInhalt
hardwareGeräteprofile, der Erkennungsbefehl, und die harten Grenzen inklusive Begründung
betriebssystemEmpfohlene TrueNAS-Version, welche zu meiden ist, der ARC-Deckel
entscheidungenDie vier Weggabelungen mit Wahl und Begründung – wird beim Abhaken aktualisiert
pfadeAlle sechzehn Datasets mit recordsize, Kompression und Zweck, plus die Schreibordnung
rackOllama: Image, jede Umgebungsvariable – und was sie bewirkt
musikerDie fünf Modelle mit Rolle, Größe, keep_alive und Auswahlgrund
libraryQdrant: Sammlung, Dimension, Quantisierung, Dateisystem-Anforderung
sicherungSnapshot-Zeitpläne, USB-Replikation, und was noch fehlt
grenzenWas diese Maschine kann und was nicht – in Zahlen, nicht in Hoffnung