Produktions-Deployment
Ihre selbst gehostete Owlat-Instanz mit TLS, Firewall-Regeln, Backups und Monitoring absichern.
Diese Anleitung behandelt das Härten Ihrer selbst gehosteten Owlat-Instanz für den produktiven Einsatz. Für die Ersteinrichtung siehe Self-Hosting. Bevor Sie den eingebauten MTA nach außen freigeben, arbeiten Sie den Preflight zu Provider, ausgehenden Ports, Reverse DNS und Warm-up in Versand von einem VPS durch.
Reverse Proxy
Setzen Sie in der Produktion einen Reverse Proxy vor den Docker-Stack, der die TLS-Terminierung übernimmt und den Traffic routet.
Caddy (empfohlen)
Owlat liefert einen fertigen Caddy-Reverse-Proxy als Service caddy innerhalb von docker-compose.yml mit, abgesichert über das Compose-Profil tls. Er beschafft und erneuert TLS-Zertifikate automatisch über Let's Encrypt und proxyt die übrigen Container über ihre Docker-Servicenamen — ein separater Proxy-Host ist nicht nötig.
Beispielkonfiguration kopieren
cp Caddyfile.example Caddyfile
Ihre Domains eintragen
Bearbeiten Sie die Caddyfile, ersetzen Sie jedes example.com durch Ihre echte Domain und setzen Sie die email im globalen Optionsblock (wird für die Registrierung bei Let's Encrypt verwendet). Die mitgelieferte Datei proxyt drei Subdomains auf die netzinternen Servicenamen:
owlat.example.com {
reverse_proxy web:3000
}
api.example.com {
reverse_proxy convex:3210
}
rest.api.example.com {
reverse_proxy convex:3211
}
convex (3210, WebSocket/HTTP zum Browser) und convex-site (3211, HTTP-Actions wie BetterAuth und Webhooks) werden vom selben convex-Container bedient — es sind zwei Ports eines Service, nicht zwei Services.
DNS setzen und Ports öffnen
Legen Sie für jede Subdomain einen A-Record an, der auf diesen Server zeigt, und stellen Sie sicher, dass die Ports 80 und 443 offen sind. Caddy benötigt sie, um die ACME-HTTP-Challenge abzuschließen.
Stack starten
docker compose --profile tls up -d
Der caddy-Service bindet 80/443 und bezieht die Zertifikate beim ersten Start.
Sobald der Proxy läuft, passen Sie Ihre .env auf die HTTPS-URLs an:
NUXT_PUBLIC_CONVEX_URL=https://api.example.com
NUXT_PUBLIC_CONVEX_SITE_URL=https://rest.api.example.com
NUXT_PUBLIC_SITE_URL=https://owlat.example.com
Aktualisieren Sie anschließend die zugehörigen Convex-Umgebungsvariablen:
npx convex env set SITE_URL "https://owlat.example.com" --url http://localhost:3210 --admin-key <key>
# CONVEX_SITE_URL is a Convex BUILT-IN — do not set it via `convex env set`
# (the CLI rejects it). It derives from CONVEX_SITE_ORIGIN on the convex
# container, which docker-compose interpolates from NUXT_PUBLIC_CONVEX_SITE_URL.
Um den Proxy zu testen, ohne die Rate Limits von Let's Encrypt aufzubrauchen, entfernen Sie im globalen Optionsblock der Caddyfile das Kommentarzeichen vor der Zeile acme_ca https://acme-staging-v02.api.letsencrypt.org/directory, starten den Stack und wechseln zurück zur Produktion, sobald DNS und Routing stimmen.
Externer Reverse Proxy (Alternative)
Wenn Sie den Proxy lieber außerhalb von Docker auf dem Host betreiben — oder dem Stack ein bestehendes Nginx/Caddy voranstellen —, verwenden Sie das mitgelieferte tls-Profil nicht. Proxyen Sie stattdessen auf die am Host veröffentlichten Container-Ports. Standardmäßig veröffentlicht docker-compose.yml web (3000) auf allen Interfaces, während convex (3210) und convex-site (3211) nur an 127.0.0.1 gebunden sind — ein hostlokaler Proxy auf localhost:* funktioniert also genau wie beschrieben. Läuft der Proxy auf einem anderen Host, setzen Sie CONVEX_BIND, um 3210/3211 über das Loopback hinaus verfügbar zu machen (und sichern Sie sie per Firewall ab). Da der Proxy hier außerhalb des Docker-Netzwerks läuft, muss er localhost:* ansprechen, nicht die Servicenamen.
server {
listen 80;
server_name owlat.example.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name owlat.example.com;
ssl_certificate /etc/letsencrypt/live/owlat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/owlat.example.com/privkey.pem;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Repeat similar blocks for api.example.com (localhost:3210)
# and rest.api.example.com (localhost:3211).
Installieren Sie die Zertifikate mit Certbot:
sudo certbot --nginx -d owlat.example.com -d api.example.com -d rest.api.example.com
Firewall
Verwenden Sie UFW (oder die Firewall Ihrer Distribution), um den Zugriff auf die notwendigen Ports zu beschränken.
# Allow SSH
sudo ufw allow 22/tcp
# Allow HTTP/HTTPS (reverse proxy)
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Allow SMTP (bounce processing)
sudo ufw allow 25/tcp
# Enable the firewall
sudo ufw enable
Die Ports 3210, 3211, 3000, 3100, 6379 und 3310 sollten aus dem Internet nicht erreichbar sein. Der Reverse Proxy nimmt externen Traffic auf den Ports 80/443 entgegen und leitet ihn an die internen Services weiter.
Beachten Sie, dass web (3000) standardmäßig auf allen Interfaces veröffentlicht wird (damit die SPA für den lokalen Quickstart-Weg erreichbar ist), während convex (3210/3211) an das Loopback gebunden ist. Wenn nur der Reverse Proxy die Web-Oberfläche erreichen soll, setzen Sie WEB_PORT mit vorangestellter Host-IP (z. B. 127.0.0.1:3000) oder sichern Sie den Port per Firewall ab.
Zwei weitere verdienen Beachtung:
3200— der Updater-Sidecar. Er ist konzeptbedingt rein intern (kein veröffentlichter Port indocker-compose.yml, erreichbar nur alshttp://updater:3200über das Docker-Netzwerk). Lassen Sie das so.6791— das Convex-Dashboard. Es ist an das Loopback gebunden (127.0.0.1:6791) und profilgesteuert, also standardmäßig aus und nur über einen SSH-Tunnel erreichbar, nachdem Sie es mit--profile dashboardgestartet haben. Siehe Dashboard absichern weiter unten.
Dashboard absichern
Das Convex-Dashboard auf Port 6791 bietet vollen Lese-/Schreibzugriff auf Ihre Datenbank und hat keine eingebaute Authentifizierung. Die docker-compose.yml im Projektstamm bindet es bereits ausschließlich an das Loopback-Interface (127.0.0.1:${DASHBOARD_PORT:-6791}:6791) und sichert es über das Compose-Profil dashboard ab; in einem frischen Stack ist es also weder öffentlich veröffentlicht noch überhaupt gestartet. Belassen Sie es vor dem Produktivgang dabei.
Option 1: Den ausgelieferten Standard nutzen (empfohlen)
Es ist kein Override nötig — das Dashboard ist bereits an das Loopback gebunden und startet nur unter --profile dashboard. Starten Sie es bei Bedarf und prüfen Sie die Bindung:
docker compose --profile dashboard up -d convex-dashboard
docker compose --profile dashboard port convex-dashboard 6791
# Should show: 127.0.0.1:6791
Option 2: SSH-Tunnel (für Fernzugriff nach der Bindung an localhost)
ssh -L 6791:localhost:6791 user@your-server
# Then open http://localhost:6791 in your browser
Option 3: Per Firewall absichern — wenn Sie die Bindung nicht ändern können, blockieren Sie Port 6791 in der Host-Firewall (er fehlt in den UFW-Regeln oben bewusst), sodass nur das Loopback oder ein VPN ihn erreicht.
Wenn Sie das Dashboard über den mitgelieferten Caddy-Proxy freigeben, sichern Sie es hinter Basic Auth oder einer IP-Allowlist ab — es wird ohne Login ausgeliefert. Der auskommentierte Block convex-dashboard.example.com in Caddyfile.example zeigt ein Beispiel für eine IP-Allowlist.
Redis-Authentifizierung
Vergeben Sie für die Produktion ein Passwort für Redis. Legen Sie eine docker-compose.override.yml an:
services:
redis:
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
mta:
environment:
REDIS_URL: redis://:${REDIS_PASSWORD}@redis:6379
Fügen Sie REDIS_PASSWORD Ihrer .env hinzu:
# Generate a Redis password
echo "REDIS_PASSWORD=$(openssl rand -base64 32)" >> .env
Starten Sie den Stack neu:
docker compose up -d
Backups
Verwenden Sie das mitgelieferte Skript — es ist der einzige Erzeuger von Archiven,
die scripts/restore.sh akzeptiert, und es übernimmt die Konsistenzarbeit für Sie
(kurzes Pausieren von convex + redis rund um den Kopiervorgang, Ermitteln sämtlicher
Projekt-Volumes, Sichern von .env, des Compose-Overrides und der Caddyfile sowie
Schreiben einer SHA256-Sidecar-Datei):
bash scripts/backup.sh # → ./backups/owlat-<timestamp>.tar.gz
owlat backup # same, via the CLI wrapper
Das Volume convex-data ist das kritische — es enthält sämtliche Anwendungsdaten
(Kontakte, Kampagnen, Vorlagen, hochgeladene Dateien, Einstellungen). Das
Convex-Backend speichert seinen Zustand in SQLite, rohe Hot-Kopien des Volumes
sind daher nicht crash-konsistent; das Skript pausiert den Container für die
Sekunden, die das Kopieren dauert. Dasselbe gilt für die mehrteilige AOF von
Redis 7 (appendonlydir/).
Volume-Snapshots des Anbieters (Hetzner, AWS, …) sind eine gute Ergänzung, mit demselben Vorbehalt: Erstellen Sie den Snapshot bei gestopptem Stack, oder nehmen Sie in Kauf, dass ein mitten im Schreibvorgang erstellter Snapshot beim Wiederherstellen eine AOF-/SQLite-Wiederherstellung erfordern kann.
ClamAV-Signaturen und Ollama-Modelle benötigen kein Backup — beide laden sich neu herunter.
Disaster Recovery (neuer VPS / defekte Festplatte)
- Stellen Sie einen frischen VPS bereit und installieren Sie Docker + Compose v2.
- Klonen Sie das Owlat-Repository per
git clone(dieselbe Version wie das Backup — das MANIFEST hält fest, wann es erstellt wurde) und wechseln Sie mitcdhinein. - Kopieren Sie Ihr
owlat-<timestamp>.tar.gzund dessen.sha256-Sidecar-Datei herüber. bash scripts/restore.sh owlat-<timestamp>.tar.gz— das überprüft die Prüfsumme, stellt jedes Volume sowie.env/Override/Caddyfile wieder her und startet den Stack mit Ihren Feature-Profilen.- Richten Sie DNS auf den neuen Server um; TLS-Zertifikate werden automatisch neu ausgestellt, sobald DNS auflöst.
Bewahren Sie mindestens ein Backup außerhalb des VPS auf (Object Storage, Ihr Laptop) — ein Backup auf der defekten Festplatte ist kein Backup.
Monitoring
Healthchecks
Für die kritischen Services (Convex, Redis, ClamAV) sind Docker-Healthchecks bereits konfiguriert. Ihren Status prüfen Sie so:
docker compose ps
Für externes Monitoring erstellen Sie ein Healthcheck-Skript:
#!/bin/bash
# health-check.sh
CONVEX_OK=$(curl -sf http://localhost:3210/version && echo "ok" || echo "fail")
WEB_OK=$(curl -sf http://localhost:3000 && echo "ok" || echo "fail")
MTA_OK=$(curl -sf http://localhost:3100/health && echo "ok" || echo "fail")
echo "Convex: $CONVEX_OK | Web: $WEB_OK | MTA: $MTA_OK"
if [[ "$CONVEX_OK" != "ok" || "$WEB_OK" != "ok" || "$MTA_OK" != "ok" ]]; then
exit 1 # Unhealthy — trigger your alerting system
fi
MTA-Metriken
Der MTA stellt Prometheus-kompatible Metriken bereit:
curl http://localhost:3100/metrics
Verbinden Sie diese mit Prometheus + Grafana, um Dashboards für Versandraten, Bounce-Raten und Queue-Tiefe zu erhalten.
Ressourcenanforderungen
Dies sind informelle Größenklassen, keine Tarife — der OSS-Self-Host-Build kennt weder Stufen noch Abrechnung.
| Größe | vCPU | RAM | Festplatte | Geeignet für |
|---|---|---|---|---|
| Klein | 2 | 4 GB | 40 GB | Bis zu 10.000 Kontakte, geringes Versandvolumen |
| Mittel | 4 | 8 GB | 80 GB | Bis zu 100.000 Kontakte, regelmäßige Kampagnen |
| Groß | 8 | 16 GB | 160 GB | 100.000+ Kontakte, hohes Versandvolumen |
ClamAV belegt für die Virendefinitionen etwa 1 GB RAM. Wenn der Arbeitsspeicher knapp ist, können Sie ClamAV deaktivieren, indem Sie es aus der Docker-Compose-Datei entfernen und die Konfiguration von CLAMAV_HOST/CLAMAV_PORT weglassen.