Docs › Self-Hosting › Betrieb

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.

Zielgruppe: Self-Hosting (Paid/Founder) · Docker Compose · Linux-Grundkenntnisse hilfreich

Der Stack im Überblick

The-Y läuft als Docker-Compose-Stack. Die Dienste:

DienstZweck
appdie Web-App (Next.js) — bindet intern auf Port 3000
workerHintergrund-Jobs (E-Mail-Abruf, Erinnerungen, Sweeps, Automatisierungs-Scheduler)
postgresDatenbank (alle Mandantendaten)
redisJob-Queue (BullMQ)
minioDatei-/Medien-Speicher (Anhänge, Aufnahmen) — S3-kompatibel
caddyReverse-Proxy mit automatischem HTTPS
watchtowernur 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, coturnTelefonanlage 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).

cd ~/the-y-crm
docker compose pull app worker
docker compose up -d app worker
Datenbank-Migrationen laufen beim App-Start automatisch (der Container führt 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):

  1. 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
  2. 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 .env die Zeile COMPOSE_FILE=docker-compose.yml:docker-compose.backup.yml ein 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, ebenso restore.sh, restore-files.sh und restore-test.sh; die Datei docker-compose.backup.yml mit curl -fsSL https://the-y.at/compose-backup.yml -o docker-compose.backup.yml); was dabei noch zu tun ist, steht in SELF-HOSTING.md „Wiederherstellen“.
  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“ muss bei app unter volumes die Zeile ./update-signal:/they-update stehen (Installationen ab 1.1.52 haben sie); sonst ergänzen und mkdir -p update-signal ausführen:
      app:
        # … bestehende Einträge bleiben …
        volumes:
          - ./update-signal:/they-update
    Dann docker compose up -d app worker.
  5. 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“).
  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 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.

BedarfWert
RAMLeerlauf ~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.
PlatteSprachmodelle einmalig ~0,6 GB (Deutsch, Englisch, Türkisch), dazu ~0,6 GB für das Image
Internetnur 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"
Zusätzlich gibt es im Produkt einen Snapshot-Export/Import je Mandant (Einstellungen → Datensicherung) — praktisch für Umzüge und als anwendungsnahe Sicherung. Der Snapshot enthält keine Dateien (Anhänge, Belege, Rechnungs-PDFs, Aufnahmen) und keine Passwörter, 2FA- oder Webhook-Schlüssel. Wiederhergestellte Nutzer sind deaktiviert und vergeben ihr Passwort über „Passwort vergessen?“. Tarif-, Abrechnungs- und Sicherheits-Einstellungen werden nicht übernommen. Die DB+MinIO-Sicherung oben ist die vollständige Infrastruktur-Sicherung.
⚠ Der Verschlüsselungsschlüssel ist kritisch. Postfach-/SIP-/API-/Kanal-Tokens werden mit 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?

Umgebungsvariablen (Referenz)

Die wichtigsten Werte in deiner .env (Vorlage: .env.self-host.example). Kritische fett.

VariableZweck
APP_ENCRYPTION_KEYFester 32-Byte-Hex-Schlüssel (openssl rand -hex 32) für die Feld-Verschlüsselung. Nie ändern.
DATABASE_URLPostgres-Superuser-Verbindung (Migration/Skripte)
APP_DATABASE_URLApp-Verbindung als Nicht-Superuser crm_app (erzwingt Row-Level-Security = Mandantentrennung)
BASE_DOMAINdeine Domain (z. B. crm.deinefirma.at) — u. a. fürs Softphone (WSS)
TENANT_MODEself-host: single (ein Mandant, kein Fremd-Signup)
LICENSE_KEYdein Founder-/Lizenzschlüssel; wird online validiert (14 Tage Offline-Toleranz)
CONTROL_PLANE_URLLizenz-Prüf-Endpunkt (Standard: crm.the-y.at)
S3_ENDPOINT / S3_BUCKETMinIO-Anbindung (minio:9000 / crm-media)
MINIO_ROOT_USER / _PASSWORDMinIO-Zugangsdaten (generieren)
REDIS_PASSWORDoptional (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_FROMoptionaler System-Mailer (Passwort-Reset/Einladungen) ohne eigenes Postfach
CRM_IMAGE/_WORKER_IMAGE/CRM_TAGDocker-Hub-Images + Tag (Standard :latest)
COMPOSE_PROFILEStranslate schaltet den optionalen Übersetzungsdienst ein (ab 1.1.42, Übersetzung)
TRANSLATE_LANGSSprachen des eigenen Übersetzungsdienstes (Standard de,en,tr)
TRANSLATE_MEMSpeichergrenze des Übersetzungsdienstes (Standard 1536m)
TRANSLATE_URLnur nötig, wenn dein LibreTranslate woanders läuft (Standard http://translate:5000). MYMEMORY_EMAIL wird seit 1.1.42 nicht mehr verwendet.
DEFAULT_LOCALEoptional (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.

Weiter geht's