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 vorstellst | Was tatsächlich läuft | Ergebnis |
|---|---|---|
| Ein großes Modell für alles | Fünf kleine mit klaren Rollen | schneller, nachvollziehbar, wartbar |
| GPU-Server für 2000 € | Der Beelink, der schon da steht | 0 € Mehrkosten, ~15 W |
| Warten bis Hardware reicht | Heute Abend anfangen | in 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.
| Technik | Im Studio | Was es tut |
|---|---|---|
| Der Beelink | Der Amp | die Maschine, auf der alles steht |
| ZFS-Datasets | Die Bühne | wer wo steht, und wer schreiben darf |
| Ollama | Das Instrumenten-Rack | hält die Modelle spielbereit |
| Modelle & Modelfiles | Die Session-Musiker | je einer für eine Aufgabe |
| Qdrant | Die Sample-Library | findet in Sekunden, was du mal notiert hast |
| Obsidian-Vault | Das Songbook | dein Wissen, als schlichtes Markdown |
| Git & Forgejo | Das Bandtagebuch | jeder Take archiviert, nichts geht verloren |
| Open WebUI | Das Mischpult | wo du die Regler in der Hand hast |
| Tailscale | Der Backstage-Pass | von überall rein, ohne offene Tür |
Was dich das kostet
- Geld: nichts. Alles Open Source, alles läuft auf der vorhandenen Kiste.
- Strom: 6–19 W im Leerlauf, 20–40 W wenn ein Modell arbeitet. Weniger als eine Glühbirne.
- Zeit: zwei bis drei Abende. Zwölf Takes, jeder für sich abgeschlossen.
- RAM: das ist der einzige echte Engpass. Dazu gleich mehr – ehrlich und unbeschönigt.
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.
# 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).txtWas du in der Ausgabe suchst
| Zeile | Was sie dir sagt | Warum es zählt |
|---|---|---|
Model name: Intel N150 oder N95 | welche CPU-Klasse | bestimmt, welche Modellgrößen Sinn ergeben |
free -h → 12 oder 16 GB | dein hartes Budget | verlötet. Es gibt kein Upgrade. |
Realtek RTL8126 in lspci | 5-GbE-Port vorhanden | → es ist ein ME Pro |
zweimal Intel I226, sonst nichts | zwei 2,5-GbE-Ports | → es ist ein ME mini |
ASMedia ASM2824 | PCIe-Switch für 6 Steckplätze | → ME mini, und du bist vom NVMe-Fehler betroffen |
/dev/dri/renderD128 vorhanden | iGPU ist ansprechbar | relevant für Take 04 und für Videotranscoding |
uname -r ≥ 6.12 | Kernel neu genug | unter 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.
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:
# 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 offZum Nachschlagen
Bühne
Die Ordnerstruktur. Klingt langweilig, ist aber die Entscheidung, die du am häufigsten bereust, wenn du sie falsch triffst.
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.
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.
# 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/studioDer 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:
| Was | Wie es sich verhält | Was es braucht |
|---|---|---|
| GGUF-Modelle | wenige Dateien, mehrere GB, sequenziell gelesen | recordsize=1M, Kompression aus |
| Qdrant-Segmente | wahlfreier Zugriff mitten in großen Dateien | recordsize=16K, wie eine Datenbank |
| Markdown-Notizen | tausende winzige Textdateien | recordsize=128K, zstd-3 |
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.
# 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/tapesEin 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.
# 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'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.
# 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/inboxDie 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.
| Ort | Wer schreibt | Regel |
|---|---|---|
songbook/inbox/ | Agenten, Automatik | Alles Maschinengemachte landet hier. Du sichtest und verschiebst. |
studio/stagebox/ | nur du | Schlüssel und Passwörter. Modus 700. Kein Container greift hier hinein. |
songbook/vault/ | nur Syncthing | Indexer und Agenten lesen. Nie schreiben. |
tapes/ | nur git push | Historie 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.
# 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 loeschenRack
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 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.
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.
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 }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.
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/.envDie 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.
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: bridgedocker 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.
Zum Nachschlagen
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.
| Rolle | Modell | Größe | Kann | Immer warm? |
|---|---|---|---|---|
| Hausmeister | granite4.1:3b-q4_K_M | 2,1 GB | Chat, Werkzeuge, JSON, Deutsch | ja |
| Zweitmeinung | qwen3.5:2b-q4_K_M | 1,9 GB | Chat, Bilder, Nachdenken, 256K | 30 Min |
| Notenschreiber | qwen3-embedding:0.6b | 0,64 GB | Einbettungen für die Suche | ja |
| Setzer | qwen2.5-coder:3b | 1,9 GB | Code, Vervollständigung | 30 Min |
| Notenleser | glm-ocr:q8_0 | 1,6 GB | Papier lesen, Tabellen | nein |
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.
# 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 listPresets – 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.
# /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}# /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'."""# 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
# 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 ForeverDie 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.
Zum Nachschlagen
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.
# 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/collectionsK=$(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##*:}\"}"
doneDie drei Einstellungen, die den Unterschied machen
| Einstellung | Was sie tut | Was sie bringt |
|---|---|---|
on_disk: true | Vektoren als Datei-Abbild statt im Speicher | Speicherbedarf sinkt drastisch |
scalar int8 | Vektoren auf ein Viertel eingedampft | Faktor 4 weniger, ~1 % Fehler |
on_disk_payload | Nutztext auf der Platte | der 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
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.
#!/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 -deleteZum Nachschlagen
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 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?
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:
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
# .stversionsDer Vault als Struktur
Ordnerstruktur ist Geschmackssache – aber zwei Dinge zahlen sich handfest aus, weil der Indexer sie nutzt.
---
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]].- Frontmatter wird nicht eingebettet, sondern als Filterfeld abgelegt. Damit kannst du später „nur Notizen mit Tag ki aus 2026“ suchen – das ist um Größenordnungen schneller als jede Volltextsuche.
- Überschriften zerlegen die Notiz in Textstücke. Der Indexer stellt jedem Stück seinen Überschriften-Pfad voran –
homeserver.md > Netzwerk. Das ist der wirkungsvollste einzelne Hebel gegen Treffer, die aus dem Zusammenhang gerissen sind. - Wikilinks bleiben im Text stehen und werden zusätzlich als Liste abgelegt. Sie tragen Bedeutung.
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.
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?
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"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
EOFDie 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
#!/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
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 -1git 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:
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 }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 mainMischpult
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.
# 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 300Modell-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.
| Arbeitsplatz | Basis | Angebunden an | Wofür |
|---|---|---|---|
| Hausmeister | granite4.1:3b | Songbook-Wissen | „was habe ich zu X notiert?“ |
| Kurator | granite4.1:3b | – | langen Text zu fünf Punkten eindampfen |
| Setzer | qwen2.5-coder:3b | – | Skripte, Compose-Dateien |
| Notenleser | glm-ocr:q8_0 | – | Rechnung abfotografiert, Text raus |
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.
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.
# 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>&1Agenten – 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.
"""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.
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.
| Einstellung | Wert | Warum |
|---|---|---|
| Host Network | an | alle App-Ports sind sofort über das Tailnet erreichbar |
| Userspace Mode | aus | im Userspace fehlt der Kernel-Tunnel – manches funktioniert dann nicht |
| State Storage | Host Path | sonst verliert der Knoten seine Identität |
| Accept DNS | an | damit MagicDNS-Namen funktionieren |
| Advertise Routes | dein LAN | nur wenn du das ganze Heimnetz erreichen willst |
Mit Host Network und MagicDNS erreichst du danach alles über sprechende Namen:
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.
# 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:31028Zugriffsregeln – wer darf was
{
"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.
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.
#!/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/nullDrei 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
| Symptom | Erster Verdacht | Handgriff |
|---|---|---|
| Antworten dauern plötzlich ewig | Modell wurde entladen und lädt neu | ollama ps – steht dort nichts, mit keep_alive:-1 pinnen |
| Container startet nicht, Log sagt wenig | Rechte auf dem Bind-Mount | docker exec -it <c> id gegen ls -ln auf dem Host halten |
| Qdrant bricht beim Start ab | Ablage auf Netzwerk- oder FUSE-Dateisystem | docker logs studio-library | grep -i filesystem |
| Suche findet nichts, obwohl Notizen da sind | Dimension passt nicht zum Einbettungsmodell | Der Indexer nennt die Zahl. Sammlung neu anlegen und einmal voll einlesen. |
| Filter nach Tags findet nichts, Volltext schon | Byte-Order-Mark vor dem Frontmatter | head -c3 notiz.md | xxd – steht da efbbbf, Datei ohne Marke neu speichern |
curl localhost:11434 sagt connection refused | Port nicht veröffentlicht | Im Compose muss ports: 127.0.0.1:11434:11434 stehen, nicht expose: |
| NAS ist zäh, Übertragung kriecht | Netzwerkchip auf 100 Mbit gefallen | ethtool <if> | grep Speed – siehe Take 01 |
| Speicher voll, der Kernel schießt Prozesse ab | Lesecache und Modelle konkurrieren | zfs_arc_max auf 4 GB, OLLAMA_MAX_LOADED_MODELS auf 2 |
| Antwort sieht richtig aus, ist aber Unsinn | Grafikeinheit rechnet mit | ollama ps, Spalte PROCESSOR. Zurück auf CPU. |
git sagt not in a git directory | Repo gehört einer anderen Kennung | git config --global --add safe.directory <pfad> |
| Forgejo lässt dich nicht anmelden | Kein Zugang angelegt | Der Befehl in Take 08 legt ihn an – der Assistent ist absichtlich übersprungen. |
Die drei Regeln für den Dauerbetrieb
- Keine automatischen Updates auf
:latest. Ollama veröffentlicht fast täglich, und genau die Verhaltensänderungen, die dieser Guide dokumentiert, sind innerhalb weniger Versionen entstanden. Version festnageln, Änderungsnotizen lesen, danach kurz testen. - Vor jedem Update ein Snapshot.
sudo zfs snapshot -r tank/studio@vor-update-$(date +%Y%m%d)– ein Handgriff, und du kannst jede Änderung zurückdrehen. - Ein Modell nach dem anderen ausprobieren. Wenn du drei neue gleichzeitig holst und etwas wird langsam, weißt du nicht, welches es war.
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
- Feld ändern – etwa ein Modell tauschen oder eine Snapshot-Haltedauer anpassen.
- Prüfen drücken. Rot heißt: Syntaxfehler, mit Zeilenangabe.
- Herunterladen und nach
/mnt/tank/studio/studio.config.jsonlegen. - Committen:
git -C /mnt/tank/studio add studio.config.json && git commit -m "config: ..."
Sie maschinell auslesen
# 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.jsonWas darin steht
| Abschnitt | Inhalt |
|---|---|
hardware | Geräteprofile, der Erkennungsbefehl, und die harten Grenzen inklusive Begründung |
betriebssystem | Empfohlene TrueNAS-Version, welche zu meiden ist, der ARC-Deckel |
entscheidungen | Die vier Weggabelungen mit Wahl und Begründung – wird beim Abhaken aktualisiert |
pfade | Alle sechzehn Datasets mit recordsize, Kompression und Zweck, plus die Schreibordnung |
rack | Ollama: Image, jede Umgebungsvariable – und was sie bewirkt |
musiker | Die fünf Modelle mit Rolle, Größe, keep_alive und Auswahlgrund |
library | Qdrant: Sammlung, Dimension, Quantisierung, Dateisystem-Anforderung |
sicherung | Snapshot-Zeitpläne, USB-Replikation, und was noch fehlt |
grenzen | Was diese Maschine kann und was nicht – in Zahlen, nicht in Hoffnung |