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.

Warum Self-Hosting zählt

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

Convex BackendDB + Vectors + Files + Real-time
Web AppNuxt dashboard & email builder
MTASMTP delivery & bounce processing
RedisMTA job queue & rate limiting
ClamAVAttachment antivirus scanning
OllamaOptional self-hosted LLM
optional
Required
Optional
All services run via docker compose up

Der selbst gehostete Stack ist in der docker-compose.yml im Wurzelverzeichnis des Repositorys definiert:

DienstRolleImage
Convex-BackendDatenbank, Echtzeit-Subscriptions, Dateispeicher, Vektorsuche, serverlose Funktionenghcr.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
WebNuxt-Anwendung (Dashboard, E-Mail-Builder, Einstellungen)ghcr.io/wolvesdotink/web (oder aus apps/web/Dockerfile gebaut)
MTAEigener Mail Transfer Agent — SMTP-Zustellung, Bounce-Verarbeitung, IP-Warmingghcr.io/wolvesdotink/mta (oder aus apps/mta/Dockerfile gebaut)
RedisJob-Queue für den MTA, verteilte Koordination, Zustand des Rate Limitingsredis:7.4-alpine
ClamAVVirenprüfung für E-Mail-Anhängeclamav/clamav:stable
UpdaterSidecar für In-App-Updates — steuert docker compose pull && up -dghcr.io/wolvesdotink/updater (oder aus apps/updater/Dockerfile gebaut)
Caddy (--profile tls)Reverse Proxy mit automatischem HTTPS für die Produktioncaddy:2.8-alpine
Convex Deploy (--profile deploy)Einmal-Job, der die Tenant-Funktionen ins Backend pushtghcr.io/wolvesdotink/convex-deploy
Code Worker (--profile inbox-codetasks, auch dev)Sidecar für den KI-Coding-Agenten der Strecke Feature-Wunsch → PrototypAus 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:
Umgebungsvariablen des Web-Dienstes

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

VariableGeltungsbereichBeschreibung
INSTANCE_SECRETCompose .envInstanz-Secret des Convex-Backends — openssl rand -hex 32
CONVEX_ADMIN_KEYCompose .envAdmin-Key, nach dem ersten Start erzeugt über docker compose exec convex ./generate_admin_key.sh
MTA_API_KEYCompose .envGemeinsames Secret zwischen Convex und MTA für Versandanfragen
MTA_WEBHOOK_SECRETCompose .envHMAC-Secret für die Authentifizierung der Webhooks MTA → Convex
NUXT_PUBLIC_SITE_URLCompose .envÖffentliche, browserseitige URL der Web-App (z. B. https://owlat.example.com)
NUXT_PUBLIC_CONVEX_URL / NUXT_PUBLIC_CONVEX_SITE_URLCompose .envÖffentliche URLs von Convex-Backend und Site-Proxy
BETTER_AUTH_SECRETConvex-FunktionSecret zum Signieren der Sessions in der Auth-Schicht (mit convex env set setzen)
UNSUBSCRIBE_SECRETConvex-FunktionEigenes 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 vs. NUXT_PUBLIC_SITE_URL

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.

VariableBeschreibungStandard
LLM_PROVIDERAnbietertyp: 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_KEYAPI-Key — OPENROUTER_API_KEY / OPENAI_API_KEY werden ebenfalls akzeptiert; der zuerst gesetzte gewinnt— (für Ollama nicht nötig)
LLM_MODELStandard-Modellbezeichner (oder LLM_MODEL_FAST / LLM_MODEL_CAPABLE je Stufe)gpt-4o

Optional

VariableBeschreibungStandard
OWLAT_DEPLOYMENT_MODEBlendet nur für Hosted gedachte Oberflächen aus; zeigt das Onboarding-Banner für Self-Hostingselfhost
EMAIL_PROVIDERE-Mail-Anbieter: mta, ses, resend, smtp, mandrill, emailit— (nicht gesetzt ⇒ unkonfiguriert; Sendungen werden abgelehnt)
CLAMAV_HOSTClamAV-Hostnameclamav
CLAMAV_PORTClamAV-Port3310
GITHUB_WEBHOOK_SECRETConvex-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:

PhaseHinzugekommene DiensteZweck
Jetzt (E-Mail-Plattform)convex, convex-dashboard, web, mta, redis, clamav, updaterVollstä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() in apps/api/convex/lib/llmProvider.ts gelesen; der dortige Credential-Resolver resolveApiKey() akzeptiert LLM_API_KEY, OPENROUTER_API_KEY oder OPENAI_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: updateChannelConfig plant encryptAndPersistConfig ein (apps/api/convex/channels/outbound.ts), das die Konfiguration über lib/credentialCrypto in einen AES-256-GCM-Umschlag hüllt, bevor sie in der Zeile channelConfigs landet. 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, wenn LLM_PROVIDER=ollama gesetzt 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 dev aktiviert) erhält sein eigenes Volume code-workspace, getrennt vom Volume convex-data des Backends, und läuft mit no-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 VariableWer sie lesen darfBeispiel
Convex-Backendnur Convex-FunktionenINSTANCE_SECRET, LLM_API_KEY, BETTER_AUTH_SECRET, UNSUBSCRIBE_SECRET, GITHUB_WEBHOOK_SECRET
MTAnur der MTA-ProzessMTA_API_KEY, MTA_WEBHOOK_SECRET, DKIM-Schlüssel
Webnur browsersicherNUXT_PUBLIC_CONVEX_URL, NUXT_PUBLIC_SITE_URL (keine Secrets)
Code Workernur aufgabenspezifischCONVEX_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.

Bereit zum Deployment?

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
Einrichtung mit einem Befehl

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.