Self-Hosting-Betrieb (Updates, Backup, Env, Monitoring)
Dieses Handbuch richtet sich an Founder, die The-Y CRM selbst hosten. Es deckt den laufenden Betrieb ab: den Docker-Stack, Updates, Backup & Restore, die wichtigsten Umgebungsvariablen, Reverse-Proxy/TLS, Monitoring und die Lizenz. Für Erstinstallation & Server-Größe siehe Installation und Server-Anforderungen.
Der Stack im Überblick
The-Y läuft als Docker-Compose-Stack. Die Dienste:
| Dienst | Zweck |
|---|---|
app | die Web-App (Next.js) — bindet intern auf Port 3000 |
worker | Hintergrund-Jobs (E-Mail-Abruf, Erinnerungen, Sweeps, Automatisierungs-Scheduler) |
postgres | Datenbank (alle Mandantendaten) |
redis | Job-Queue (BullMQ) |
minio | Datei-/Medien-Speicher (Anhänge, Aufnahmen) — S3-kompatibel |
caddy | Reverse-Proxy mit automatischem HTTPS |
watchtower | nur Installationen vor 1.1.53: optionaler Auto-Updater (nächtlicher Image-Pull) — ab 1.1.53 aktualisiert update.sh im Installationsordner (Updates) |
auf Wunsch vom Installer: asterisk, coturn | Telefonanlage und TURN (WebRTC) im Host-Netz, als Zusatz docker-compose.pbx.yml — seit 1.1.58 fragt der Installer danach (Telefonanlage). Früher gab es dazu piper (neuronale Sprachansagen, nur bis 1.1.50) |
optional (ab 1.1.42): translate, translate-models | Übersetzung im Posteingang auf deinem Server (LibreTranslate, internes Netz ohne Internet) bzw. Einmal-Dienst zum Laden der Sprachmodelle — siehe unten |
Updates
Neue Versionen kommen als Images von Docker Hub. Ein Banner in der App meldet neue Versionen (Update-Check gegen crm.the-y.at).
- Automatisch (ab 1.1.53): Neue Installationen aktualisieren sich nachts (03:30 Uhr Serverzeit, plus bis zu 45 Minuten Zufall) mit
update.sh: neue Version holen → Sicherung → App und Worker neu starten → prüfen (gesund + neue Version) → klappt es nicht, läuft automatisch wieder die vorherige Version. Protokoll:update.log. „Jetzt aktualisieren“ in der App startet denselben Ablauf. Bestehende Installationen mit Watchtower stellen einmal um (ca. 5 Minuten): Umstieg von Watchtower auf update.sh. Bei einer Version bleiben:CRM_TAGin der.env. - Nur Installationen vor 1.1.53 (Watchtower): Der
watchtower-Dienst zieht nächtlich neue:latest-Images fürapp/workerund startet sie neu – ohne Sicherung vorher. Wir empfehlen den Umstieg. - Manuell (jederzeit): im Installationsordner
bash update.sh(mit Sicherung und Prüfung; Probelauf, der nichts ändert:bash update.sh --probe). Ohneupdate.sh:
cd ~/the-y-crm docker compose pull app worker docker compose up -d app worker
migrate vor start aus) — kein separater Schritt nötig. Update auf 1.1.53: Beim ersten Start legt die App einen Such-Index für Nachrichten an; bei sehr vielen Nachrichten (über 1 Mio.) überspringt sie das, den Befehl zum Nachholen findest du im Protokoll (docker compose logs app). Die Adresse des Kontos ändern gibt es beim Selbsthosten nicht – du nutzt deine eigene Domain.Automatische Updates abschalten: bash update.sh --zeitplan-aus (danach nur noch von Hand mit bash update.sh).
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, z. B. ~/the-y-crm; ausführlich auch in SELF-HOSTING.md):
update.sh(und, wenn du magst,schluessel.sh) von the-y.at in den Installationsordner laden:cd ~/the-y-crm curl -fsSL https://the-y.at/update.sh -o update.sh curl -fsSL https://the-y.at/schluessel.sh -o schluessel.sh # optional- Probelauf:
bash update.sh --probe– am Ende muss „ERGEBNIS: bereit“ stehen. Steht bei „Sicherung vorher“ kein Sicherungs-Dienst, obwohl du tägliche Sicherungen eingerichtet hast (Installationen von vor 1.1.52), trag in die.envdie ZeileCOMPOSE_FILE=docker-compose.yml:docker-compose.backup.ymlein und starte den Probelauf noch einmal – sonst aktualisiert das Skript ohne Sicherung. Die aktuellen Sicherungs-Skripte gibt es genauso (curl -fsSLO https://the-y.at/backup.sh, ebensorestore.sh,restore-files.shundrestore-test.sh; die Dateidocker-compose.backup.ymlmitcurl -fsSL https://the-y.at/compose-backup.yml -o docker-compose.backup.yml); was dabei noch zu tun ist, steht inSELF-HOSTING.md„Wiederherstellen“. - Watchtower anhalten und entfernen:
docker compose stop watchtower && docker compose rm -f watchtower, dann in derdocker-compose.ymlden ganzen Blockwatchtower:löschen (bis vor den nächsten Dienst bzw. vorvolumes:). - In der
.envdie ZeilenWATCHTOWER_HTTP_API_TOKEN=…undWATCHTOWER_PERIODIC=…löschen (werden nicht mehr gebraucht). Für den Knopf „Jetzt aktualisieren“ muss beiappuntervolumesdie Zeile./update-signal:/they-updatestehen (Installationen ab 1.1.52 haben sie); sonst ergänzen undmkdir -p update-signalausführen:app: # … bestehende Einträge bleiben … volumes: - ./update-signal:/they-updateDanndocker compose up -d app worker. - Zeitplan einrichten:
bash update.sh --zeitplan(als root ein systemd-Timer, sonst ein cron-Eintrag; dazu ein Wächter für den Knopf „Jetzt aktualisieren“). - Prüfen:
systemctl list-timers the-y-crm-update.timerbzw.crontab -l, und einmalbash update.sh --probe.
Die Watchtower-Labels an App und Worker 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 dann ohne Sicherung; sichere vorher selbst (Backup & Restore). Zurück zu Watchtower: bash update.sh --zeitplan-aus, den watchtower-Block wieder in deine Compose-Datei, die beiden .env-Zeilen zurück, docker compose up -d.
Watchtower meldet „client version … is too old“ (nur Installationen vor 1.1.53)
Neue Docker-Versionen lehnen die alte Schnittstelle ab, die Watchtower sonst nutzt; dann kommen keine automatischen Updates mehr. Mit install.sh ist die passende Zeile schon drin, seit 1.1.48 auch in docker-compose.self-host.yml. Hast du anders installiert, ergänzt du beim Dienst watchtower die Zeile (gibt es dort schon environment:, nur die zweite Zeile) und startest ihn neu. Automatische Updates tauschen die Compose-Datei nicht.
watchtower:
# … bestehende Einträge bleiben …
environment:
DOCKER_API_VERSION: "1.44"
docker compose up -d watchtower
Übersetzung (optional, ab 1.1.42)
Der Knopf „🌐 Übersetzen“ im Posteingang übersetzt auf deinem eigenen Server: LibreTranslate im Docker-Netz translate-internal ohne Internetzugang und ohne 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.
| Bedarf | Wert |
|---|---|
| RAM | Leerlauf ~0,1 GB, Deutsch/Englisch/Türkisch voll geladen bis ~0,9 GB; harte Grenze 1,5 GB (TRANSLATE_MEM). Jede weitere Sprache +0,2–0,4 GB. |
| Platte | Sprachmodelle einmalig ~0,6 GB (Deutsch, Englisch, Türkisch), dazu ~0,6 GB für das Image |
| Internet | nur einmal zum Laden der Modelle (Einmal-Dienst translate-models); der laufende Dienst hat keinen Internetzugang |
Einschalten
Im Installationsordner ausführen (z. B. ~/the-y-crm). Hast du mit docker-compose.self-host.yml installiert, ergänzt du nach docker compose jeweils -f docker-compose.self-host.yml.
# 1) Profil einschalten (Sprachen optional, Standard de,en,tr) echo 'COMPOSE_PROFILES=translate' >> .env # echo 'TRANSLATE_LANGS=de,en,tr' >> .env # 2) Sprachmodelle einmalig laden (~0,6 GB, ~1 Minute, braucht Internet) docker compose --profile translate-models run --rm translate-models # 3) Dienst starten und App/Worker neu verbinden docker compose up -d translate app worker
Prüfen: Einstellungen → KI → Übersetzung zeigt „Übersetzungsdienst läuft · Sprachen: EN, DE, TR“, und „Übersetzung testen“ probiert es aus. Ohne den Dienst blendet der Posteingang die Knöpfe „Übersetzen“ seit 1.1.59 aus (kein Warten, kein fremder Dienst), und die Karte in den Einstellungen zeigt die Schritte. Dort kannst du stattdessen deinen eigenen KI- oder DeepL-Schlüssel wählen (DeepL-Free-Schlüssel werden bei WhatsApp, Messenger und Instagram nicht verwendet).
Installation von vor 1.1.42?
Automatische Updates (Watchtower bzw. ab 1.1.53 update.sh) tauschen nur die Images, nicht die Compose-Datei. Deiner Datei fehlen deshalb die Dienste translate + translate-models, das Netz translate-internal und TRANSLATE_URL bei app/worker. Die App läuft trotzdem normal (die Knöpfe „Übersetzen“ sind dann ausgeblendet). Zum Einschalten sicherst du die Datei, ergänzt die Blöcke unten (so stehen sie auch in docker-compose.self-host.yml und in der Compose-Datei, die der Installer ab 1.1.42 schreibt) und führst dann die drei Schritte oben aus.
cp docker-compose.yml docker-compose.yml.bak-pre-translate
# a) bei app UND worker: unter environment: eine Zeile ergänzen, dazu networks: auf Höhe von environment: environment: # … deine bisherigen Zeilen … TRANSLATE_URL: ${TRANSLATE_URL:-http://translate:5000} networks: [default, translate-internal] # b) zwei neue Dienste unter services: (z. B. nach minio) translate: image: libretranslate/libretranslate:v1.9.6@sha256:1de2d7056bb8ad607a412f4563d9abe324ff632b43b5be9428bcc8e213aebb32 profiles: ["translate"] environment: LT_LOAD_ONLY: ${TRANSLATE_LANGS:-de,en,tr} LT_THREADS: "1" LT_DISABLE_WEB_UI: "true" LT_DISABLE_FILES_TRANSLATION: "true" LT_CHAR_LIMIT: "5000" ARGOS_INTRA_THREADS: "2" volumes: ["translate-models:/home/libretranslate/.local"] networks: [translate-internal] mem_limit: ${TRANSLATE_MEM:-1536m} memswap_limit: ${TRANSLATE_MEM:-1536m} healthcheck: test: ["CMD", "./venv/bin/python", "scripts/healthcheck.py"] interval: 30s timeout: 15s retries: 3 start_period: 60s restart: unless-stopped translate-models: image: libretranslate/libretranslate:v1.9.6@sha256:1de2d7056bb8ad607a412f4563d9abe324ff632b43b5be9428bcc8e213aebb32 profiles: ["translate-models"] entrypoint: ["./venv/bin/python", "scripts/install_models.py", "--update", "--load_only_lang_codes", "${TRANSLATE_LANGS:-de,en,tr}"] volumes: ["translate-models:/home/libretranslate/.local"] networks: [default] restart: "no" # c) ganz unten: translate-models bei volumes: ergänzen und das interne Netz anlegen volumes: { postgres-data: {}, redis-data: {}, minio-data: {}, translate-models: {} } # bisherige Einträge (z. B. caddy-data) behalten networks: { default: {}, translate-internal: { internal: true } }
Danach docker compose config -q && echo OK — es muss „OK“ kommen. Weitere Sprachen: TRANSLATE_LANGS erweitern (z. B. de,en,tr,ar), Schritt 2 wiederholen und TRANSLATE_MEM erhöhen (je Sprache +0,2–0,4 GB). Kroatisch, Bosnisch und Serbisch gibt es bei LibreTranslate nicht — dafür den eigenen KI-Schlüssel nutzen. MYMEMORY_EMAIL wird nicht mehr verwendet.
Backup & Restore
Sichere regelmäßig drei Dinge: die Postgres-Datenbank, das MinIO-Volume (Dateien/Medien) und deine .env (enthält u. a. den Verschlüsselungsschlüssel — ohne ihn sind verschlüsselte Secrets verloren!).
Backup
# 1) Datenbank-Dump cd ~/the-y-crm docker compose exec -T postgres pg_dump -U crm crm | gzip > db-$(date +%F).sql.gz # 2) MinIO-Volume (Dateien/Medien) — Name prüfen: docker volume ls | grep minio docker run --rm -v theycrm_minio-data:/data -v "$PWD":/backup alpine \ tar czf /backup/minio-$(date +%F).tgz -C /data . # 3) .env sichern (enthält APP_ENCRYPTION_KEY!) cp .env env-$(date +%F).bak
Lege die drei Dateien an einen getrennten Ort (anderer Server/Storage). Automatisiere das per Cron.
Restore
# DB zurückspielen gunzip -c db-JJJJ-MM-TT.sql.gz | docker compose exec -T postgres psql -U crm crm # MinIO-Volume zurückspielen (Stack vorher stoppen) docker run --rm -v theycrm_minio-data:/data -v "$PWD":/backup alpine \ sh -c "rm -rf /data/* && tar xzf /backup/minio-JJJJ-MM-TT.tgz -C /data"
APP_ENCRYPTION_KEY (AES-256) verschlüsselt gespeichert. Ändert oder verlierst du diesen Schlüssel, sind alle bestehenden Secrets nicht mehr entschlüsselbar. Setze ihn einmal fest und sichere ihn (Teil deiner .env-Sicherung).Schlüssel mit Passphrase bei den Sicherungen (optional, ab 1.1.53)
Zusätzlich zur getrennten Aufbewahrung kannst du den Verschlüsselungs-Schlüssel mit einer Passphrase (einem langen Kennwort) verschlüsselt neben die Sicherungen legen. Dann reicht nach einem Totalausfall die Sicherung von deinem zweiten Speicherort 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.
Neue Installationen (ab 1.1.53) haben schluessel.sh im Installationsordner; ältere holen es einmal mit curl -fsSL https://the-y.at/schluessel.sh -o schluessel.sh.
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?
- Die tägliche Sicherung (
backup.shab 1.1.53; ältere Installationen:curl -fsSLO https://the-y.at/backup.sh) kopiertschluessel.encmit an deinen zweiten Speicherort (BACKUP_REMOTE) – nur die verschlüsselte Datei, nie den Schlüssel im Klartext. - Schlüssel geändert (
APP_ENCRYPTION_KEYneu)? Dannbash schluessel.sh --sichernerneut ausführen. - Die Wiederherstellung (
restore.shab 1.1.53; ältere Installationen:curl -fsSLO https://the-y.at/restore.sh) prüft vor jeder Änderung, ob der Schlüssel in der.envzur Datei passt, und bricht sonst ab. - Neuer Server ohne die alte
.env:install.shausführen, die Sicherungen samtschluessel.encnach~/the-y-crm/backups/kopieren undbash schluessel.sh --zurueckholenstarten – es fragt die Passphrase ab und schreibt den alten Schlüssel in die.env(eine Kopie der bisherigen.envbleibt liegen). Danach die Sicherung zurückspielen und App und Worker mitdocker compose up -d app workerstarten („up“, nicht „start“ – so lesen sie den zurückgeholten Schlüssel).
Umgebungsvariablen (Referenz)
Die wichtigsten Werte in deiner .env (Vorlage: .env.self-host.example). Kritische fett.
| Variable | Zweck |
|---|---|
APP_ENCRYPTION_KEY | Fester 32-Byte-Hex-Schlüssel (openssl rand -hex 32) für die Feld-Verschlüsselung. Nie ändern. |
DATABASE_URL | Postgres-Superuser-Verbindung (Migration/Skripte) |
APP_DATABASE_URL | App-Verbindung als Nicht-Superuser crm_app (erzwingt Row-Level-Security = Mandantentrennung) |
BASE_DOMAIN | deine Domain (z. B. crm.deinefirma.at) — u. a. fürs Softphone (WSS) |
TENANT_MODE | self-host: single (ein Mandant, kein Fremd-Signup) |
LICENSE_KEY | dein Founder-/Lizenzschlüssel; wird online validiert (14 Tage Offline-Toleranz) |
CONTROL_PLANE_URL | Lizenz-Prüf-Endpunkt (Standard: crm.the-y.at) |
S3_ENDPOINT / S3_BUCKET | MinIO-Anbindung (minio:9000 / crm-media) |
MINIO_ROOT_USER / _PASSWORD | MinIO-Zugangsdaten (generieren) |
REDIS_PASSWORD | optional (seit 1.1.60): Passwort für Redis, nur Kleinbuchstaben und Ziffern (z. B. openssl rand -hex 24). Neue Installationen mit install.sh bekommen es automatisch. Bestehende nachrüsten: Zeile in die .env; in der Compose-Datei bei app und worker REDIS_URL: redis://${REDIS_PASSWORD:+:${REDIS_PASSWORD:-}@}redis:6379 und bei redis command: redis-server ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD:-}} eintragen, dann docker compose up -d. Ohne Wert läuft Redis wie bisher ohne Passwort. |
PLATFORM_SMTP_* / PLATFORM_MAIL_FROM | optionaler System-Mailer (Passwort-Reset/Einladungen) ohne eigenes Postfach |
CRM_IMAGE/_WORKER_IMAGE/CRM_TAG | Docker-Hub-Images + Tag (Standard :latest) |
COMPOSE_PROFILES | translate schaltet den optionalen Übersetzungsdienst ein (ab 1.1.42, Übersetzung) |
TRANSLATE_LANGS | Sprachen des eigenen Übersetzungsdienstes (Standard de,en,tr) |
TRANSLATE_MEM | Speichergrenze des Übersetzungsdienstes (Standard 1536m) |
TRANSLATE_URL | nur nötig, wenn dein LibreTranslate woanders läuft (Standard http://translate:5000). MYMEMORY_EMAIL wird seit 1.1.42 nicht mehr verwendet. |
DEFAULT_LOCALE | optional (seit 1.1.59): Sprache der Oberfläche vor der Anmeldung, de, en oder tr. Ohne Angabe gilt die Sprache des Browsers, sonst Deutsch. Nach der Anmeldung gilt die Sprache der Person. |
Telefonie: AMI_SECRET, TURN_SECRET, PBX u. a. | nur bei aktivierter Telefonanlage. Richtet der Installer sie ein (ab 1.1.58), erzeugt er AMI_SECRET und TURN_SECRET selbst und setzt PBX=1 |
Reverse-Proxy & TLS
Das Bundle bringt Caddy mit automatischem HTTPS mit (Let's Encrypt) — du zeigst nur deine Domain per DNS auf den Server. Betreibst du bereits nginx/Apache davor, proxie stattdessen auf die App (intern Port 3000) und terminiere TLS dort. Achte auf durchgereichte WebSocket-Verbindungen (Softphone/Realtime). Für das Telefon im Browser zusätzlich /asterisk-ws weiterleiten (Zeilen für Nginx und Apache).
Monitoring & Health
Der Endpunkt /api/public/health liefert {"ok":true} mit HTTP 200 (gesund) bzw. {"ok":false} mit HTTP 503 (z. B. Datenbank nicht erreichbar oder nur lesbar) — seit 1.1.42 nur noch dieses eine Feld. Hänge ihn an dein Monitoring (Uptime-Check) und werte den HTTP-Status aus, nicht ein Wort im Inhalt. Die Version steht unter /api/public/version. Container-Status prüfst du mit docker compose ps, Logs mit docker compose logs -f app worker.
Lizenz
Der LICENSE_KEY wird regelmäßig online gegen die Control-Plane validiert; bei Ausfall gilt eine 14-Tage-Offline-Toleranz. Ist die Lizenz ungültig/abgelaufen, geht die Instanz in einen schonenden Nur-Lese-Zustand (kein harter Lockout, keine Datenlöschung). Der Funktionsumfang ist identisch zur gehosteten Version.
Ohne Schlüssel läuft die Instanz ab dem ersten Start 14 Tage voll und danach nur lesend, bis du einen Schlüssel einträgst. Mit einem Gratis-Schlüssel gelten die Free-Limits, mit einem Paid-Schlüssel gibt es keine Grenzen.
Fehlerbehebung
Nach einem Neuaufsetzen sind Postfach/WhatsApp/Telefonie „kaputt"
Fast immer hat sich der APP_ENCRYPTION_KEY geändert (z. B. neu generiert), wodurch bestehende verschlüsselte Secrets nicht mehr lesbar sind. Stelle den ursprünglichen Schlüssel aus deiner .env-Sicherung wieder her. Ist er endgültig weg, müssen die externen Secrets (Tokens/Passwörter) neu eingegeben werden.
Mails gehen nicht raus (Timeout)
Viele Cloud-Anbieter (Hetzner, netcup, DigitalOcean, AWS/GCP/Azure) blockieren ausgehende SMTP-Ports (25/465/587) standardmäßig als Spam-Schutz. Lass die Ports beim Anbieter freischalten (Support-Ticket) oder nutze einen HTTP-API-Mailversand. Details: Server-Anforderungen §5b.
Instanz ist im Nur-Lese-Modus
Die Lizenzprüfung schlug fehl (ungültiger/abgelaufener Key oder länger als 14 Tage keine erfolgreiche Online-Prüfung). Prüfe LICENSE_KEY und die Erreichbarkeit der CONTROL_PLANE_URL.
Ein Container startet nicht
docker compose logs app zeigt die Ursache. Häufig: falsche DB-Zugangsdaten, fehlender APP_ENCRYPTION_KEY oder ein belegter Port. Nach .env-Änderungen docker compose up -d --force-recreate app worker.
