Self-Hosting-Architektur
Wie Owlat als vollständig selbst gehosteter Stack über Docker Compose läuft — quelloffenes Convex-Backend, eigener MTA und ein austauschbarer LLM-Anbieter.
Self-Hosting-Architektur
Owlat ist darauf ausgelegt, vollständig auf Ihrer eigenen Infrastruktur zu laufen. Jede Komponente ist quelloffen, und der gesamte Stack wird über eine einzige Docker-Compose-Datei deployt.
Datensouveränität, Compliance-Anforderungen, abgeschottete Umgebungen oder schlicht der Wunsch nach voller Kontrolle. Owlat benötigt für seinen Betrieb keinerlei Cloud-Dienste — nicht einmal für KI-Funktionen, sofern Sie ein lokales Modell betreiben.
Der Stack
docker compose upDer selbst gehostete Stack ist in der docker-compose.yml im Wurzelverzeichnis des Repositorys definiert:
| Dienst | Rolle | Image |
|---|---|---|
| Convex-Backend | Datenbank, Echtzeit-Subscriptions, Dateispeicher, Vektorsuche, serverlose Funktionen | ghcr.io/get-convex/convex-backend |
Convex-Dashboard (--profile dashboard) | Admin-/Debugging-Oberfläche für das Backend (Port 6791, nur an 127.0.0.1 gebunden, keine eingebaute Authentifizierung — erreichen Sie sie über einen SSH-Tunnel) | ghcr.io/get-convex/convex-dashboard |
| Web | Nuxt-Anwendung (Dashboard, E-Mail-Builder, Einstellungen) | ghcr.io/wolvesdotink/web (oder aus apps/web/Dockerfile gebaut) |
| MTA | Eigener Mail Transfer Agent — SMTP-Zustellung, Bounce-Verarbeitung, IP-Warming | ghcr.io/wolvesdotink/mta (oder aus apps/mta/Dockerfile gebaut) |
| Redis | Job-Queue für den MTA, verteilte Koordination, Zustand des Rate Limitings | redis:7.4-alpine |
| ClamAV | Virenprüfung für E-Mail-Anhänge | clamav/clamav:stable |
| Updater | Sidecar für In-App-Updates — steuert docker compose pull && up -d | ghcr.io/wolvesdotink/updater (oder aus apps/updater/Dockerfile gebaut) |
Caddy (--profile tls) | Reverse Proxy mit automatischem HTTPS für die Produktion | caddy:2.8-alpine |
Convex Deploy (--profile deploy) | Einmal-Job, der die Tenant-Funktionen ins Backend pusht | ghcr.io/wolvesdotink/convex-deploy |
Code Worker (--profile inbox-codetasks, auch dev) | Sidecar für den KI-Coding-Agenten der Strecke Feature-Wunsch → Prototyp | Aus apps/code-worker/Dockerfile gebaut |
Ollama (--profile ai, auch dev) | Optionale lokale LLM-Inferenz (keine veröffentlichten Ports; nur internes Netzwerk) | ollama/ollama |
Ein lokales LLM kommt als optionaler Dienst mit. Starten Sie den mitgelieferten Ollama-Container mit --profile ai (auch unter --profile dev aktiviert) und setzen Sie LLM_PROVIDER=ollama (das Backend löst ihn unter http://ollama:11434/v1 auf), oder richten Sie dieselbe Variable auf eine von Ihnen bereitgestellte Ollama-Instanz — siehe LLM-Konfiguration weiter unten.
Was Convex von Haus aus mitbringt
Das quelloffene Convex-Backend (ADR-006) bündelt Fähigkeiten, die sonst eigene Dienste erfordern würden:
- Datenbank — dokumentorientiert mit Indizes, Volltextsuche und ACID-Transaktionen
- Vektorsuche — native Vektorindizes für Embedding-basierten Abruf (das Substrat, auf dem der Wissensgraph und die semantische Dateisuche aufbauen, während diese Funktionen reifen)
- Dateispeicher — Up- und Downloads von Binärdateien über
ctx.storage(Medienbibliothek, Anhänge, semantische Dateien) - Echtzeit-Subscriptions — reaktive Queries, die Aktualisierungen über WebSocket an die Oberfläche pushen
- Geplante Funktionen — Cronjobs und einmalig geplante Aufgaben (Kampagnenversand, Analytics-Reporting, Aufräumen der Versandreputation, Wissensverfall)
- HTTP-Actions — Webhook-Endpunkte, API-Routen, Tracking-Pixel
Das bedeutet: kein PostgreSQL, kein MinIO, keine separate Vektordatenbank. Das Convex-Backend ist der einzige zustandsbehaftete Dienst.
Docker Compose
Die maßgebliche Definition liegt in der docker-compose.yml im Wurzelverzeichnis des Repositorys. Der folgende Auszug zeigt die Verdrahtung der Kerndienste — für den vollständigen Satz an Build-Argumenten, Health Checks und Umgebungsvariablen zur Versionsfixierung ziehen Sie bitte die tatsächliche Datei heran.
services:
# Database + serverless functions
convex:
image: ghcr.io/get-convex/convex-backend:${CONVEX_BACKEND_VERSION:-...}
ports:
- "${CONVEX_PORT:-3210}:3210" # Backend API
- "${CONVEX_SITE_PORT:-3211}:3211" # HTTP actions / site proxy
volumes:
- convex-data:/convex/data
environment:
INSTANCE_SECRET: ${INSTANCE_SECRET}
CONVEX_CLOUD_ORIGIN: ${NUXT_PUBLIC_CONVEX_URL:-http://localhost:3210}
CONVEX_SITE_ORIGIN: ${NUXT_PUBLIC_CONVEX_SITE_URL:-http://localhost:3211}
# Admin/debugging UI (separate service, port 6791)
convex-dashboard:
image: ghcr.io/get-convex/convex-dashboard:${CONVEX_DASHBOARD_VERSION:-...}
ports:
- "${DASHBOARD_PORT:-6791}:6791"
environment:
CONVEX_URL: http://convex:3210
# Nuxt web app
web:
image: ghcr.io/wolvesdotink/web:${OWLAT_VERSION:-latest}
ports:
- "${WEB_PORT:-3000}:3000"
environment:
NUXT_PUBLIC_CONVEX_URL: ${NUXT_PUBLIC_CONVEX_URL:-http://localhost:3210}
NUXT_PUBLIC_CONVEX_SITE_URL: ${NUXT_PUBLIC_CONVEX_SITE_URL:-http://localhost:3211}
NUXT_PUBLIC_SITE_URL: ${NUXT_PUBLIC_SITE_URL:-http://localhost:3000}
OWLAT_DEPLOYMENT_MODE: ${OWLAT_DEPLOYMENT_MODE:-selfhost}
INSTANCE_SECRET: ${INSTANCE_SECRET} # for calling the updater sidecar
mta:
image: ghcr.io/wolvesdotink/mta:${OWLAT_VERSION:-latest}
ports:
- "${MTA_SMTP_PORT:-25}:25" # Inbound SMTP (bounce processing)
- "${MTA_HTTP_PORT:-3100}:3100" # HTTP API
environment:
REDIS_URL: redis://redis:6379
CONVEX_SITE_URL: http://convex:3211
MTA_API_KEY: ${MTA_API_KEY}
MTA_WEBHOOK_SECRET: ${MTA_WEBHOOK_SECRET}
redis:
image: redis:${REDIS_VERSION:-7.4-alpine}
command: redis-server --appendonly yes
volumes:
- redis-data:/data
clamav:
image: clamav/clamav:${CLAMAV_VERSION:-stable}
volumes:
- clamav-data:/var/lib/clamav
# In-app update sidecar (reached only via the internal network)
updater:
image: ghcr.io/wolvesdotink/updater:${OWLAT_VERSION:-latest}
# mounts only the install dir; reaches Docker through the least-privilege docker-socket-proxy (DOCKER_HOST), not the raw socket
# Reverse proxy with automatic HTTPS (production)
caddy:
image: caddy:${CADDY_VERSION:-2.8-alpine}
ports: ["80:80", "443:443"]
profiles: [tls]
# One-shot: push tenant functions to the backend after first boot
convex-deploy:
image: ghcr.io/wolvesdotink/convex-deploy:${OWLAT_VERSION:-latest}
environment:
CONVEX_SELF_HOSTED_URL: http://convex:3210
CONVEX_SELF_HOSTED_ADMIN_KEY: ${CONVEX_ADMIN_KEY}
profiles: [deploy]
# AI coding-agent sidecar (feature request → prototype)
code-worker:
build: { context: ., dockerfile: apps/code-worker/Dockerfile }
profiles: [inbox-codetasks, dev]
volumes:
convex-data:
redis-data:
clamav-data:
code-workspace:
caddy-data:
caddy-config:
Der Dienst web liest die browserseitigen NUXT_PUBLIC_*-URLs. CONVEX_SELF_HOSTED_URL / CONVEX_SELF_HOSTED_ADMIN_KEY werden ausschließlich vom Einmal-Dienst convex-deploy zum Pushen der Funktionen genutzt, nicht von der laufenden Web-App.
Umgebungsvariablen
Owlat teilt seine Konfiguration auf in Compose-Variablen (in .env, kopiert aus .env.selfhost.example), die die Container miteinander verdrahten, und Convex-Funktions-Variablen, die über convex env set am Backend gesetzt werden (oder von owlat quickstart für Sie angewendet werden). Die folgenden Tabellen decken beides ab — die Spalte „Geltungsbereich“ sagt Ihnen, wo die jeweilige Variable lebt.
Erforderlich
| Variable | Geltungsbereich | Beschreibung |
|---|---|---|
INSTANCE_SECRET | Compose .env | Instanz-Secret des Convex-Backends — openssl rand -hex 32 |
CONVEX_ADMIN_KEY | Compose .env | Admin-Key, nach dem ersten Start erzeugt über docker compose exec convex ./generate_admin_key.sh |
MTA_API_KEY | Compose .env | Gemeinsames Secret zwischen Convex und MTA für Versandanfragen |
MTA_WEBHOOK_SECRET | Compose .env | HMAC-Secret für die Authentifizierung der Webhooks MTA → Convex |
NUXT_PUBLIC_SITE_URL | Compose .env | Öffentliche, browserseitige URL der Web-App (z. B. https://owlat.example.com) |
NUXT_PUBLIC_CONVEX_URL / NUXT_PUBLIC_CONVEX_SITE_URL | Compose .env | Öffentliche URLs von Convex-Backend und Site-Proxy |
BETTER_AUTH_SECRET | Convex-Funktion | Secret zum Signieren der Sessions in der Auth-Schicht (mit convex env set setzen) |
UNSUBSCRIBE_SECRET | Convex-Funktion | Eigenes Secret zum Signieren von Abmelde-/Präferenz-Links (mit convex env set setzen) |
Die Datei .env.selfhost.example im Wurzelverzeichnis des Repositorys listet jede Compose-Variable auf. Die Convex-Funktions-Secrets werden von owlat quickstart automatisch gesetzt oder von Hand mit convex env set, sobald das Backend läuft.
SITE_URL ist eine Umgebungsvariable der Convex-Funktionen (apps/api/convex/lib/env.ts), die Backend-Funktionen zum Bauen absoluter Links verwenden. Der Web-Container nutzt die browserseitige NUXT_PUBLIC_SITE_URL. Beide sind verschieden — setzen Sie beide. (owlat quickstart setzt SITE_URL für Sie.)
Das gehärtete VPS-Template (infra/templates/.env.vps.template) verlangt zusätzlich REDIS_PASSWORD.
LLM-Konfiguration (für die Agenten-Strecke)
Dies sind Variablen der Convex-Funktionen (mit convex env set setzen). Details finden Sie in ADR-007: Austauschbarer LLM-Anbieter.
| Variable | Beschreibung | Standard |
|---|---|---|
LLM_PROVIDER | Anbietertyp: openai (Standard), openrouter oder ollama (alle OpenAI-kompatibel). anthropic ist kein anerkannter Wert — betreiben Sie Claude über openai mit einer OpenAI-kompatiblen LLM_BASE_URL. | openai |
LLM_BASE_URL | Überschreibt den API-Endpunkt (für Ollama: http://ollama:11434/v1) | Standard des Anbieters |
LLM_API_KEY | API-Key — OPENROUTER_API_KEY / OPENAI_API_KEY werden ebenfalls akzeptiert; der zuerst gesetzte gewinnt | — (für Ollama nicht nötig) |
LLM_MODEL | Standard-Modellbezeichner (oder LLM_MODEL_FAST / LLM_MODEL_CAPABLE je Stufe) | gpt-4o |
Optional
| Variable | Beschreibung | Standard |
|---|---|---|
OWLAT_DEPLOYMENT_MODE | Blendet nur für Hosted gedachte Oberflächen aus; zeigt das Onboarding-Banner für Self-Hosting | selfhost |
EMAIL_PROVIDER | E-Mail-Anbieter: mta, ses, resend, smtp, mandrill, emailit | — (nicht gesetzt ⇒ unkonfiguriert; Sendungen werden abgelehnt) |
CLAMAV_HOST | ClamAV-Hostname | clamav |
CLAMAV_PORT | ClamAV-Port | 3310 |
GITHUB_WEBHOOK_SECRET | Convex-Funktions-Variable. HMAC-Secret für den Webhook zum GitHub-PR-Merge (POST /webhooks/github, apps/api/convex/webhooks/githubHttp.ts), der eine Code-Aufgabe auf merged schaltet, sobald ihr PR landet. Nicht gesetzt deaktiviert den Endpunkt (liefert 503); mit convex env set setzen. | — |
Wie der Stack sich weiterentwickelt
Die Docker-Compose-Datei wächst mit jeder Phase der Roadmap:
| Phase | Hinzugekommene Dienste | Zweck |
|---|---|---|
| Jetzt (E-Mail-Plattform) | convex, convex-dashboard, web, mta, redis, clamav, updater | Vollständige E-Mail-Marketing-Plattform |
| Als Nächstes (Eingang & Agenten) | — | Agenten-Strecke, Verarbeitung eingehender Mail, Prüf-Warteschlange (alles Convex-Funktionen) |
| Danach (Kommunikationsintelligenz) | — | Wissensgraph, Multi-Channel, CRM, Dateisystem — alles läuft innerhalb von Convex |
| Später (Vollständige Vision) | code-worker (--profile inbox-codetasks, auch dev) | Coding-Agent-Sidecar für die Strecke Feature-Wunsch → Prototyp |
Die Architektur ist so entworfen, dass das Hinzufügen von KI-Fähigkeiten keine zusätzliche erforderliche Infrastruktur nach sich zieht. Agenten-Strecke, Wissensgraph und semantische Suche laufen allesamt als Convex-Funktionen. Das LLM selbst ist eine Konfigurationsentscheidung, kein erforderlicher Dienst — nutzen Sie einen gehosteten OpenAI-/OpenRouter-/Anthropic-Key oder lassen Sie die Inferenz lokal im mitgelieferten Ollama-Container laufen (--profile ai, auch unter --profile dev aktiviert).
Sicherheit & Isolation
Self-Hosting heißt, KI-Agenten auf der eigenen Infrastruktur zu betreiben — was denselben Defense-in-Depth-Ansatz erfordert, der auch auf die E-Mail-Strecke angewendet wird. Das Sicherheitsmodell umfasst die Isolation von Zugangsdaten, das Sandboxing von Prozessen und Hygiene bei Umgebungsvariablen.
Isolation von Zugangsdaten
Funktionen der Agenten-Strecke sehen niemals unmittelbar rohe API-Keys oder Secrets. Sämtliche sensible Konfiguration fließt durch das System für Umgebungsvariablen des Convex-Backends:
- LLM-Zugangsdaten — der API-Key wird innerhalb von
getLLMProvider()inapps/api/convex/lib/llmProvider.tsgelesen; der dortige Credential-ResolverresolveApiKey()akzeptiertLLM_API_KEY,OPENROUTER_API_KEYoderOPENAI_API_KEY(der zuerst gesetzte gewinnt). Der Key taucht nie im Agentenkontext, in LLM-Prompts oder in der Log-Ausgabe auf. - Zugangsdaten der Kanal-Anbieter — API-Keys von Kanälen (SMS/WhatsApp/generisch) sind im Ruhezustand verschlüsselt:
updateChannelConfigplantencryptAndPersistConfigein (apps/api/convex/channels/outbound.ts), das die Konfiguration überlib/credentialCryptoin einen AES-256-GCM-Umschlag hüllt, bevor sie in der ZeilechannelConfigslandet. Entschlüsselt werden sie erst, wenn eine ausgehende Kanalfunktion sie für einen Aufruf beim Anbieter braucht. - Selbst gehostetes Ollama — betreiben Sie einen lokalen Ollama-Container, halten Sie ihn im internen Docker-Netzwerk (
ollama:11434) ohne veröffentlichten Host-Port. Für Ollama ist kein API-Key erforderlich, und das Backend löst dessen Basis-URL automatisch auf, wennLLM_PROVIDER=ollamagesetzt ist.
# If you supply an Ollama container, leave it unpublished —
# reachable only from other Docker services
ollama:
image: ollama/ollama
# No 'ports:' mapping — only accessible via the Docker network
Prozess-Sandboxing
Vom Agenten erzeugte Inhalte laufen in isolierten Ausführungsumgebungen:
- Ausgabe des Visualisierungsagenten — gerendert in
<iframe sandbox="allow-scripts">ohne Zugriff auf das übergeordnete DOM, den Convex-Client, Cookies oder die Navigation. Siehe Visualisierungsagent. - Coding-Agent-Sidecar — der Container
code-worker(betrieben unter--profile inbox-codetasks, ebenfalls durch--profile devaktiviert) erhält sein eigenes Volumecode-workspace, getrennt vom Volumeconvex-datades Backends, und läuft mitno-new-privileges. Er spricht mit dem Backend ausschließlich über die Convex-API-URL.
code-worker:
build:
context: .
dockerfile: apps/code-worker/Dockerfile
volumes:
- code-workspace:/workspace # Isolated workspace, not convex-data
environment:
CONVEX_URL: http://convex:3210
CONVEX_ADMIN_KEY: ${CONVEX_ADMIN_KEY} # authenticates internal-function calls
security_opt:
- no-new-privileges:true
profiles:
- inbox-codetasks
- dev
Hygiene bei Umgebungsvariablen
Für sensible Variablen gelten strikte Geltungsbereichsregeln:
| Geltungsbereich der Variable | Wer sie lesen darf | Beispiel |
|---|---|---|
| Convex-Backend | nur Convex-Funktionen | INSTANCE_SECRET, LLM_API_KEY, BETTER_AUTH_SECRET, UNSUBSCRIBE_SECRET, GITHUB_WEBHOOK_SECRET |
| MTA | nur der MTA-Prozess | MTA_API_KEY, MTA_WEBHOOK_SECRET, DKIM-Schlüssel |
| Web | nur browsersicher | NUXT_PUBLIC_CONVEX_URL, NUXT_PUBLIC_SITE_URL (keine Secrets) |
| Code Worker | nur aufgabenspezifisch | CONVEX_URL (API-Endpunkt), CONVEX_ADMIN_KEY (zum Ansteuern interner Funktionen), LLM_* (für den direkten LLM-Zugriff) |
Convex-Umgebungsvariablen werden niemals an vom Agenten erzeugten Code weitergereicht. Das Sidecar code-worker erhält nur die Convex-Client-URL, den Admin-Key des Deployments und die LLM-Konfiguration. Es pollt eine internalQuery und steuert internalMutations an, die ein anonymer Client nicht erreichen kann — deshalb authentisiert es sich, genau wie apps/imap und apps/mail-sync, mit dem Admin-Key des Deployments (CONVEX_ADMIN_KEY) über setAdminAuth am ConvexHttpClient (apps/code-worker/src/convexClient.ts). Compose reicht den Key in den Dienst hinein (docker-compose.yml). Halten Sie den Worker im internen Docker-Netzwerk und legen Sie seine Endpunkte nicht offen.
Eine Schritt-für-Schritt-Einrichtungsanleitung finden Sie im Self-Hosting-Leitfaden. Diese Seite behandelt die Architektur und die Entwurfsphilosophie.
Erste Schritte
# 1. Clone the repository
git clone https://github.com/wolvesdotink/owlat.git
cd owlat
# 2. Configure secrets
cp .env.selfhost.example .env # then fill in INSTANCE_SECRET, MTA_*, URLs, ...
# 3. Start the stack
docker compose up -d
# 4. Generate the admin key and put it in .env as CONVEX_ADMIN_KEY
docker compose exec convex ./generate_admin_key.sh
# 5. Deploy the tenant functions (one-shot job, reads CONVEX_ADMIN_KEY)
docker compose --profile deploy run --rm convex-deploy
# 6. Open the dashboard
open http://localhost:3000
Der Befehl owlat quickstart im Setup-CLI führt diesen gesamten Ablauf für Sie aus — er erzeugt den Admin-Key am Backend und setzt die Convex-Funktions-Umgebungsvariablen (convex env set) automatisch. Siehe Setup-CLI und Self-Hosting-Leitfaden.