# Self-Hosting — Kunden-Support CRM Diese Anleitung beschreibt, wie du das CRM auf deinem **eigenen Server** betreibst. Du bekommst dieselbe Software wie die Cloud-Version (crm.the-y.at) — der einzige Unterschied ist, dass **du** den Server betreibst. Die Bilder kommen von Docker Hub und aktualisieren sich automatisch. ## Voraussetzungen - Ein Linux-Server (z. B. 2 CPU / 4 GB RAM / 40 GB SSD genügen für den Start) mit **Docker** + **Docker Compose**. - Eine **Domain** (z. B. `crm.deine-firma.at`), deren DNS-A-Record auf die Server-IP zeigt. - Die Ports **80** und **443** müssen von außen erreichbar sein (Caddy holt darüber automatisch das HTTPS-Zertifikat). - Ein **Lizenzschlüssel** von [the-y.at](https://the-y.at): gratis (Free-Tier mit Limits) oder bezahlt (ohne Limits). Ohne Schlüssel läuft die Instanz ab dem ersten Start 14 Tage voll und danach nur noch lesend, bis ein Schlüssel eingetragen ist. ## Installation ```bash # 1) Konfigurationsdatei anlegen und ausfüllen (Domain, Passwörter, Lizenz) cp .env.self-host.example .env nano .env # 2) Stack starten (App, Worker, PostgreSQL, Redis, MinIO, Caddy, Watchtower) docker compose -f docker-compose.self-host.yml up -d # 3) Datenbank-Schema anlegen (macht die App beim Start auch selbst; hier ausdrücklich, damit Schritt 4 # sicher auf dem fertigen Schema läuft, wie in install.sh; mehrfach ausführbar) docker compose -f docker-compose.self-host.yml exec app npm run migrate # 4) Firma und erstes Admin-Konto anlegen (wie install.sh) docker compose -f docker-compose.self-host.yml exec \ -e ADMIN_EMAIL=admin@deine-firma.at -e ADMIN_PASSWORD='dein-sicheres-passwort' -e COMPANY_NAME='Deine Firma' \ app npm run bootstrap ``` Danach ist das CRM unter **https://** erreichbar. Melde dich mit dem Admin-Konto an. > **Wichtig:** Setze in `.env` **eigene, lange** Werte für `POSTGRES_PASSWORD`, `MINIO_ROOT_PASSWORD` > und `APP_ENCRYPTION_KEY`. Der `APP_ENCRYPTION_KEY` verschlüsselt gespeicherte Zugangsdaten > (E-Mail-Postfächer, API-Tokens) — geht er verloren, sind diese nicht mehr entschlüsselbar. Ältere Installationen > haben ihn unter dem Namen `ENCRYPTION_KEY`; der wird weiter gelesen (seit 1.1.39 — vorher wurde er ignoriert und der > Schlüssel hing am Datenbank-Passwort; vorhandene Daten bleiben lesbar). Am besten den Wert in `APP_ENCRYPTION_KEY` > umbenennen. `APP_SECRET` wird nicht verwendet. ## Ohne Domain im Heimnetz (ab 1.1.58) Auf einem Heimserver (z. B. CasaOS/ZimaOS, TrueNAS) ohne Domain und ohne Zertifikat geht das CRM auch über `http://`. Das ist **nur fürs eigene Netz** gedacht: Die Verbindung ist dann nicht verschlüsselt. Für Zugriff von außen eine Domain mit https verwenden (siehe oben). **Mit `install.sh` (ab 1.1.59):** bei „Deine Domain“ einfach die IP-Adresse des Servers eingeben (z. B. `192.168.1.50`) — der Installer schreibt dann die Werte unten selbst und sagt das. **Von Hand:** in `.env` statt der Domain die Adresse mit `http://` eintragen (Beispiel-IP ersetzen): ```bash CADDY_DOMAIN=http://192.168.1.50 # mit „http://“ davor holt Caddy kein Zertifikat BASE_DOMAIN=192.168.1.50 # nur die Adresse, ohne http:// und ohne Pfad APP_URL=http://192.168.1.50 # beginnt mit http:// → Anmelden über http geht UND alle Links werden http://192.168.1.50/… PUBLIC_BASE_URL=http://192.168.1.50 ``` Danach `docker compose -f docker-compose.self-host.yml up -d`. Öffnet ein App-Store die App direkt über einen Port (ohne Caddy), gehört diese Adresse samt Port in `APP_URL`, z. B. `APP_URL=http://192.168.1.50:3000`. **Links in Mails und Benachrichtigungen:** Die App baut Links (Einladungen, Benachrichtigungen, Termin-Datei, Abmelde- und Umfrage-Links, Report-Zusammenfassung) aus `https://` — **außer** `APP_URL` beginnt mit `http://`: dann aus `APP_URL` (ab 1.1.59; vorher entstand `https://192.168.1.50/…`, und dort antwortet nichts). Deshalb `APP_URL` immer genau so eintragen, wie die App im Browser aufgerufen wird (mit Port, falls einer nötig ist). Solche Links funktionieren nur **im eigenen Netz** — wer die Mail unterwegs öffnet, erreicht die Adresse nicht. Bilder in Mails (Logo, Bilder aus E-Mail-Vorlagen) werden in diesem Fall in die Mail eingebettet, damit Empfänger sie sehen. Woran es hängt: Ohne https speichert der Browser die Anmelde-Cookies nur, wenn sie nicht als „nur über https“ markiert sind. Das macht die App genau dann, wenn `APP_URL` mit `http://` beginnt. Mit `https://` (und in der Cloud) bleibt alles wie bisher. Auf der Anmeldeseite steht dann der Hinweis „Verbindung im Heimnetz nicht verschlüsselt“. **Was über http nicht geht** (Browser erlauben das nur mit https): - Telefon im Browser (Mikrofon) und Videotermine (Kamera), Push am Handy, App aufs Handy legen. - Manche „Kopieren“-Knöpfe — dann den Text markieren und kopieren. - Was eine öffentliche https-Adresse braucht: WhatsApp/Facebook/Instagram, Telegram, Anmelden über Google, Microsoft-365-Postfach verbinden. E-Mail mit IMAP/SMTP und Passwort geht. ## Updates **Neu installiert mit `install.sh` ab 1.1.53:** Updates macht das Skript **`update.sh`** im Installationsordner — nachts automatisch (03:30 Uhr Serverzeit plus bis zu 45 Minuten Zufall), **vorher wird gesichert, danach geprüft**, und klappt etwas nicht, läuft wieder die vorherige Version. Watchtower ist dort nicht mehr dabei (das Projekt wird nicht mehr weiterentwickelt). **Ältere Installationen** und Installationen mit `docker-compose.self-host.yml` nutzen weiter **Watchtower** (nächtlich 04:00 Uhr, fest auf Version 1.7.1), bis du umstellst — Anleitung unten („Umstieg von Watchtower auf update.sh“). Datenbank, Redis und MinIO bleiben bei jedem Update unberührt. - Ein **Banner in der App** (für Admins) meldet, wenn eine neuere Version verfügbar ist. - Datenbank-Änderungen einer neuen Version spielt die App beim Start selbst ein (`npm run migrate` läuft vor dem Start, mehrfach ausführbar). Nach einem Update ist von Hand nichts zu tun. **Auto-Update abschalten** (manuell aktualisieren): mit `update.sh` → `bash update.sh --zeitplan-aus` (danach von Hand `bash update.sh`). Mit Watchtower → den `watchtower`-Dienst aus der Compose-Datei entfernen und Updates selbst einspielen: ```bash docker compose -f docker-compose.self-host.yml pull docker compose -f docker-compose.self-host.yml up -d ``` ### Update-Skript mit Sicherung vorher (ab 1.1.53) Was `update.sh` bei jedem Lauf macht: 1. **Sperre** — es läuft nie zweimal gleichzeitig. 2. **Prüft:** Docker läuft, App und Worker laufen und sind gesund, genug Platz für neue Images (Standard 3 GB, `UPDATE_MIN_FREE_MB`). Ist die App gerade gestoppt (z. B. für eine Wiederherstellung) oder schon krank, wird nicht aktualisiert. 3. **Holt die neuen Images** für App und Worker (Version aus der `.env`: `CRM_TAG`, Standard `latest`). 4. Hat sich nichts geändert, ist es fertig. Sonst **Sicherung** mit dem vorhandenen `backup.sh` im Sicherungs-Dienst. **Scheitert die Sicherung, bricht das Update ab — es wurde nichts geändert** (`UPDATE_BACKUP=pflicht`, Standard bei Installationen mit Backups). `wenn-moeglich`: ohne Sicherungs-Dienst ohne Sicherung weiter · `aus`: nie sichern. 5. Startet **nur App und Worker** neu — Datenbank, Redis und Datei-Speicher nicht. 6. **Prüft die neue Version:** `/api/public/health` meldet „ok“ und die App läuft wirklich mit der neuen Versionsnummer (wartet bis zu 10 Minuten, `UPDATE_HEALTH_WAIT_SEK`; beim Start ergänzt die App die Datenbank). 7. Klappt die Prüfung nicht: **zurück auf die vorherigen Images**, erneut prüfen, klare Meldung. Diese Version wird danach nicht jede Nacht neu versucht, sondern erst wieder, wenn eine neuere erscheint (bewusst trotzdem: `bash update.sh --erneut`). ```bash bash update.sh --probe # Probelauf: zeigt, was passieren würde — ändert nichts, lädt nichts bash update.sh # jetzt aktualisieren bash update.sh --zeitplan # nachts automatisch (als root: systemd-Timer, sonst cron) — install.sh macht das schon bash update.sh --zeitplan-aus # Zeitplan entfernen, danach nur noch von Hand ``` Was passiert ist: `update.log` im Installationsordner (ausführlich), `tail update-signal/verlauf.log` (kurz: Sicherung, „Version vorher“, Ergebnis), `cat update-signal/status` (nur da, wenn das letzte Update übersprungen oder zurückgenommen wurde — mit Grund). Ergebnis-Code: 0 aktuell bzw. aktualisiert · 1 abgebrochen, nichts geändert · 2 läuft schon · 3 neue Version zurückgenommen, die vorherige läuft · 4 auch die vorherige Version antwortet nicht — dann die Meldung lesen: sie nennt die Sicherung von vor dem Update und den Befehl zum Zurückspielen (Abschnitt „Wiederherstellen“). Mit `UPDATE_PING_URL` (z. B. healthchecks.io) meldet das Skript Erfolg und Fehler an deine Überwachung. Die jeweils vorherige Version bleibt als Rückweg auf dem Server (Image `theycrm-vorher/app:letzte`). **Version festhalten / zurück zu einer bestimmten Version:** in der `.env` z. B. `CRM_TAG=1.1.52` eintragen und `bash update.sh` starten — das Skript sichert vorher und stellt auf genau diese Version um. Solange dort eine Nummer steht, ändert der Zeitplan nichts. Später wieder automatisch: `CRM_TAG=latest`. **„Jetzt aktualisieren“ in der App:** `--zeitplan` richtet zusätzlich einen kleinen Wächter ein (als root eine systemd-Pfad-Einheit, sonst ein cron-Eintrag, der jede Minute nachsieht). Der Knopf im Update-Banner legt dann nur die Datei `update-signal/jetzt-aktualisieren` an, und der Wächter startet `update.sh` — mit Sicherung vorher, Prüfung und Rückweg wie nachts (höchstens eine Anfrage je 5 Minuten). Die App selbst bekommt dafür keinen Zugriff auf Docker. Ohne Zeitplan (`--zeitplan-aus`) zeigt die App eine neue Version nur an; aktualisiert wird dann mit `bash update.sh`. Der Knopf braucht bei `app` die Einbindung `./update-signal:/they-update` (in `install.sh` seit 1.1.52 dabei). Dauert die Sicherung länger als 6 Minuten, läuft das Update im Hintergrund weiter — die Seite später neu laden. ### Umstieg von Watchtower auf update.sh (Installationen vor 1.1.53) Es wird nichts automatisch umgebaut — Watchtower läuft weiter, bis du das hier machst (etwa 5 Minuten, im Installationsordner): 1. `update.sh` (und, wenn du magst, `schluessel.sh`) aus diesem Repository (`selfhost/`) in den Installationsordner legen. 2. Probelauf: `bash update.sh --probe` — am Ende muss „ERGEBNIS: bereit“ stehen. 3. Watchtower anhalten und entfernen: `docker compose stop watchtower && docker compose rm -f watchtower`, dann in der `docker-compose.yml` den ganzen Block `watchtower:` löschen (bis vor den nächsten Dienst bzw. vor `volumes:`). 4. In der `.env` die Zeilen `WATCHTOWER_HTTP_API_TOKEN=…` und `WATCHTOWER_PERIODIC=…` löschen (werden nicht mehr gebraucht). Für den Knopf „Jetzt aktualisieren“: prüfen, dass bei `app` unter `volumes` die Zeile `./update-signal:/they-update` steht (Installationen ab 1.1.52 haben sie; sonst ergänzen und `mkdir -p update-signal`). Dann `docker compose up -d app worker`. 5. Zeitplan einrichten: `bash update.sh --zeitplan` (als root ein systemd-Timer, sonst ein cron-Eintrag; dazu der Wächter für den Knopf). 6. Prüfen: `systemctl list-timers the-y-crm-update.timer` bzw. `crontab -l`, und einmal `bash update.sh --probe`. Die Watchtower-Labels an App und Worker, `pre-update.sh` und `update-guard.sh` dürfen bleiben — ohne Watchtower tun sie nichts. Installiert mit `docker-compose.self-host.yml`: dieselben Schritte; damit `update.sh` die richtige Datei nimmt, in die `.env`: `COMPOSE_FILE=docker-compose.self-host.yml`. Dort gibt es keinen Sicherungs-Dienst → das Skript aktualisiert ohne Sicherung (`UPDATE_BACKUP=wenn-moeglich`); sichere vorher selbst (Abschnitt „Backups“) oder nutze `install.sh`. **Zurück zu Watchtower:** `bash update.sh --zeitplan-aus`, den `watchtower`-Block aus `docker-compose.self-host.yml` wieder in deine Compose-Datei, die beiden `.env`-Zeilen zurück, `docker compose up -d`. ### Datei-Speicher (MinIO): neues Image (Oktober 2026) MinIO veröffentlicht seit Oktober 2025 **keine Docker-Images mehr**. `minio/minio:latest` lässt sich nicht mehr laden, und es gibt dafür keine Sicherheitsupdates mehr. Die Compose-Dateien und `install.sh` nutzen deshalb den gepflegten **Chainguard-Build** von MinIO (`cgr.dev/chainguard/minio`, gratis, per Digest fixiert). Das Datenformat ist gleich: **deine Dateien bleiben, wo sie sind, es gibt keine Migration.** **Neu installiert ab 14.10.2026?** Dann ist nichts zu tun. **Ältere Installation?** Dein MinIO läuft weiter, bekommt aber keine Updates mehr. Ein `docker compose pull` bricht beim Dienst `minio` mit einem Fehler ab. So stellst du um (ein paar Sekunden ohne Dateizugriff, keine Daten gehen verloren): ```bash cd ~/the-y-crm # bzw. dein Installationsordner cp docker-compose.yml docker-compose.yml.bak-minio # 1) Image tauschen und als root laufen lassen (deine vorhandenen Dateien gehören root) sed -i 's#^\(\s*\)image: minio/minio:latest#\1image: cgr.dev/chainguard/minio@sha256:2136d593beacf4461187035eca949fe49abd589d5079735025c711ca58fee0c8\n\1user: "0:0"#' docker-compose.yml grep -n -A1 'chainguard/minio' docker-compose.yml # muss die image- und die user-Zeile zeigen # 2) Laden und nur MinIO neu starten docker compose pull minio && docker compose up -d minio docker compose ps minio # "Up" docker compose logs --tail 20 minio # keine "FATAL"- oder "Unable to write"-Zeilen ``` Hast du nicht mit `install.sh`, sondern mit `docker-compose.self-host.yml` (bzw. `.proxy.yml`) installiert: dieselben Befehle mit diesem Dateinamen und `docker compose -f …` ausführen. Danach in der App einen vorhandenen Anhang öffnen und einen neuen hochladen. Zurück geht es mit der Sicherungskopie: `cp docker-compose.yml.bak-minio docker-compose.yml && docker compose up -d minio` (solange das alte Image noch auf dem Server liegt, also kein `docker image prune` vorher). Die Zeile `user: "0:0"` ist Pflicht: ohne sie startet der neue Container als Benutzer 65532 und kann deine vorhandenen Dateien nicht schreiben (`FATAL Unable to initialize backend`). ### Neue Nutzungsbedingungen mit einem Update (ab 1.1.49) Bringt eine neue Version eine **zustimmungspflichtige** Fassung der Nutzungsbedingungen mit, steht das in den Release Notes und im Update-Banner („Das Update enthält neue Nutzungsbedingungen …“). Mit dem Update gelten sie für deine Installation. Nach dem Update sehen Admins **einmal** einen Dialog: **Zustimmen** (wird lokal gespeichert und an den Lizenzserver gemeldet: nur Schlüssel, Kennung der Installation, Fassung und Prüfsumme, **keine Angaben zu Personen**; ist der Lizenzserver nicht erreichbar, wird bei der nächsten stündlichen Lizenzprüfung nachgemeldet) oder **Ablehnen** (nur lokal gespeichert). The-Y sperrt deine Installation deswegen **nie**. Ohne Lizenzschlüssel (Testphase) erscheint kein Dialog. Founder- und Lifetime-Lizenzen behalten die Fassung ihres Kaufs. **Bei der vorherigen Version bleiben** (Free-Lizenz, wenn du nicht zustimmen möchtest): 1. Daten sichern: Einstellungen → Datensicherung → Snapshot herunterladen (oder auf dem Server im Installationsordner `docker compose exec backup sh /usr/local/bin/backup.sh`). 2. In der `.env` der Installation eintragen (sonst holt Watchtower nachts wieder die neueste Version): ```bash CRM_TAG=1.1.48 # die Version vor dem Update (Einstellungen → Lizenz zeigt sie nach dem Ablehnen) WATCHTOWER_PERIODIC=false ``` Mit `update.sh` (ab 1.1.53) genügt die Zeile `CRM_TAG=…` — der Zeitplan hält sich daran. 3. Im Installationsordner `docker compose up -d` ausführen (mit `update.sh`: `bash update.sh` — sichert vorher und prüft danach). 4. Neue Versionen ergänzen die Datenbank nur (additive Migrationen) – die bisherige Version läuft damit weiter. Klappt etwas nicht, die Sicherung zurückspielen (Abschnitt „Wiederherstellen“ unten; das Skript `restore.sh` liegt im Installationsordner). ### Sicherung vor jedem Update und Rückweg (ab 1.1.52) *Dieser Abschnitt gilt für Installationen, die noch mit **Watchtower** aktualisieren (vor 1.1.53 bzw. `docker-compose.self-host.yml`). Mit `update.sh` siehe oben „Update-Skript mit Sicherung vorher“ — dort ist Sicherung, Prüfung und Rückweg eingebaut.* Mit `install.sh` und eingeschalteten Backups macht jedes Update — das nächtliche wie „Jetzt aktualisieren“ — **vorher eine Sicherung**: Bevor Watchtower App und Worker anhält, fragt ein Haken in beiden (`pre-update.sh`) den Sicherungs-Dienst (`update-guard.sh`, benutzt dasselbe `backup.sh` wie die tägliche Sicherung) über den Ordner `update-signal/` an und wartet, bis die Sicherung geprüft fertig ist. **Ohne erfolgreiche Sicherung kein Update** (`UPDATE_BACKUP=pflicht` in der `.env`); Watchtower versucht es in der nächsten Nacht wieder. Was passiert ist, steht im Installationsordner: ```bash cat update-signal/status # nur da, wenn das letzte Update übersprungen wurde — mit Grund tail update-signal/verlauf.log # „Sicherung vor Update: db-….sql.gz — Version vorher: 1.1.51“ ``` - `UPDATE_BACKUP=wenn-moeglich` (Standard ohne Backups): sichern, wenn der Sicherungs-Dienst läuft, sonst ohne Sicherung aktualisieren. `UPDATE_BACKUP=aus`: nie. Große Datenbank: `UPDATE_BACKUP_WAIT_MIN` (Standard 30 Minuten) erhöhen. - Watchtower selbst ist **fest auf Version 1.7.1 per Digest** eingetragen (genau das bisherige „latest“) — es wird nicht mehr still ein anderes Watchtower geladen. Das Projekt wird nicht mehr weiterentwickelt; der Ersatz ist ab 1.1.53 `update.sh` (Umstieg siehe oben). - Hinweis: Der Knopf „Jetzt aktualisieren“ wartet höchstens 6 Minuten auf die neue Version; dauert die Sicherung länger, läuft das Update im Hintergrund weiter — einfach die Seite später neu laden. **Einmal von Hand (Installationen von vor 1.1.52):** Watchtower bringt Dateien im Installationsordner nicht mit. 1. Aus diesem Repository (`selfhost/`) nach `~/the-y-crm` kopieren: `pre-update.sh`, `update-guard.sh`, `docker-compose.backup.yml` (bringt den Sicherungs-Dienst mit dem Wächter mit; siehe auch „Wiederherstellen“ — `restore.sh` usw. gleich mit erneuern). 2. `mkdir -p update-signal` und in die `.env`: `UPDATE_BACKUP=pflicht` (nur mit Backups, sonst `wenn-moeglich`). 3. In `docker-compose.yml` bei **app und worker** die Zeile `labels: […]` ersetzen und `volumes` ergänzen, beim Dienst **watchtower** Image und eine Umgebungs-Zeile ändern (wie die Vorlage in `install.sh` 1.1.52; einfacher ist ab 1.1.53 der Umstieg auf `update.sh`): ```yaml labels: ["com.centurylinklabs.watchtower.enable=true", "com.centurylinklabs.watchtower.lifecycle.pre-update=[ ! -f /usr/local/bin/they-pre-update.sh ] || sh /usr/local/bin/they-pre-update.sh", "com.centurylinklabs.watchtower.lifecycle.pre-update-timeout=40"] volumes: ["./pre-update.sh:/usr/local/bin/they-pre-update.sh:ro", "./update-signal:/they-update"] # watchtower: image: containrrr/watchtower:1.7.1@sha256:6dd50763bbd632a83cb154d5451700530d1e44200b268a4e9488fefdfcf2b038 # unter environment: WATCHTOWER_LIFECYCLE_HOOKS: "true" ``` 4. `docker compose up -d` — danach zeigt `tail update-signal/verlauf.log` beim nächsten Update die Sicherung. Mit `docker-compose.self-host.yml` (ohne Sicherungs-Dienst) ist nur das feste Watchtower-Image dabei: dort vor Updates selbst sichern. **Rückweg — zur Version vor dem Update zurück:** 1. Version und Sicherung von vor dem Update nachsehen: `tail update-signal/verlauf.log`. 2. In der `.env` die alte Version festhalten und die Automatik anhalten, dann neu starten (Docker lädt die alte Version wieder, Docker Hub hält jede Version unter ihrer Nummer): ```bash CRM_TAG=1.1.51 # „Version vorher“ aus dem Verlauf WATCHTOWER_PERIODIC=false ``` `docker compose up -d app worker` 3. Neue Versionen ergänzen die Datenbank nur — die alte Version läuft damit meist weiter. Startet sie nicht sauber oder fehlen Daten: die Sicherung aus Schritt 1 zurückspielen (Abschnitt „Wiederherstellen“: `docker compose stop app worker` → `docker compose exec backup sh /usr/local/bin/restore.sh db-….sql.gz` → `docker compose start app worker`). 4. Später wieder automatisch: `CRM_TAG=latest`, `WATCHTOWER_PERIODIC=true`, `docker compose up -d`. ## Datenbank-Rolle der App (ab 1.1.52) Die App soll mit der **eingeschränkten Datenbank-Rolle `crm_app`** arbeiten — wie in der Cloud. Dann greifen die Schutzregeln der Datenbank: Trennung der Konten auf Zeilen-Ebene (RLS), das Sicherheits-Protokoll lässt sich nicht ändern oder löschen, Lizenz- und Zahlungs-Tabellen sind für die App gesperrt. Ohne `APP_DATABASE_URL` in der `.env` verbindet die App als Eigentümer der Datenbank (`POSTGRES_USER`) — das funktioniert, die Schutzregeln greifen dann aber nicht; die App schreibt beim Start einen Hinweis ins Log (`docker compose logs app | grep ACHTUNG`). - **Neue Installationen** mit `install.sh` (ab 1.1.52) sind automatisch umgestellt (`APP_DATABASE_URL` in der `.env`). - **Bestehende Installationen** — einmal im Installationsordner (die aktuelle `app-rolle.sh` aus `selfhost/` bzw. von einer neuen `install.sh` dorthin legen): ```bash bash app-rolle.sh # fragt nach; App + Worker starten einmal neu (ca. 30 Sekunden) bash app-rolle.sh --pruefen # zeigt, als welche Rolle App und Worker verbunden sind (ändert nichts) ``` Das Skript erzeugt ein zufälliges Passwort (steht nur in der `.env`), setzt es für `crm_app`, legt eine Kopie der `.env` an (`.env.vor-app-rolle-`), startet App + Worker neu und **prüft**, dass beide als `crm_app` (kein Superuser) verbunden sind und die App antwortet. Klappt etwas nicht, stellt es den alten Stand selbst wieder her. Mehrfach ausführbar. - **Zurück:** `bash app-rolle.sh --zurueck` (App wieder als Datenbank-Eigentümer). - **Nach einer Wiederherstellung auf einem neuen Server** (alte `.env` mit `APP_DATABASE_URL`): `install.sh` gleicht das Passwort selbst an; sonst einmal `bash app-rolle.sh` (stellt nur das Passwort der Rolle auf den Wert aus der `.env`). - Installiert mit `docker-compose.self-host.yml`: `COMPOSE_FILE=docker-compose.self-host.yml bash app-rolle.sh`. - Anmeldung, Lizenz und die Datenbank-Änderungen beim Start nutzen weiter `DATABASE_URL` (Eigentümer) — das ist so gewollt. ## Lizenz Trage deinen Lizenzschlüssel als `LICENSE_KEY` in `.env` ein und starte den `app`-Dienst neu (`docker compose -f docker-compose.self-host.yml up -d app`). Die Instanz prüft die Lizenz regelmäßig gegen `CONTROL_PLANE_URL` (Standard: crm.the-y.at) und funktioniert bei kurzen Ausfällen dank einer Toleranzfrist (14 Tage) weiter. Ein **Gratis-Schlüssel** lässt die Instanz mit den **Free-Tier-Limits** laufen, ein bezahlter ohne Limits. **Ohne Schlüssel** ist die Instanz ab dem ersten Start 14 Tage voll nutzbar und danach **nur lesend**, bis ein Schlüssel eingetragen ist. Ist der Schlüssel ungültig oder abgelaufen oder die Toleranzfrist vorbei, gilt ebenfalls „nur lesend“. ## E-Mail mit Microsoft 365 / Outlook.com (optional, ab 1.1.50; „über The-Y“ ab 1.1.52) Microsoft lässt bei Microsoft-365- und Outlook.com-Postfächern keine Anmeldung mit Passwort mehr zu (IMAP schon lange nicht, SMTP schrittweise). Deine Nutzer binden solche Postfächer unter Einstellungen → Kanäle → E-Mail mit **„Mit Microsoft verbinden“** an. Dafür gibt es zwei Wege; der Weg mit Server + Passwort (IMAP/SMTP) bleibt für alle anderen Anbieter wie bisher. ### Weg A: über The-Y (ohne eigene Microsoft-App, ab 1.1.52 — Standard) Nichts einzurichten, wenn deine Installation **einen Lizenzschlüssel** hat (der kostenlose reicht) und unter einer **https-Adresse mit Domainnamen** läuft (`APP_URL`, z. B. `https://crm.deine-domain.at`, ohne Unterpfad). Im Microsoft-Fenster steht dann „The-Y CRM (Self-Hosting)“. So läuft es: 1. Deine Installation legt den Anmelde-Vorgang an (Zufallswert + PKCE-Schlüssel — **bleiben auf deinem Server**) und lässt sich vom Lizenzserver einen versiegelten Anmelde-Wert ausstellen (nur mit gültiger Lizenz; übermittelt werden Lizenzschlüssel, Kennung der Installation, Programmversion, Adresse der Installation, Sprache und der Zufallswert). 2. Microsoft schickt nach der Anmeldung den **einmaligen Anmeldecode** an die Weiterleitungsseite auf crm.the-y.at. Die zeigt „Weiter zu “; der Link trägt den Code nur im Fragment (#…), das nie an einen Server geht. Die Seite speichert und protokolliert nichts (nur Zähler); wie bei jedem Seitenaufruf steht die IP-Adresse kurz im Server-Protokoll. 3. Deine Installation löst den Code **selbst** bei Microsoft ein — ohne den PKCE-Schlüssel, der nur bei dir liegt, ist der Code wertlos. Zugangsschlüssel (Tokens) liegen nur verschlüsselt in deiner Datenbank; die Erneuerung läuft direkt zwischen deinem Server und Microsoft. crm.the-y.at sieht weder Passwort noch Postfach noch Tokens. Braucht ausgehendes HTTPS zu `CONTROL_PLANE_URL` (wie die Lizenzprüfung) und zu `login.microsoftonline.com`. Ist der Lizenzserver nicht erreichbar, sagt die Einrichtung das ehrlich und nennt Weg B. **Abschalten:** `MS_OAUTH_RELAY=off` in die `.env`, dann `docker compose -f docker-compose.self-host.yml up -d app`. Schon verbundene Postfächer laufen weiter (Erneuerung ohne The-Y). ### Weg B: eigene Microsoft-App (wie ab 1.1.50) Hast du eine eigene App eingetragen und ist Weg A auch verfügbar, wählt die Person in der Einrichtung „über The-Y“ oder „eigene App“ (Standard: eigene App). 1. [entra.microsoft.com](https://entra.microsoft.com) → **App-Registrierungen** → **Neue Registrierung**: Name z. B. „CRM“, Kontotypen **„Konten in einem beliebigen Organisationsverzeichnis und persönliche Microsoft-Konten“**, Umleitungs-URI Plattform **Web**: `https:///api/channels/email/oauth/microsoft/callback` (genau `APP_URL` + dieser Pfad; ohne `APP_URL` gilt `PUBLIC_BASE_URL` bzw. `https://`). 2. **Zertifikate & Geheimnisse** → neuer geheimer Clientschlüssel (Ablaufdatum notieren — vor dem Ablauf einen neuen anlegen und tauschen). 3. **API-Berechtigungen** → delegiert: `offline_access`, `openid`, `email`, `profile` und unter **„Von meiner Organisation verwendete APIs“ → „Office 365 Exchange Online“** `IMAP.AccessAsUser.All` + `SMTP.Send`. 4. In die `.env` (Werte nie weitergeben) und App + Worker neu erzeugen: ```bash MS_OAUTH_CLIENT_ID= MS_OAUTH_CLIENT_SECRET= # MS_OAUTH_TENANT=common # Standard; nur eigene Firma: Verzeichnis-ID (Tenant-ID) eintragen docker compose -f docker-compose.self-host.yml up -d app worker ``` 5. Prüfen: Einstellungen → Kanäle → E-Mail-Postfach → Einrichten → Anbieter „Microsoft 365 / Outlook.com“ → „Mit Microsoft verbinden“. Gut zu wissen: In vielen Firmen-Konten muss ein Microsoft-365-Admin der App einmal zustimmen — die App zeigt dann einen Link für den Admin (bei Weg A für „The-Y CRM (Self-Hosting)“; danach zeigt crm.the-y.at nur „Zustimmung erteilt“). Für den Versand muss „Authentifiziertes SMTP“ beim Postfach erlaubt sein. Die Tokens liegen nur verschlüsselt in der Datenbank (`APP_ENCRYPTION_KEY`) und sind nicht in Datensicherungen aus der App (Snapshot) enthalten — nach einer Wiederherstellung einmal „Neu verbinden“. **Rückweg (Weg B):** die beiden Zeilen aus der `.env` nehmen und App + Worker neu erzeugen → ist Weg A verfügbar, läuft „Mit Microsoft verbinden“ danach über The-Y, sonst ist der Knopf weg; mit der eigenen App verbundene Postfächer zeigen nach Ablauf des aktuellen Tokens (max. ~90 min) einen Hinweis und lassen sich trennen und neu verbinden. **Gmail** braucht kein OAuth: Bestätigung in zwei Schritten + App-Passwort (myaccount.google.com/apppasswords), `imap.gmail.com:993` / `smtp.gmail.com:465`. ## Push-Benachrichtigungen am Handy (Beta, ab 1.1.50) Jede Person schaltet Push für ihr Gerät selbst ein: **Sicherheit** (persönliche Einstellungen) → „Push-Benachrichtigungen auf diesem Gerät“. **Du musst nichts eintragen:** Die nötigen Schlüssel (VAPID) entstehen beim ersten Einschalten automatisch und liegen verschlüsselt in der Datenbank (`APP_ENCRYPTION_KEY`; wird der Schlüssel geändert, schalten die Geräte Push einmal neu ein). Wer eigene Schlüssel verwenden will, setzt `VAPID_PUBLIC_KEY`, `VAPID_PRIVATE_KEY` und `VAPID_SUBJECT` in der `.env` (erzeugen: `docker compose -f docker-compose.self-host.yml exec -T app npx tsx src/scripts/vapid-keys.ts mailto:du@deine-firma.at >> .env`, dann App + Worker neu erzeugen). Die Benachrichtigungen gehen verschlüsselt über den Push-Dienst des jeweiligen Geräts (Google, Apple, Mozilla, Microsoft); ohne „Vorschau zeigen“ enthalten sie weder Namen noch Text. Deine Installation braucht dafür ausgehendes HTTPS. iPhone/iPad: nur in der installierten App (Safari → Teilen → „Zum Home-Bildschirm“), ab iOS/iPadOS 16.4, und nur über HTTPS mit gültigem Zertifikat (Caddy erledigt das). ## Sprache der Oberfläche (ab 1.1.59) Wer die App zum ersten Mal öffnet (noch keine Sprache gewählt), sieht sie in der **Sprache seines Browsers** (Deutsch, Englisch oder Türkisch; sonst Deutsch). Fest vorgeben: in die `.env` z. B. `DEFAULT_LOCALE=en` (de, en oder tr) und `docker compose up -d app`. Die Auswahl oben in der App und die Sprache im eigenen Profil gehen immer vor. ## Übersetzung im Posteingang (optional, ab 1.1.42) Ohne diesen Dienst blendet der Posteingang die Knöpfe „Übersetzen“ ab 1.1.59 aus (vorher kam erst beim Klick „nicht eingerichtet“). Der Knopf „🌐 Übersetzen" übersetzt auf **deinem eigenen Server** (LibreTranslate im Docker-Netz `translate-internal` ohne Internetzugang, kein Port nach außen). Kein Text geht an einen fremden Dienst. Der Dienst ist ein **optionales Profil**, weil er bis zu **1,5 GB RAM** braucht (Leerlauf ~0,1 GB, Deutsch/Englisch/Türkisch voll geladen ~0,9 GB). ```bash # 1) in die .env: COMPOSE_PROFILES=translate (Sprachen optional: TRANSLATE_LANGS=de,en,tr) # 2) Sprachmodelle einmalig laden (~0,6 GB, ~1 min, braucht Internet) docker compose -f docker-compose.self-host.yml --profile translate-models run --rm translate-models # 3) Dienst starten und App/Worker neu verbinden docker compose -f docker-compose.self-host.yml up -d translate app worker ``` Prüfen: Einstellungen → KI → Übersetzung zeigt „Übersetzungsdienst läuft · Sprachen: EN, DE, TR" und „Übersetzung testen". Ohne den Dienst meldet der Posteingang sofort „Übersetzung ist auf diesem Server nicht eingerichtet" (kein Warten, kein fremder Dienst), und die Karte in den Einstellungen zeigt die Schritte oben; dort kannst du stattdessen deinen **eigenen KI- oder DeepL-Schlüssel** wählen (DeepL-Free-Schlüssel werden bei WhatsApp/Messenger/Instagram nicht verwendet). **Installation von vor 1.1.42?** Automatische Updates (Watchtower) tauschen nur die Images, nicht die Compose-Datei. Deine Datei hat die Dienste `translate` + `translate-models`, das Netz `translate-internal` und `TRANSLATE_URL` bei `app`/`worker` deshalb noch nicht. Die App läuft trotzdem normal (Übersetzen meldet „nicht eingerichtet"). Zum Einschalten die Blöcke aus `docker-compose.self-host.yml` (Stand 1.1.42) übernehmen: bei `app` und `worker` je `TRANSLATE_URL: ${TRANSLATE_URL:-http://translate:5000}` unter `environment:` und `networks: [default, translate-internal]`, die Dienste `translate` + `translate-models`, unten `translate-models:` bei `volumes:` und `networks: { default: {}, translate-internal: { internal: true } }` — dann die drei Schritte oben. Weitere Sprachen: `TRANSLATE_LANGS` erweitern, Schritt 2 wiederholen, `TRANSLATE_MEM` erhöhen (je Sprache +0,2–0,4 GB). Kroatisch, Bosnisch und Serbisch gibt es bei LibreTranslate nicht. `MYMEMORY_EMAIL` wird nicht mehr verwendet. Details: `docs/deploy-1.1.42-translate.md`. ## Backups (dringend empfohlen) Sichere regelmäßig das PostgreSQL-Volume und den MinIO-Speicher. Ein einfacher DB-Dump (mit `pipefail`, sonst meldet ein fehlgeschlagener `pg_dump` trotzdem Erfolg): ```bash set -o pipefail docker compose -f docker-compose.self-host.yml exec -T postgres \ pg_dump -U crm crm | gzip > crm-backup-$(date +%F).sql.gz && gunzip -t crm-backup-$(date +%F).sql.gz && echo BACKUP_OK ``` ### Sicherungs-Skript erneuern (ab 1.1.51, einmal von Hand) Das mitgelieferte `selfhost/backup.sh` (bzw. die Vorlage in `install.sh`) prüft ab 1.1.51 jeden Dump (`set -eu -o pipefail`, `gunzip -t`, Abschlusszeile von `pg_dump`, Mindestgröße `BACKUP_MIN_BYTES`, Standard 4096) und löscht alte Stände erst nach einer gültigen neuen Sicherung; die neuesten `BACKUP_KEEP_MIN` (Standard 3) bleiben immer. Vorher konnte eine leere Sicherung „OK“ melden, und die Aufbewahrung löschte danach die guten Stände. **Watchtower bringt das Skript nicht mit** (es liegt im Installationsordner, nicht im Image): Ersetze deine `backup.sh` einmal durch die aktuelle Fassung aus diesem Repository (`selfhost/backup.sh`) und starte den Sicherungs-Dienst neu. Ebenso einmal `pbx/asterisk/modules.conf` übernehmen (lädt `System()`/`SHELL()` nicht mehr) und ohne Neustart `docker compose exec -T asterisk asterisk -rx "module unload app_system.so"` sowie `"module unload func_shell.so"` ausführen. ### Sicherheits-Protokoll und Meta-Signatur (ab 1.1.51) - Das Sicherheits-Protokoll ist unveränderbar (Datenbank-Trigger) und bleibt beim „Konto zurücksetzen“ erhalten. Alte Einträge löschst du frühestens nach 365 Tagen per SQL: `select audit_log_purge_before(now() - interval '400 days');` — oder automatisch: mit `AUDIT_RETENTION_DAYS=` (mindestens 365, z. B. 730 = 2 Jahre) löscht der Worker täglich ältere Einträge beider Protokolle und vermerkt das im Protokoll. Ohne die Variable wird selbst gehostet nichts automatisch gelöscht. - Eingehende Nachrichten von WhatsApp/Messenger/Instagram werden nur mit gültigem `META_APP_SECRET` angenommen (ohne → abgewiesen, Warnung im Log). Die Test-Routen `/api/channels/*/inbound/` gibt es in Produktion nur mit `META_SLUG_ROUTES=on` (nicht nötig). - Der Service-PIN für den Support-Zugang gilt nur für die gehostete Cloud; deine Installation hat keinen Zugang von außen. - `OPERATOR_CRM_HOOK_URL`/`_SECRET` sind nur für die Cloud des Betreibers; selbst gehostet sind sie wirkungslos, es wird nie etwas gesendet. - E-Mail-Abruf: Mails über `IMAP_MAX_MAIL_MB` (Standard 40) erscheinen nur als Hinweis im Gespräch; nicht importierbare Mails zeigt die Postfach-Gesundheit. ## Wiederherstellen (ab 1.1.52) Gilt für Installationen mit `install.sh` und eingeschalteten Backups (Dienst `backup`, Ordner `backups/` im Installationsordner, z. B. `~/the-y-crm`). **Alle Befehle im Installationsordner ausführen.** Eine Sicherung besteht aus zwei Dateien mit gleichem Zeitstempel: `db-JJJJMMTT-HHMMSS.sql.gz` (Datenbank) und `files-JJJJMMTT-HHMMSS.tar.gz` (Dateien: Anhänge, Belege, Rechnungs-PDFs, Aufnahmen). Die Skripte liegen im Installationsordner (`restore.sh`, `restore-files.sh`, `restore-test.sh`), im Repository unter `selfhost/`. **Einmal vorher (Installationen von vor 1.1.52):** Watchtower bringt die Skripte nicht mit (sie liegen nicht im Image). Ersetze im Installationsordner `restore.sh`, `backup.sh` und `docker-compose.backup.yml` durch die aktuellen Fassungen aus `selfhost/`, leg `restore-files.sh` und `restore-test.sh` dazu und trag in die `.env` ein (damit jedes `docker compose …` den Sicherungs-Dienst kennt): ```bash COMPOSE_FILE=docker-compose.yml:docker-compose.backup.yml ``` Danach `docker compose up -d backup`. Neue Installationen haben das schon. Das neue `backup.sh` sichert auch die Rechte der Datenbank-Rolle der App mit; ältere Sicherungen lassen sich trotzdem zurückspielen (das Skript meldet dann einen Hinweis). ### Datenbank zurückspielen 1. App und Worker anhalten (das CRM ist ab jetzt nicht erreichbar): ```bash docker compose stop app worker ``` 2. Sicherungen ansehen und eine wählen: `ls -l backups/` 3. Zurückspielen — das Skript fragt nach, zum Fortfahren `WIEDERHERSTELLEN` eintippen: ```bash docker compose exec backup sh /usr/local/bin/restore.sh db-20261021-030000.sql.gz ``` Das Skript prüft zuerst die Datei (unbeschädigt, vollständig, wirklich eine Sicherung dieses CRM), bricht ab, solange noch etwas mit der Datenbank verbunden ist, legt den aktuellen Stand nach `backups/vor-wiederherstellung-db-….sql.gz` und spielt die Sicherung **in einem Schritt** ein. Danach entspricht die Datenbank genau der Sicherung. Geht unterwegs etwas schief, bleibt sie **genau wie vorher** — die Meldung sagt das ausdrücklich. Vor dem Abschluss vergleicht es die Zahl der Konten, Nutzer und Kontakte mit der Datei. 4. Sollen auch die Dateien zurück: weiter mit dem nächsten Abschnitt. Sonst App und Worker starten: ```bash docker compose start app worker ``` Die App ergänzt beim Start selbst, was einer älteren Sicherung an Datenbank-Änderungen fehlt. ### Dateien zurückspielen 1. Zusätzlich den Datei-Speicher anhalten: `docker compose stop app worker minio` 2. Die Datei-Sicherung mit **demselben Zeitstempel** wie die Datenbank zurückspielen (fragt ebenfalls nach): ```bash docker compose run --rm restore-files files-20261021-030000.tar.gz ``` Das Skript prüft das Archiv, bricht ab, solange MinIO oder die App laufen, legt den aktuellen Stand nach `backups/vor-wiederherstellung-files-….tar.gz` und tauscht erst nach vollständigem Entpacken aus. 3. Alles starten und prüfen (anmelden, einen Anhang öffnen): `docker compose start minio app worker` Die Dateien `backups/vor-wiederherstellung-*` löscht kein Skript automatisch — entferne sie, wenn alles passt. Während des Einspielens braucht die Datenbank kurz etwa doppelt so viel Platz wie sonst. Aufträge, die nach der Sicherung in der Warteschlange lagen, können nach dem Zurückspielen ins Leere laufen; das ist harmlos. ### Neuer Server (der alte ist weg) 1. Die `.env` des alten Servers in den neuen Installationsordner legen (`~/the-y-crm/.env`) — **bevor** du `install.sh` startest. `install.sh` übernimmt eine vorhandene `.env` unverändert. Ohne den darin stehenden `APP_ENCRYPTION_KEY` lassen sich gespeicherte Zugangsdaten (Postfächer, Schnittstellen) und die Zwei-Faktor-Anmeldung nicht mehr lesen. 2. `install.sh` wie bei der Erstinstallation ausführen (gleiche Domain, Backups „ja“). 3. Die Sicherungsdateien (z. B. vom Offsite-Ziel) nach `~/the-y-crm/backups/` kopieren. 4. Weiter mit „Datenbank zurückspielen“ und „Dateien zurückspielen“. Postfächer mit Microsoft-Anmeldung zeigen danach einmal „Neu verbinden“. ### Schlüssel getrennt aufbewahren Die Sicherungen enthalten den Schlüssel `APP_ENCRYPTION_KEY` **bewusst nicht** — wer an eine Sicherung kommt, soll damit nicht auch die gespeicherten Zugangsdaten lesen können. Bewahre die Datei `.env` (oder mindestens die Zeile `APP_ENCRYPTION_KEY=…`) **außerhalb des Servers** auf, z. B. im Passwort-Manager: `scp @:~/the-y-crm/.env ./the-y-crm.env`. `install.sh` zeigt den Schlüssel am Ende der Installation an, die App erinnert unter Einstellungen → Datensicherung daran. Ältere Installationen mit `ENCRYPTION_KEY` statt `APP_ENCRYPTION_KEY`: diese Zeile aufbewahren. ### Schlüssel mit Passphrase bei den Sicherungen (optional, ab 1.1.53) Zusätzlich zur getrennten Aufbewahrung kannst du den Schlüssel **mit einer Passphrase verschlüsselt** neben die Sicherungen legen. Dann reicht nach einem Totalausfall die Sicherung vom Offsite-Ziel plus deine Passphrase. Ohne die Passphrase ist die Datei wertlos — für dich und für jeden, der an deine Sicherungen kommt. **Die Passphrase wird nie auf dem Server gespeichert** (nicht in Dateien, nicht im Protokoll); sie wird nur im Terminal abgefragt. Geht sie verloren, hilft die Datei nicht mehr — dann gilt wieder die getrennt aufbewahrte `.env`. Wer das nicht einrichtet, für den bleibt alles wie bisher. ```bash bash schluessel.sh --sichern # Passphrase zweimal eingeben (mindestens 12 Zeichen) → backups/schluessel.enc bash schluessel.sh --pruefen # ab und zu: Passphrase noch gewusst? passt die Datei zum Schlüssel in der .env? ``` - Verfahren: `openssl enc -aes-256-cbc -pbkdf2 -iter 600000 -md sha256` mit eigenem Salz; im verschlüsselten Inhalt steckt eine Prüfsumme, eine falsche Passphrase wird sicher erkannt. Die Datei wird direkt nach dem Anlegen probeweise entschlüsselt. - Die tägliche Sicherung (`backup.sh`) kopiert `schluessel.enc` mit ans Offsite-Ziel (`BACKUP_REMOTE`) — nur eine echte, verschlüsselte Schlüssel-Sicherung, nie einen Schlüssel im Klartext. - Schlüssel geändert (`APP_ENCRYPTION_KEY` neu)? Dann `bash schluessel.sh --sichern` erneut ausführen. - `restore.sh` erkennt die Datei: Fehlt der Schlüssel in der `.env` oder passt er nicht dazu, bricht es **vor jeder Änderung** ab (Code 3) und nennt den Weg unten. Bewusst trotzdem weiter: `--ohne-schluessel`. Die Probe-Wiederherstellung (`restore-test.sh`) zeigt nur einen Hinweis. - Passphrase aus einem Passwort-Manager statt Tastatur: `--passphrase-fd ` (z. B. `bash schluessel.sh --pruefen --passphrase-fd 3 3< <(pass show crm)`). **Neuer Server ohne die alte `.env`, aber mit `schluessel.enc` und Passphrase:** 1. `install.sh` wie bei „Neuer Server“ ausführen (legt eine neue `.env` mit einem neuen Schlüssel an). 2. Die Sicherungen samt `schluessel.enc` nach `~/the-y-crm/backups/` kopieren. 3. Schlüssel zurückholen — fragt die Passphrase ab, schreibt den alten Schlüssel als `APP_ENCRYPTION_KEY` in die `.env` und legt eine Kopie der bisherigen `.env` an (`.env.vor-schluessel-`): ```bash bash schluessel.sh --zurueckholen docker compose stop app worker docker compose up -d backup # der Sicherungs-Dienst liest die geänderte .env docker compose exec backup sh /usr/local/bin/restore.sh db-20261021-030000.sql.gz docker compose up -d app worker # „up“ statt „start“: so lesen App und Worker den zurückgeholten Schlüssel ``` 4. Dateien wie unter „Dateien zurückspielen“. Danach die Kopie `.env.vor-schluessel-…` löschen. ### Probe-Wiederherstellung (empfohlen: wöchentlich) `restore-test.sh` spielt die neueste Sicherung in eine **Wegwerf-Datenbank** (eigener Container ohne Netzwerk, danach gelöscht), bringt sie mit der installierten Version auf den neuesten Stand und prüft Stichproben (Konten, Nutzer, Kontakte, Gespräche). Die echte Datenbank, die Dateien und die Sicherungen werden dabei nie verändert. ```bash bash restore-test.sh # Ergebnis: RESTORE_TEST_OK … bzw. RESTORE_TEST_FEHLER … ``` Wöchentlich automatisch: `crontab -e` und diese Zeile eintragen (sonntags 05:00 Serverzeit): ``` 0 5 * * 0 cd ~/the-y-crm && bash restore-test.sh >> backups/restore-test.log 2>&1 ``` Ist `BACKUP_PING_URL` (oder eine eigene `RESTORE_TEST_PING_URL`) gesetzt, meldet die Probe Erfolg bzw. Fehler an deine Überwachung. ### Ohne `install.sh` installiert (`docker-compose.self-host.yml`) Dort gibt es keinen Sicherungs-Dienst. Eine mit dem Befehl unter „Backups“ erstellte Sicherung spielst du so zurück (Projekt `theycrm`, Netz `theycrm_default`): ```bash docker compose -f docker-compose.self-host.yml stop app worker docker run --rm -it --network theycrm_default --env-file .env -e PGHOST=postgres \ -v "$PWD:/backups" -v "$PWD/selfhost/restore.sh:/r.sh:ro" postgres:16-alpine sh /r.sh crm-backup-2026-10-21.sql.gz docker compose -f docker-compose.self-host.yml start app worker ``` Dateien gehen genauso mit `selfhost/restore-files.sh`, wenn du das Volume `theycrm_minio-data` als `files-….tar.gz` gesichert hast (`-v theycrm_minio-data:/minio-data`, MinIO vorher anhalten). ## Telefonie (PBX) Die eingebaute Telefonanlage (Asterisk für Anrufe, coturn für Video-Termine) ist **optional**. Alles andere — Omnichannel-Inbox, E-Mail/WhatsApp/Social, CRM, Rechnungen, Automatisierung, Belegschaft — läuft ohne sie. ### Telefonanlage gleich mit installieren (ab 1.1.58) `install.sh` fragt **„Telefonanlage gleich mit einrichten?“** — nur auf Linux-Servern mit Docker direkt (nicht Docker Desktop, nicht „rootless“), weil Sprache im Host-Netz läuft. Mit **ja** läuft danach alles wie in der Cloud: Leitungen deines Telefonie-Anbieters, Durchwahlen, Sprachmenüs, Warteschlangen, Voicemail, Aufnahmen und das Telefon im Browser; die Konfiguration erzeugt das CRM selbst, die Anlage lädt Änderungen automatisch. Mit **nein** bleibt alles wie bisher. Nachrüsten geht jederzeit (mehrfach ausführbar, Vorhandenes bleibt): ```bash cd ~/the-y-crm && curl -fsSL https://the-y.at/install.sh -o install.sh && bash install.sh --add-phone ``` Vorbelegen (z. B. ohne Terminal): `PBX=1 PBX_PUBLIC_IP=203.0.113.10 bash install.sh` (`PBX=0` = nein; hinter NAT zusätzlich `PBX_LOCAL_IP=`). Ohne Terminal richtet der Installer die Anlage nur mit `PBX=1` ein. **Was dabei passiert** (die Arbeit macht `telefonanlage.sh` im Installationsordner, auch einzeln nutzbar — `bash telefonanlage.sh --hilfe`): - Vorher prüft er, ob die Ports frei sind (5060/udp, 3478, 5038, 8188). Sind sie belegt, schlägt er „nein“ vor und nennt sie. - Er fragt die öffentliche IPv4-Adresse des Servers (Vorschlag: die eigene Adresse bzw. die, auf die deine Domain zeigt — kein fremder Dienst wird gefragt). Hinter NAT merkt er sich zusätzlich die eigene Adresse (`PBX_LOCAL_IP`). - Die Zugänge `AMI_SECRET` und `TURN_SECRET` entstehen zufällig und stehen nur in der `.env` (werden nie angezeigt). Das AMI-Konto des Workers (`pbx/asterisk/include/manager_custom.conf`) legt er nur an, wenn es fehlt. - Die Dateien der Anlage kommen aus dem Worker-Image (`pbx/asterisk/*.conf`, Reload-Wächter, Platzhalter) und werden an deinen Server angepasst: öffentliche Adresse für Leitungen und Geräte; AMI und Asterisk-HTTP lauschen nur für Container auf diesem Server (`PBX_DOCKER_GW`, meist `172.17.0.1`). - Englische Standardansagen (Asterisk Core Sounds 1.6.1, 2,4 MB von downloads.asterisk.org, mit Prüfsumme) nach `pbx/asterisk/sounds/en`. Ohne Internet: die Datei woanders laden, auf den Server kopieren, dann `PBX_ANSAGEN_DATEI= bash telefonanlage.sh --ansagen`. - Compose-Zusatz `docker-compose.pbx.yml` (Asterisk + coturn im Host-Netz, dieselben fest eingetragenen Images wie die Cloud) — eingetragen in `COMPOSE_FILE` in der `.env`, damit jedes `docker compose …` im Ordner die Anlage kennt. - Telefon im Browser: Mit dem eingebauten Caddy trägt er `/asterisk-ws` ins Caddyfile ein (nur wenn es noch das des Installers ist, sonst zeigt er die Zeilen zum Selbst-Eintragen). Hinter einem eigenen Webserver: Weiterleitung siehe unten. - Firewall: Hat der Installer die Firewall eingerichtet (Härtung, ufw), öffnet er dort die Ports der Anlage; ist ufw sonst aktiv, fragt `--add-phone` nach. Andernfalls nennt er die Ports. Eine Firewall schaltet er nie ab. **Ports** (eingehend — auch in einer Cloud-Firewall deines Anbieters bzw. als Weiterleitung am Router, wenn der Server hinter NAT steht): | Port | wofür | |---|---| | 5060/udp | SIP — Anrufe deines Anbieters, Tischtelefone, Telefon-Apps | | 10000–20000/udp | Sprache (RTP) — ohne sie klingelt es, aber niemand hört etwas | | 3478/udp + 3478/tcp | STUN/TURN für Video-Termine | | 49152–49252/udp | TURN-Weiterleitung für Video-Termine | | 443/tcp | Telefon im Browser über deine Domain (`/asterisk-ws`) — für die App ohnehin offen | **Nie von außen öffnen:** 5038/tcp (AMI) und 8188/tcp (Asterisk-HTTP). Nur Container auf dem Server brauchen sie — mit ufw: `ufw allow from 172.16.0.0/12 to any port 5038,8188 proto tcp`. Alle Befehle: `bash telefonanlage.sh --ports`. **Hinter eigenem Webserver (Modus „hinter Proxy“):** zusätzlich `/asterisk-ws` als WebSocket an `http://:8188/ws` weiterleiten (Wert aus der `.env`, meist `172.17.0.1`; der Installer zeigt die Zeilen mit dem richtigen Wert): ```nginx # Nginx — im HTTPS-vhost deiner Domain, vor „location /“ location = /asterisk-ws { proxy_pass http://172.17.0.1:8188/ws; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $host; proxy_read_timeout 3600s; proxy_send_timeout 3600s; } ``` ```apache # Apache — a2enmod proxy_wstunnel; vor „ProxyPass /“ ProxyPass /asterisk-ws ws://172.17.0.1:8188/ws ProxyPassReverse /asterisk-ws ws://172.17.0.1:8188/ws ``` Ohne diese Weiterleitung klappen Anrufe über Leitungen und Tischtelefone, nur das Telefon im Browser nicht. **Updates:** `update.sh` übernimmt nach jedem geglückten Update mit neuem Worker die Grund-Dateien der Anlage (`telefonanlage.sh --dateien`); die laufende Anlage lädt sie neu, Gespräche laufen weiter. Nie angefasst werden `pbx/asterisk/include/` (Konfiguration aus dem CRM, eigene Leitungen, AMI-Konto), `voicemail/`, `recordings/`, `certs/`, `sounds/custom/` und `moh/`. Eigene Ergänzungen gehören nach `include/` — die Grund-Dateien (`pbx/asterisk/*.conf`) werden ersetzt, die vorige Fassung liegt dann in `pbx/.vorher/`. Neue Stände von Asterisk/coturn kommen mit einer neuen Fassung von `telefonanlage.sh`: dazu `bash install.sh --add-phone` erneut ausführen. **Zustand prüfen:** `bash telefonanlage.sh --pruefen` (Dateien, Ansagen, Container, AMI-Anmeldung des Workers, 5060/udp). **Abschalten:** `docker compose rm -sf asterisk coturn`, in der `.env` bei `COMPOSE_FILE` den Teil `:docker-compose.pbx.yml` entfernen und `PBX=0` setzen, dann `docker compose up -d` (App und Worker starten ohne die Anlage). Die Dateien unter `pbx/` bleiben liegen. **Grenzen (Stand 1.1.58):** nur IPv4; nicht auf dem Standby-Server (slave) — die Anlage läuft auf dem Master; WhatsApp-Anrufe (eigener TLS-Port) und fail2ban für SIP sind nicht vorkonfiguriert (`pbx/fail2ban/README.md`); ohne eigene Wartemusik (Telefonie → Musik) ist die Warteschleife still; liegen deine Docker-Netze außerhalb von `172.16.0.0/12`, brauchen AMI-Konto und Firewall eine Zeile mehr. Von Hand eingerichtet (vor 1.1.58, Dienst `asterisk` in deiner `docker-compose.yml`): Die folgenden Abschnitte gelten weiter; `install.sh --add-phone` lässt eine solche Anlage unangetastet. ### Einmaliger Reload nach dem Update (ab 1.1.32) Gilt, wenn du die Telefonanlage aus `docker-compose.yml` (Dienst `asterisk`) betreibst. Bis 1.1.31 hat Asterisk Änderungen aus dem CRM (Ansagen, Menüs, Durchwahlen, Nummern-Ziele) nur übernommen, wenn ein **externer Cron** neu geladen hat — ohne ihn wirkte nichts. Ab 1.1.32 lädt der **Worker** selbst neu (über die vorhandene AMI-Verbindung, ~3 s nach dem Speichern); ohne AMI übernimmt ein kleiner **Wächter im Asterisk-Container** (~20 s). Was sich bis zum Update aufgestaut hat, lädt der erste Worker-Start automatisch **einmal** neu. 1. Update einspielen (`docker compose pull` bzw. Build, dann `docker compose up -d app worker`) — die App migriert (0170–0172). 2. Die Telefonie-Dateien liegen außerhalb der Images → einmal aus dem Repo übernehmen: `pbx/asterisk/musiconhold.conf`, `pbx/asterisk/reload-watch.sh`, `pbx/asterisk/include/generated-000-placeholder-moh.conf` sowie aus `docker-compose.yml` den Worker-Mount `./pbx/asterisk/include:/pbx-include` (+ `PBX_INCLUDE_DIR`) und `entrypoint`/`command`/`environment` des Dienstes `asterisk`. Danach: `docker compose up -d --force-recreate asterisk worker`. 3. Prüfen: `docker compose logs worker | grep pbx-reload` → „Asterisk hat die Änderungen übernommen". Das AMI-Konto braucht `write = system` (Vorlage `pbx/asterisk/include/manager_custom.conf.example`). 4. Ohne AMI (kein `AMI_SECRET`) einmal von Hand: `docker compose exec -T asterisk asterisk -rx "core reload"` — danach übernimmt der Wächter jede weitere Änderung selbst. 5. Optional (empfohlen), damit alle Mandanten mit dem neuen Generator erzeugt werden (Nummernerkennung, feste Reihenfolge): `docker compose exec -T app npx tsx src/scripts/pbx-resync-all.ts` (macht der Worker selbst, wenn er den Include-Ordner sieht). 6. Kontrolle: `docker compose exec -T asterisk asterisk -rx "dialplan show" | grep -c "did"` ist größer als 0. ### Telefon-Paket (ab 1.1.34): AMI-Leserecht `user` für „Drücken Sie 1 für WhatsApp" Der neue Menü-Baustein **WhatsApp-Rückruf** meldet den Tastendruck per AMI-`UserEvent` (`TheyWaOptIn`) an den Worker; der schickt dann die WhatsApp-Vorlage (bzw. SMS/Vermerk), trägt die Einwilligung ein und benachrichtigt das Team. Damit Asterisk diese Ereignisse an das AMI-Konto des Workers liefert, braucht es die Leseklasse **`user`**: 1. In `pbx/asterisk/include/manager_custom.conf` (auf deinem Server, nicht im Repo) beim Konto `[crm]` die Zeile `read = system,call,dialplan,cdr,agent` ändern auf `read = system,call,dialplan,cdr,agent,user` (Vorlage: `pbx/asterisk/include/manager_custom.conf.example`). 2. AMI neu laden: `docker compose exec -T asterisk asterisk -rx "manager reload"` (bestehende Verbindungen bleiben; der Worker meldet sich nach einem Abbruch selbst neu an). 3. Prüfen: einen Testanruf in ein Menü mit „1 → WhatsApp-Rückruf" machen und 1 drücken → `docker compose logs worker | grep wa-optin` zeigt „Tastendruck verarbeitet → whatsapp|sms|note". Ohne `user` funktioniert der Baustein trotzdem über einen Rückfallweg (das `Newexten`-Ereignis der Klasse `dialplan`, die das Konto schon hat) — die Leseklasse `user` ist der vorgesehene, robuste Weg. Weitere Punkte des Pakets brauchen keine Konfiguration: die Migration `0173` läuft beim App-Start, und der Worker erzeugt nach dem Update alle Mandanten einmal neu (Generator `1.1.34`; ohne Include-Mount: `docker compose exec -T app npx tsx src/scripts/pbx-resync-all.ts`). Wichtig für WhatsApp: die Vorlage (Standard `rueckruf_verpasst` aus der Galerie unter **Vorlagen**) muss in deinem WhatsApp-Konto bei Meta **genehmigt** sein — sonst greift der Ersatzweg (SMS, falls eingerichtet, sonst Vermerk im Posteingang). ### Telefonstimme und eigene Aufnahmen (ab 1.1.50) Die festen Ansagen der Telefonanlage („Alle Leitungen sind belegt“, Hinweis vor einer Aufnahme, Bestätigung nach „1 für WhatsApp“) gibt es auf **Deutsch oder Englisch** (Telefonie → Übersicht → „Telefonstimme“; abweichend je Rufnummer oder Sprachmenü möglich). Jede feste Ansage lässt sich durch eine **eigene Aufnahme** ersetzen — für das ganze Konto oder eine Rufnummer, z. B. auf Türkisch (Karte „Eigene Aufnahmen“ mit Mustertexten zum Vorlesen; MP3, WAV, M4A oder OGG). Gesprochen werden die Ansagen vom Sprachdienst `piper`, falls du ihn betreibst (Image `theyat/crm-piper`, Englisch ab `1.1.50`); ohne ihn spricht die App mit espeak-ng (einfacher, aber überall verfügbar). Nach dem Update erzeugt der Worker die Anlage einmal neu (Generator `1.1.50`, Dialplan-Reload, laufende Gespräche bleiben; ohne Include-Mount: `docker compose exec -T app npx tsx src/scripts/pbx-resync-all.ts`). Englische Standard-Ansagen einmal erzeugen: `docker compose exec -T app npx tsx src/scripts/pbx-1150-sounds.ts` (sonst erzeugt die App sie beim Öffnen von Telefonie → Übersicht). **Ab 1.1.51 überholt** (siehe nächster Abschnitt): keine Sprachausgabe mehr, `pbx-1150-sounds.ts` tut nichts mehr. ### Ansagen, Wartemusik und Gruppen (ab 1.1.51) - **Ansagen sind eigene Aufnahmen** — im Browser aufgenommen (Mikrofon, anhören, neu aufnehmen) oder als Datei hochgeladen; die App gleicht die Lautstärke an und kürzt Stille (ffmpeg im App-Image). Es gibt keine Sprachausgabe mehr (kein Piper, kein espeak-ng). Alte, per Computerstimme erzeugte Ansagen spielen weiter und sind zum Neu-Aufnehmen markiert. Ohne eigene Aufnahme spielt Asterisk die **englischen Standardsätze** aus `pbx/asterisk/sounds/en` (Asterisk Core Sounds EN 1.6.1, Sprecherin Allison Smith, Lizenz CC BY-SA 3.0, Datei `LICENSE-asterisk-core-en-1.6.1` — bitte installiert und unverändert lassen) bzw. einen kurzen Ton. Telefonie → Übersicht zeigt je feste Ansage, ob eine eigene Aufnahme fehlt, und warnt, wenn aufgezeichnet wird, aber der Hinweis „Dieses Gespräch wird aufgezeichnet“ noch nicht aufgenommen ist. - **Wartemusik je Konto:** Telefonie → Musik (Upload nur mit Bestätigung der Rechte), Ablage `pbx/asterisk/moh/t//`. Nach dem Update einmal: `docker compose exec -T app npx tsx src/scripts/pbx-1151-moh-assign.ts` (Probelauf), dann mit `--apply` — übernimmt deine bisherigen Dateien aus `pbx/asterisk/moh/` in dein Konto (Originale nach `pbx/asterisk/moh/_vor-1151-gesichert/`). Optional ein eigenes lizenzfreies `_standard-.wav` (8 kHz mono) für Stellen ohne gewählte Musik, dann `asterisk -rx "moh reload"`. - **Klingeln während der Begrüßung / Gruppen:** Ring-Gruppen mit Tasten oder Aufzeichnung laufen über einen Warteschlangen-Motor (`app_queue`, keine neuen Module). An-/Abmelden je Gruppe per Knopf („Meine Gruppen“) oder `*45` braucht keine neuen AMI-Rechte (`read … user`, `write system` wie bisher); ohne Worker mit AMI wirkt nur `*45`. - **Notrufe** (112, 133, 144, 122, 128, 140, 141, 147, 110) wählt die Anlage immer über die Hauptleitung, vor allen anderen Regeln (eigene Datei `generated--sos-dialplan.conf`, wird vom vorhandenen `generated-*-dialplan.conf`-Muster eingebunden). Notrufe funktionieren nur mit einer eigenen Leitung; hinterlege den Standort bei deinem Anbieter. - Nach dem Update erzeugt der Worker die Anlage einmal neu (Generator `1.1.51`, Dialplan-Reload, laufende Gespräche bleiben).