Self-Hosting mit Docker

Dein eigenes Personal Brain in einem Container — auf deinem Laptop, einem kleinen NAS, oder einem Hetzner VPS. Dasselbe Image, dieselben Befehle.

Status: Stabil. Image wird bei jedem Push zu main automatisch gebaut und unter ghcr.io/gsalami/gts-personal:latest veröffentlicht (amd64 + arm64).


Warum Self-Hosting

  • Datensouveränität — deine Brains liegen als Markdown-Files auf deinem Rechner, nicht auf einem fremden Server.
  • Offline-fähig — der lokale Brain läuft weiter, wenn das Internet down ist. Sync zum Business passiert sobald du wieder online bist.
  • Outbound-only — die Sync-Richtung geht immer vom Personal aus zum Business. Du musst keinen Port nach aussen öffnen, keine DynDNS, keine Portweiterleitung. Firewall-/NAT-freundlich.
  • Kosten: 0 €. Gratis-Image, läuft auf deiner vorhandenen Hardware.

Variante A · lokal auf Mac / Windows / Linux

Voraussetzungen

  • Docker Desktop (docker.com/products/docker-desktop) auf Mac/Win, oder Docker Engine auf Linux.
  • 2 GB freier RAM + 500 MB Disk.
  • Ein eindeutiger Slug (z.B. alex) — falls du dich später mit einem Business Brain verbindest, muss der Slug matchen mit dem was dir dort im Invite steht.

Setup in 5 Schritten

mkdir my-brain && cd my-brain

curl -O https://raw.githubusercontent.com/gsalami/ground-truth-system/main/docker/docker-compose.yml
curl -O https://raw.githubusercontent.com/gsalami/ground-truth-system/main/docker/.env.example

mv .env.example .env
# → .env in deinem Editor öffnen, GTS_INSTANCE_SLUG + GTS_OWNER_EMAIL setzen

docker compose up -d

Beim ersten Start zieht Docker das Image (~250 MB, einmalig), scaffold't /data mit deinem Slug + Owner, erstellt ein frisches Git-Repo darin, startet den Server. Logs live mitlesen:

docker compose logs -f

Dann im Browser: http://localhost:3100/bootstrap → Admin-Passwort vergeben (min. 12 Zeichen) → du bist drin.

Konfiguration (.env)

Variable Pflicht Beispiel Erklärung
GTS_INSTANCE_SLUG ja alex Dein eindeutiger Slug. Kleinbuchstaben, Bindestriche. Muss stabil bleiben — steht in jeder Brain-ID drin.
GTS_OWNER_EMAIL ja alex@kuble.com Wird Admin-Account + Git-Author deiner Commits.
GTS_PUBLIC_URL nein http://localhost:3100 URL unter der du's aufrufst. Nur relevant wenn du's hinter Reverse-Proxy steckst.

Nur beim ersten docker compose up werden SLUG + OWNER gelesen — danach liegt die Config im gts-data Volume und ändert sich nicht mehr, selbst wenn du die .env anpasst.

Mit einem Business Brain verbinden

  1. Auf dem Business Brain (z.B. ground.kuble.com) einen Invite generieren: /admin/invites → neuer Invite für deine Email
  2. Invite-Code kopieren
  3. Bei dir lokal: http://localhost:3100/admin/connections/new
  4. Einfügen: Invite-Code + Business-URL (z.B. https://ground.kuble.com) → Verbinden
  5. Unter /syncAlle pullen drückt den initialen Pull
  6. Brains mit sync.direction: push in deinem Personal werden bei Alle pushen ans Business gespiegelt

Alltag

docker compose logs -f          # live mitlesen
docker compose restart          # neustarten
docker compose down             # stoppen, Daten bleiben erhalten
docker compose pull && \
  docker compose up -d          # Update auf neueste Version

# Backup (tar.gz im aktuellen Ordner)
docker run --rm -v gts-data:/src -v "$(pwd)":/dst alpine \
  tar czf /dst/brain-backup-$(date +%F).tar.gz -C /src .

Metadata-Backups (meta.sqlite)

Die SQLite-Metadatenbank (meta.sqlite) enthält Sessions, Connections, API-Keys, OAuth-State, Health-Checks und Chat-History. Brain-Inhalte selbst liegen in git — die sind durch den Git-Repo schon versioniert.

Auf VPS-Setups (Variante B unten, systemd) installierst du einen stündlichen Backup-Timer per Repo-Script:

cd /opt/gts-<slug>
bash scripts/install-backup.sh

Was das macht:

  • Kopiert backup-db.sh nach /opt/gts-scripts/
  • Installiert gts-backup@<slug>.timer (stündlich, mit 0-5 min Jitter) + .service Template
  • Legt den ersten Snapshot sofort an unter /var/backups/gts-<slug>/
  • scripts/update.sh macht zusätzlich einen pre-update-Snapshot vor jedem Service-Restart

Rotation: letzte 7 Tage alles, dann wöchentlich (Mo 02:00Z) für 4 Wochen, monatlich (1. 02:00Z) für 3 Monate. Tagged Backups (pre-update, pre-deploy-*) werden nie automatisch gelöscht.

On-demand:

/opt/gts-scripts/backup-db.sh <slug> [optional-tag]

Jeder Snapshot läuft durch PRAGMA integrity_check vor UND nach der Extraktion — korrupte Backups landen gar nicht erst auf Disk.

Restore (Disaster-Recovery):

systemctl stop gts-<slug>
cd /tmp && tar xzf /var/backups/gts-<slug>/<timestamp>.tar.gz
cp /tmp/meta.sqlite /var/lib/gts-<slug>/.gts/meta.sqlite   # Pfad variiert
systemctl start gts-<slug>

Variante B · Hetzner VPS mit HTTPS

Gleiches Image, aber mit Traefik als Reverse-Proxy davor, der automatisch Let's-Encrypt-Zertifikate zieht.

Voraussetzungen

  • Hetzner (oder ähnlicher) VPS — CX22 reicht (2 vCPU, 4 GB RAM, ~5 €/Monat)
  • Ubuntu 22.04+ mit Docker + Compose-Plugin
  • Traefik läuft bereits als Reverse-Proxy im Docker-Netzwerk proxy (Standard-Setup auf Kuble-Servern)
  • DNS A-Record: brain.deinedomain.tld → VPS-IP

Setup

# auf dem VPS, als root
mkdir -p /opt/gts-brain && cd /opt/gts-brain

curl -O https://raw.githubusercontent.com/gsalami/ground-truth-system/main/docker/docker-compose.yml
curl -O https://raw.githubusercontent.com/gsalami/ground-truth-system/main/docker/docker-compose.vps.yml
curl -O https://raw.githubusercontent.com/gsalami/ground-truth-system/main/docker/.env.example

mv .env.example .env
# → .env bearbeiten: GTS_INSTANCE_SLUG, GTS_OWNER_EMAIL, GTS_HOST=brain.deinedomain.tld

docker compose -f docker-compose.yml -f docker-compose.vps.yml up -d

Traefik erkennt den Container über Labels, holt sich ein Let's-Encrypt-Cert (~30 Sek), danach ist https://brain.deinedomain.tld/bootstrap erreichbar.

Update-Flow ist identisch — docker compose pull reicht.


Aus dem Source bauen (für Contributors)

Wenn du am Code arbeitest und dein Image lokal bauen willst statt das fertige zu ziehen:

git clone https://github.com/gsalami/ground-truth-system.git
cd ground-truth-system/docker
cp .env.example .env && $EDITOR .env
docker compose up -d --build

Der Build dauert ~3 Min (Next.js kompilieren + better-sqlite3 nativ bauen). --build überstimmt den image:-Tag in der Compose-Datei.


Troubleshooting

GTS_INSTANCE_SLUG and GTS_OWNER_EMAIL are not set.env nicht angelegt oder die Werte fehlen. docker compose down, fixen, neu starten.

Port 3100 belegt → In docker-compose.yml "127.0.0.1:3100:3100""127.0.0.1:3200:3100" ändern, dann läuft's auf http://localhost:3200.

Pull/Push failed — unauthorized → Invite ist abgelaufen (24h TTL) oder API-Key wurde rotiert. Neuen Invite generieren am Business, unter /admin/connections/<id> rotieren.

"Duplicate column" / Migration-Fehler → Sollte nie passieren. Wenn doch: Logs posten, docker compose downdocker volume rm gts-data (⚠️ löscht alles lokal) → neu starten.

Ich will in die SQLite reinschauen

docker exec -it gts-brain sh
# im Container:
apk add --no-cache sqlite
sqlite3 /data/.gts/meta.sqlite

Datensicherheit

  • GTS_AUTH_SECRET (signiert Sessions) wird beim ersten Start generiert und in /data/.gts/auth.secret (chmod 600) persistiert. Geht beim docker volume rm gts-data verloren — Nutzer müssen sich dann neu einloggen.
  • Integrationen (Recall.ai etc.) speichern API-Keys in meta.sqlite (unverschlüsselt). Disk-Encryption (FileVault / LUKS) sinnvoll.
  • Brain-Inhalte sind Markdown — keine Secrets rein. API-Keys gehören in .env-Files oder Passwortmanager.

Deinstallieren

docker compose down -v          # stoppt + löscht Daten-Volume
docker image rm ghcr.io/gsalami/gts-personal:latest