Einstieg

Customermates mit Docker Compose selbst betreiben: Installation, .env-Werte, E-Mail, Registrierung, Updates, Backup und Restore, Unterschiede zur Cloud.

Self-Hosting betreibt Customermates mit zwei Dateien, docker-compose.yml und .env, und dem Befehl docker compose up -d. Beide Dateien holen Sie mit curl aus dem Customermates-Repo; ein git clone und ein Build-Schritt sind nicht nötig. Die Compose-Datei startet zwei Container: PostgreSQL 16 (postgres:16-alpine) und das App-Image ghcr.io/customermates/customermates:latest, das bei jedem Start ausstehende Datenbank-Migrationen anwendet. Die Migrationen legen die Erweiterung pg_trgm an: Ab PostgreSQL 13 gilt sie als vertrauenswürdig, manche verwaltete Anbieter verlangen aber, dass Sie sie vorher freischalten.

Sollte ich selbst hosten oder die Cloud nutzen?

EntscheidungsfaktorManaged CloudSelf-Hosted
Betrieb von Anwendung und DatenbankCustomermates übernimmt den BetriebSie betreiben Docker, PostgreSQL, Netzwerkzugriff, TLS, Updates und Backups
Kern-CRM und AnsichtenJe nach Tarif enthaltenStarter-Basis
Einheitlicher Posteingang und verbundene KalenderIn berechtigten Cloud-Tarifen verfügbarNicht enthalten
KI-ZugriffExternes MCP sowie der gehostete Assistent Mate mit den KI-Credits Ihres TarifsExternes MCP mit eigenem Client und Anbieter
Verbundene AnbieterIm verwalteten Produkt konfiguriertSie konfigurieren und bewerten jeden externen Anbieter
KostenCloud-Abonnement pro Nutzer ab 12 € pro MonatInfrastruktur, Anbieter und Betriebszeit Ihres Teams

Beim Self-Hosting wählen Sie, wo Anwendung und Datenbank laufen. Daraus entsteht nicht automatisch eine Garantie für Sicherheit, Compliance, Datenschutz oder einen isolierten Betrieb; Ihr Unternehmen bleibt für Konfiguration, Anbieter, Verträge, Aufbewahrung und Betriebskontrollen verantwortlich. Vergleichen Sie die gesamten Betriebskosten, statt pauschal von einem günstigeren Modell auszugehen: Infrastruktur, Backups, Wiederherstellungstests, Monitoring, Updates, Störungsbehandlung und externe Anbieter.

Datensätze wandern als Excel-Arbeitsmappen zwischen Cloud und Self-Hosting, jeweils eine Liste auf einmal. Ein vollständiger Plattformwechsel bleibt ein separat entworfenes und geprüftes Projekt.

Ist Self-Hosting kostenlos, und brauche ich eine Lizenz?

Eine Self-Hosted-Instanz fasst beliebig viele Workspaces, und ein Login kann zu mehreren davon gehören (Workspaces). Kein Workspace hat Abonnement oder Testphase: Jeder startet aktiv, zählt keine Nutzerplätze und rechnet nichts ab. Sie zahlen nur für Ihre eigene Infrastruktur und Ihre Anbieter.

Customermates ist Open Core. Code außerhalb von ee/ und Material in ee/, das an den Browser ausgeliefert wird, stehen unter AGPL-3.0-only. Der serverseitige Code in ee/ steht unter der Customermates Commercial License, die den Betrieb des offiziellen Images mit APP_MODE=self-hosted als Community Build erlaubt, auch produktiv, solange die Cloud-Funktionen ausgeschaltet bleiben. Die genauen Bedingungen stehen in LICENSE und ee/LICENSE.md. Ein gekaufter Cloud-Tarif schaltet in einer Self-Hosted-Installation nichts frei.

Welche Funktionen fehlen beim Self-Hosting?

Jede Self-Hosted-Installation umfasst den Funktionsumfang von Starter. Diese Cloud-Funktionen bleiben ausgeschaltet, auch wenn Sie zusätzlich einen Cloud-Tarif bezahlen: verbundene Messaging-Konten (E-Mail, LinkedIn, WhatsApp, Instagram, Telegram), der einheitliche Posteingang, verbundene Kalender und ihre Ereignisse, der gehostete Assistent Mate mit seinen gehosteten KI-Credits, Routinen, die Mate ausführt, und die KI-Einrichtung im Onboarding, daher startet ein neuer Workspace ohne Listen. Externe KI-Clients funktionieren weiterhin über MCP mit Ihrem eigenen KI-Anbieter. Enterprise-SSO (SAML) und White-Labeling sind in keiner Variante implementiert.

Die App blendet aus, was nicht gilt: Die Seitenleiste zeigt weder Posteingang noch Routinen, die Einstellungen haben keine Seiten Abrechnung und Kanäle, und der Rolleneditor blendet die Zeilen Inbox-Nachrichten und Routinen aus. Wer eine dieser Seiten per URL öffnet, landet wieder auf dem Dashboard.

Alles andere funktioniert wie in der Cloud: Listen und Ansichten, Excel-Import und -Export, Konfigurieren, Dashboard-Widgets, Mitglieder und Rollen, das Aktivitätsprotokoll, REST, Webhooks und MCP.

Wie installiere und hoste ich Customermates selbst mit Docker Compose?

Sie brauchen Docker mit Docker Compose v2; das Image ist für linux/amd64 und linux/arm64 gebaut. Einen Domain-Namen brauchen Sie nur für HTTPS.

  1. Beide Dateien holen

    mkdir customermates && cd customermates
    curl -fsSL https://raw.githubusercontent.com/customermates/customermates/main/docker-compose.yml -o docker-compose.yml
    curl -fsSL https://raw.githubusercontent.com/customermates/customermates/main/.env.selfhost.template -o .env
  2. .env ausfüllen

    Ersetzen Sie die Platzhalter in .env vor dem ersten Start, mindestens POSTGRES_PASSWORD, BETTER_AUTH_SECRET (zum Beispiel aus openssl rand -hex 32), BASE_URL, RESEND_API_KEY und RESEND_OPERATOR_EMAIL. Ohne funktionierenden RESEND_API_KEY geht keine E-Mail hinaus; dann kann niemand seine E-Mail-Adresse bestätigen, und jedes neue Konto wird nach 24 Stunden festgehalten.

  3. Starten

    docker compose up -d
    docker compose logs -f app

    Der erste Start dauert etwa eine Minute, während die Migrationen laufen. Öffnen Sie danach <BASE_URL>/auth/signup, standardmäßig http://localhost:4000/auth/signup.

  4. Registrieren

    Registrieren Sie sich, wählen Sie Neuen Workspace erstellen und schließen Sie den Wizard ab; Ihr Konto wird Admin. Siehe Wie lege ich das erste Konto auf meiner Instanz an?.

Welche Umgebungsvariablen gehören in die .env?

  • POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB: Benutzer, Passwort und Name der Datenbank (Vorlage: postgres, change-this-password, customermates). Setzen Sie diese Werte vor dem ersten Start. PostgreSQL übernimmt sie nur einmal, beim Anlegen des Datenbank-Volumes; eine spätere Änderung in .env ändert die bestehende Datenbank nicht, und die App kann sich dann nicht mehr verbinden.
  • BETTER_AUTH_SECRET: ein langer Zufallswert, der die Anmeldesitzungen signiert. Wenn Sie ihn später ändern, werden alle abgemeldet; bewahren Sie deshalb eine Kopie bei Ihren Backups auf.
  • BASE_URL: genau die Adresse, die im Browser geöffnet wird, siehe Worauf muss BASE_URL gesetzt sein?.
  • APP_PORT: der Host-Port, unter dem die App veröffentlicht wird, standardmäßig 4000.
  • RESEND_API_KEY, RESEND_OPERATOR_EMAIL: E-Mail-Versand; RESEND_API_KEY darf nicht leer sein. Siehe Wie richte ich E-Mail mit Resend ein?.
  • APP_MODE: bleibt self-hosted; die Compose-Datei setzt diesen Wert für die App selbst.
  • WEBHOOK_BLOCK_PRIVATE_DESTINATIONS: optional; true sperrt Webhook-Anfragen an private, Loopback- und Link-Local-Adressen. Standardmäßig sind private Ziele erlaubt, damit Webhooks Dienste im eigenen Netzwerk erreichen. Akzeptiert nur true oder false.
  • CRON_SECRET: optional und in der Vorlage auskommentiert; das Secret für die geplanten Wartungsaufrufe, siehe Welche geplanten Aufgaben muss ich ausführen?.
  • GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, AZURE_AD_CLIENT_ID, AZURE_AD_CLIENT_SECRET: optional und in der Vorlage auskommentiert, siehe Können sich Nutzer mit Google oder Microsoft anmelden?.
  • ADDRESS_LOOKUP_URL: optional und in der Vorlage auskommentiert; schaltet die Adresssuche über einen selbst betriebenen Photon-Dienst ein, siehe Wie werden Adressen geprüft, und wie schalte ich die Adresssuche ein?.
  • POSTAL_CODE_DATA_URL: optional und in der Vorlage auskommentiert; woher die Postleitzahlen für die Vorschläge von Ort und Region kommen: ohne Wert von GeoNames, sonst von der URL eines Mirrors, oder off, siehe Wie werden Adressen geprüft, und wie schalte ich die Adresssuche ein?.
  • FILE_STORAGE_DRIVER, FILE_MAX_UPLOAD_BYTES und die S3_-Variablen: optional und in der Vorlage auskommentiert; wo Dateifelder ihre Dateien ablegen und wie groß eine sein darf, siehe Wo werden Dateien beim Self-Hosting gespeichert?.

Halten Sie .env aus der Versionsverwaltung heraus. Änderungen wenden Sie mit docker compose up -d an.

Worauf muss BASE_URL gesetzt sein?

Setzen Sie BASE_URL auf genau die Adresse, die im Browser eingegeben wird: Schema, Hostname und, sofern er nicht 80 oder 443 ist, den Port, ohne Pfad. Beispiele: https://crm.example.com oder http://192.168.1.20:4000. Der Vorlagenwert http://localhost:4000 funktioniert nur auf dem Rechner, auf dem Docker läuft. Enthält BASE_URL einen Pfad, eine Query oder Zugangsdaten, startet die App nicht.

Customermates bildet diese Adressen aus BASE_URL:

  • Anmelde-Weiterleitungen, die Links in Einladungs- und anderen E-Mails und die MCP-OAuth-Adresse;
  • auf einer Doku-Seite die MCP-Adresse in den Installations-Snippets und hinter MCP-Config kopieren, MCP-Command kopieren, Mit Cursor verbinden und Mit VS Code verbinden sowie die Seitenadresse, die In ChatGPT öffnen und In Claude öffnen weitergeben;
  • in der REST-API-Referenz die Server URL, die Codebeispiele und die Adresse, die Send aufruft;
  • die Links in llms.txt und llms-full.txt (Seiten als Raw-Markdown), die Adressen in sitemap.xml und robots.txt und die Canonical-Links der öffentlichen Seiten;
  • die url, die search_docs, get_docs_page, search und fetch zurückgeben und deren Ursprung Agents vor eine App-Route setzen, um einen vollständigen Link zu bilden.

Kommt eine Anfrage unter einem anderen Hostnamen, einer anderen IP-Adresse oder einem anderen Port an, zeigen diese Adressen trotzdem auf BASE_URL: Nutzer können nach der Anmeldung auf der falschen Adresse landen, und Send in der REST-API-Referenz schlägt fehl, weil der Browser einer Seite, die unter einer anderen Adresse geöffnet ist, keinen Aufruf der API unter BASE_URL erlaubt.

Drei Adressen, die die App zum Kopieren anzeigt, verwenden stattdessen die Adresse, unter der der Browser die App geöffnet hat: der Einladungslink im Tab Link teilen von Benutzer einladen und des Einladungsschritts im Onboarding, die MCP-Adresse bei den Schnellverbindungen und im Onboarding-Schritt Verbinde die KI, die du schon nutzt sowie Links über Link kopieren einer gespeicherten Ansicht. Kopieren Sie sie aus einem Browser, der die öffentliche Adresse nutzt.

Wenn Sie die Domain oder APP_PORT ändern, passen Sie auch BASE_URL an und führen docker compose up -d aus. Bei einer https://-Adresse sind die Anmelde-Cookies als Secure markiert; die App muss dann über HTTPS geöffnet werden.

Wie betreibe ich Customermates hinter einem Reverse-Proxy mit HTTPS?

Richten Sie einen Reverse-Proxy wie Caddy, nginx oder Traefik auf den App-Port (4000 oder Ihren APP_PORT) und setzen Sie BASE_URL auf die öffentliche https://-Adresse. Caddy besorgt das TLS-Zertifikat selbst:

crm.example.com {
  reverse_proxy localhost:4000
}

Die App nimmt Schema und Host für ihre Links aus BASE_URL, weitergeleitete Header wie X-Forwarded-Proto sind deshalb nicht nötig. Der Proxy muss den ursprünglichen Host-Header weitergeben, weil die App Formularübermittlungen ablehnt, deren Herkunft nicht dazu passt, auch bei Anmeldung und Registrierung. Caddy und Traefik tun das standardmäßig; in nginx ergänzen Sie proxy_set_header Host $host;. Die Compose-Datei veröffentlicht den App-Port auf allen Netzwerkschnittstellen. Soll nur der Proxy ihn erreichen, ändern Sie die Zuordnung in docker-compose.yml auf "127.0.0.1:${APP_PORT:-4000}:4000" oder sperren Sie den Port in Ihrer Firewall.

Wie richte ich E-Mail mit Resend ein, und geht auch SMTP?

Customermates versendet E-Mails nur über Resend; eine SMTP-Option gibt es nicht. Verifizieren Sie Ihre Absender-Domain in Resend, erstellen Sie einen API-Key und setzen Sie:

  • RESEND_API_KEY: den Resend-API-Key.
  • RESEND_OPERATOR_EMAIL: die Absenderadresse, auf einer in Resend verifizierten Domain. Jede E-Mail geht als Customermates <RESEND_OPERATOR_EMAIL> hinaus. Dieselbe Adresse erhält eine E-Mail „New User Registration“, wenn jemand den Profilschritt des Wizards speichert (siehe Wer kann sich registrieren), sowie die Nachrichten, die über Feedback senden im persönlichen Menü gesendet werden.

Die App verschickt Bestätigungslinks, Passwort-Resets und Einladungen per E-Mail. Kann eine E-Mail nicht hinausgehen, funktioniert die Registrierung trotzdem: Neue Nutzer können ihr Konto 24 Stunden lang verwenden und werden danach samt ihren API-Keys und MCP-Verbindungen auf E-Mail-Verifizierung festgehalten, bis sie bestätigen. Passwort vergessen? verschickt nichts, Feedback senden schlägt fehl, und der Tab Link teilen in Benutzer einladen funktioniert weiterhin.

  • Ohne RESEND_API_KEY scheitert jede Sendung mit RESEND_API_KEY is not configured im Log der App, und Einladungen per E-Mail schlagen mit einem Fehler fehl.
  • Lehnt Resend eine E-Mail ab oder erreicht der Server Resend gar nicht, etwa hinter einer Firewall, die ausgehenden Verkehr verwirft, oder ohne Internetzugang, gibt die Sendung nach höchstens 10 Sekunden auf, und das Log der App zeigt [EmailService] Email not sent mit dem Betreff, der Domain des Empfängers und dem Grund von Resend. Einladungen senden meldet dann die Einladungen, die nicht gesendet werden konnten, und lässt nur deren Adressen im Formular stehen; die Einladungsaufrufe über REST und MCP geben ihre Anzahl als failed und die Adressen als failedEmails zurück. Wird eine Einladung erneut gesendet, etwa nach einer Zeitüberschreitung, erhält eine Adresse sie innerhalb von 24 Stunden nicht zweimal. Ohne Verbindung zu Resend dauern Registrierung und Passwort vergessen? diese 10 Sekunden.

Kommen E-Mails nicht an, prüfen Sie das Log der App und ob die Absender-Domain verifiziert ist, und suchen Sie im Resend-Dashboard nach abgelehnten Sendungen.

Wie lege ich das erste Konto auf meiner Instanz an?

Öffnen Sie <BASE_URL>/auth/signup und registrieren Sie sich mit E-Mail und Passwort über Konto erstellen, oder mit Google oder Microsoft, falls Sie diese eingerichtet haben. Wählen Sie im nächsten Schritt Neuen Workspace erstellen. Der erste Schritt des Wizards fragt nach Vorname, Nachname und Anzeigesprache und legt den Workspace an; die übrigen Schritte können Sie überspringen und den Wizard mit Fertig schließen. Das Konto, das den Workspace anlegt, erhält die eingebaute Rolle Admin mit allen Berechtigungen. Der neue Workspace hat noch keine Listen: Legen Sie die benötigten unter Konfigurieren an oder mit Liste erstellen auf dem leeren Dashboard; siehe Kernkonzepte.

Klicken Sie den Bestätigungslink in der E-Mail innerhalb von 24 Stunden an. Ein Konto, das 24 Stunden nach der Registrierung noch nicht bestätigt ist, wird auf E-Mail-Verifizierung festgehalten, wo Verifizierungs-E-Mail erneut senden einen neuen Link schickt.

Öffnen Sie dann Mitglieder, laden Sie Teammitglieder mit Hinzufügen ein, geben Sie sie dort nach der Registrierung frei, und legen Sie unter Rollen fest, was sie dürfen; siehe Mitglieder und Rollen.

Wer kann sich auf meiner Instanz registrieren?

Jede Person, die Ihre BASE_URL erreicht, kann sich registrieren; die Registrierung lässt sich nicht abschalten. Wer Neuen Workspace erstellen wählt, erhält einen eigenen Workspace mit eigenem Admin, getrennt von Ihrem, aber in Ihrer Datenbank gespeichert; löschen kann ihn nur ein Admin dieses Workspace, siehe Wie lösche ich einen Workspace?. Jede angemeldete Person kann außerdem jederzeit mit Workspace erstellen im Workspace-Menü weitere Workspaces anlegen. Eine E-Mail „New User Registration“ geht an RESEND_OPERATOR_EMAIL, wenn jemand den Profilschritt des Wizards speichert, und bei einer Anmeldung mit Google oder Microsoft auch dann, wenn sich jemand vorher anmeldet. Wer sich mit E-Mail und Passwort registriert, diesen Schritt aber nie speichert, löst keine aus, und die Benachrichtigung kommt nur an, wenn Resend sie annimmt; sie ist also kein vollständiges Verzeichnis der Registrierungen.

Soll die Instanz nur einem Team dienen, beschränken Sie den Zugang, zum Beispiel mit einem VPN, einer IP-Allowlist oder einer Authentifizierung am Reverse-Proxy; MCP- und REST-Clients brauchen dann denselben Zugang.

Teammitglieder kommen über eine Einladung hinzu: Hinzufügen unter Mitglieder öffnet Benutzer einladen mit den Tabs Link teilen und E-Mails senden. Neue Mitglieder warten mit dem Status Wartet auf Genehmigung, bis ein Mitglied mit Bearbeiten bei Benutzer & Rollen sie öffnet, ihnen eine Rolle gibt und Status auf Aktiv setzt.

Können sich Nutzer mit Google oder Microsoft anmelden?

Ja, sobald Sie es einrichten. Registrieren Sie einen OAuth-Client bei Google oder eine App-Registrierung in Microsoft Entra ID mit der Weiterleitungs-URI <BASE_URL>/api/auth/callback/google beziehungsweise <BASE_URL>/api/auth/callback/microsoft. Der Microsoft-Anbieter nutzt den Endpunkt common; registrieren Sie die App deshalb für Konten in einem beliebigen Organisationsverzeichnis.

Entfernen Sie in .env das # vor den Zeilen der Anbieter, die Sie nutzen, tragen Sie GOOGLE_CLIENT_ID und GOOGLE_CLIENT_SECRET oder AZURE_AD_CLIENT_ID und AZURE_AD_CLIENT_SECRET ein und führen Sie dann docker compose up -d aus. Die Compose-Datei gibt alle vier Werte an die App weiter; listet Ihre docker-compose.yml sie noch nicht auf, holen Sie die Datei erneut mit dem curl-Befehl aus den Installationsschritten. Ein Anbieter erscheint nur, wenn beide Werte gesetzt sind: Die Anmeldeseite zeigt dann Anmelden mit Google oder Anmelden mit Microsoft, die Registrierungsseite Registrieren mit Google oder Registrieren mit Microsoft. Enterprise-SSO über SAML ist nicht implementiert.

Wie verbinde ich einen KI-Client mit meiner Self-Hosted-Instanz?

Der MCP-Endpunkt ist <BASE_URL>/api/v1/mcp, zum Beispiel https://crm.example.com/api/v1/mcp. Es gibt zwei Wege der Authentifizierung:

  • API-Key im Header x-api-key, für Clients auf Ihrem Rechner: Claude Code, Claude Desktop, Codex, Cursor und Gemini CLI. Legen Sie den Key mit Hinzufügen unter API-Schlüssel an; Schnellverbindungen zeigt dort den Einrichtungsausschnitt mit der Adresse, unter der Sie die App geöffnet haben. Der Client muss die URL nur erreichen, eine Adresse im lokalen Netz funktioniert also. Siehe CLI- und Editor-Clients verbinden.
  • OAuth-Anmeldung, für Clients, die sich aus der Cloud ihres Anbieters verbinden, etwa ein benutzerdefinierter Connector in Claude. Diese brauchen eine BASE_URL, die aus dem Internet über HTTPS erreichbar ist. Siehe Claude und ChatGPT verbinden.

Der gehostete In-App-Assistent ist nur in der Cloud verfügbar: Mate gibt es auf Self-Hosted-Instanzen nicht. Externes MCP nutzt Ihren eigenen KI-Anbieter. Enterprise-SSO und White-Labeling sind in keiner Variante implementiert.

Wie werden Adressen geprüft, und wie schalte ich die Adresssuche ein?

Die Adressprüfung braucht keine Einrichtung. Jeder Server prüft Adressen gegen das Adressformat ihres Landes, das die App mitbringt, und füllt Ort und Region aus der Postleitzahl aus, siehe Wie gebe ich eine Adresse ein?. Dafür verlässt nichts aus einem Datensatz Ihren Server.

Die Postleitzahlen stammen von GeoNames. Gibt jemand zum ersten Mal eine Adresse in einem Land ein, lädt ein Hintergrund-Job die öffentliche Datei dieses Landes von https://download.geonames.org/export/zip herunter, prüft sie und legt die Postleitzahlen in Ihrer PostgreSQL-Datenbank ab; nach 90 Tagen lädt er die Datei erneut. Heruntergeladen wird nur die öffentliche Datei: Daten aus Datensätzen, Suchtexte oder IP-Adressen Ihrer Nutzer werden nicht übermittelt. Solange ein Land noch nicht geladen ist oder sein Download fehlschlägt, laufen die Formatprüfungen weiter, und es fehlen nur die Vorschläge für Ort und Region und die Prüfung des Orts. POSTAL_CODE_DATA_URL legt fest, woher die Dateien kommen:

  • Ohne Wert: von GeoNames. Der Server braucht dafür ausgehenden HTTPS-Zugriff auf download.geonames.org.
  • off: keine Downloads. Die Formatprüfungen laufen weiter, aber Ort und Region werden nicht vorgeschlagen, und der Ort wird nicht geprüft.
  • Die URL eines Mirrors, etwa https://mirror.example.com/geonames: ein Server, der die Dateien von GeoNames unter denselben Namen anbietet, etwa DE.zip, US.zip oder CA_full.csv.zip.

Wie bei ADDRESS_LOOKUP_URL weiter unten folgt Customermates keiner Weiterleitung, und ein Hostname, der auf eine private Adresse zeigt, wird nur angenommen, wenn der eingetragene Host selbst privat ist.

Ein selbst gehosteter Server kann Adressen außerdem schon beim Tippen suchen, mit einem Photon-Dienst, den Sie selbst mit Daten von OpenStreetMap betreiben. Setzen Sie ADDRESS_LOOKUP_URL auf diesen Dienst, bekommen Adressfelder Adresse suchen und Adresse prüfen: Eine dort gewählte oder gefundene Adresse wird als bestätigt gespeichert, mit der Position des Gebäudes. Ihr Server schickt Photon nur den Suchtext oder die zu prüfende Adresse, die Sprache der Person und das gewählte Land, Photon sieht also weder IP-Adressen noch Sitzungen. Jeder App-Container erlaubt jeder Person 30 Suchen in 10 Sekunden und beantwortet gleiche Suchen 10 Minuten lang aus seinem eigenen Zwischenspeicher. Ohne ADDRESS_LOOKUP_URL (Standard) geben die Personen die Adresse selbst ein; Customermates Cloud hat keine Adresssuche.

Um Photon zu betreiben, laden Sie das Photon-Jar aus den Releases, speichern Sie es als photon.jar, und laden Sie von download1.graphhopper.com/public einen Suchindex für die ganze Welt oder ein Land, der zur Photon-Version passt. Photon braucht Java 21 oder neuer und Arbeitsspeicher passend zum Index: Für den Index der ganzen Welt, rund 95 GB auf der Festplatte, empfiehlt Photon mindestens 64 GB RAM, ein Länderindex braucht deutlich weniger. Entpacken Sie den Index so, dass photon_data neben dem Jar in /srv/photon liegt, und starten Sie Photon im Netzwerk des Compose-Projekts, das nach seinem Ordner heißt, hier customermates, mit einem Java-Heap (-Xmx), der zu Ihrem Index passt:

docker run -d --name photon --restart unless-stopped \
  --network customermates_default \
  -v /srv/photon:/photon -w /photon \
  eclipse-temurin:21-jre \
  java -Xmx4g -jar photon.jar serve -listen-ip 0.0.0.0

Setzen Sie dann ADDRESS_LOOKUP_URL=http://photon:2322 in .env und führen Sie docker compose up -d aus. Customermates ruft nur den eingetragenen Host auf und folgt keiner Weiterleitung. Ein Hostname, der auf eine private Adresse zeigt, wird nur angenommen, wenn der eingetragene Host selbst privat ist: eine IP-Adresse aus einem privaten Bereich, localhost, ein Name ohne Punkt wie photon oder ein Name unter .local, .internal, .lan oder .home.arpa. Antwortet der Dienst nicht innerhalb von 2 Sekunden, sehen die Personen, dass die Adresssuche nicht verfügbar ist, und können die Adresse weiter selbst eingeben. Entfernen Sie ADDRESS_LOOKUP_URL wieder, ist die Suche aus; gespeicherte Adressen behalten ihre Bestätigung.

Wie spiele ich ein Update der Self-Hosted-Installation ein?

Erstellen Sie zuerst ein Backup, laden Sie dann die neuen Images und erstellen Sie die Container neu:

docker compose pull
docker compose up -d
docker compose ps

Das Tag latest wird bei jeder Änderung am Main-Branch neu gebaut, jedes Pull kann also neue Funktionen und Datenbank-Migrationen bringen. Migrationen laufen bei jedem Start und nur vorwärts: Ein älteres Image nach einer Migration zu starten, ist kein sicherer Rollback. Für einen Rollback spielen Sie das Backup von vor dem Update zurück und starten das vorherige Image über sein sha--Tag. docker compose pull aktualisiert außerdem postgres:16-alpine innerhalb von PostgreSQL 16.

Um selbst zu bestimmen, wann Updates kommen, pinnen Sie das Image in docker-compose.yml auf ein Build-Tag statt latest. Jedes Image trägt zusätzlich das Tag sha- mit der kurzen Commit-ID, zum Beispiel ghcr.io/customermates/customermates:sha-<commit>.

Update von einem Release vor dem 14. Oktober 2026: Das Release, mit dem Listen, Felder und Berechnungen konfigurierbar wurden, wandelt beim ersten Start des neuen Images die CRM-Daten jedes Workspaces um. Datensätze, Werte, Verknüpfungen, Zuweisungen, Seiten der Wissensdatenbank, Mitglieder, Rollen und API-Keys bleiben erhalten. Alle Dashboard-Widgets, die gespeicherten Ansichten und persönlichen Einstellungen der Datensatzlisten, die für die Listen gewählte Terminologie und das Audit-Log werden entfernt, die Historie beginnt also mit dem Update neu, und jeder Webhook wird ausgeschaltet, bis Sie ihn mit den Datensatzereignissen, die er senden soll, wieder einschalten. Erstellen Sie vor diesem Update das Backup und führen Sie die schreibgeschützte Bestandsaufnahme aus den Migrationshinweisen aus, zum Beispiel in docker compose exec postgres sh -c 'psql -U "$POSTGRES_USER" "$POSTGRES_DB"', um die Webhooks aufzulisten, die es ausschaltet. Lehnt die Umwandlung Daten ab, die sie nicht umwandeln kann, ändert sie nichts, die App startet nicht, und ihr Log nennt die Felder und die Anzahl der abgelehnten Werte: Spielen Sie das Backup zurück, starten Sie das vorherige Image, korrigieren Sie diese Datensätze und spielen Sie das Update erneut ein.

In einem Klon des Repositorys erledigt scripts/selfhost-update.sh dieses Update für Sie: Es schreibt ein Backup nach backups/, lädt dann die Images und erstellt die Container neu. Daneben legt scripts/selfhost-setup.sh die .env aus der Vorlage an, prüft die Pflichtwerte und startet die Instanz, scripts/selfhost-restart.sh erstellt den App-Container neu, damit er .env-Änderungen übernimmt, und scripts/selfhost-reset.sh löscht wie docker compose down -v alle Daten, sobald Sie RESET eingeben.

Wie wende ich Änderungen an der .env an?

Führen Sie docker compose up -d aus. Der Befehl erstellt die Container neu, deren Konfiguration sich geändert hat, sodass sie mit den neuen .env-Werten starten. docker compose restart behält die alte Umgebung und übernimmt .env-Änderungen nicht.

Wo finde ich die Logs, und wie suche ich Fehler?

docker compose logs -f app
docker compose logs -f postgres
docker compose ps
docker compose exec app sh

Die App schreibt ihre Logs in die Container-Ausgabe. Hintergrund-Jobs wie Webhook-Zustellungen laufen im App-Container über eine PostgreSQL-gestützte Warteschlange in derselben Datenbank; einen separaten Worker-Dienst gibt es nicht. Das veröffentlichte Image schaltet die Telemetrie von Next.js und Prisma ab. Fehler erscheinen nur in diesen Logs. Jede E-Mail, die Resend abgelehnt oder nicht rechtzeitig beantwortet hat, steht als [EmailService] Email not sent mit dem Grund von Resend im Log.

Wo werden Dateien beim Self-Hosting gespeichert?

Dateifelder speichern ihre Dateien standardmäßig in der PostgreSQL-Datenbank, sodass das Backup mit pg_dump sie enthält. Eine Datei darf höchstens FILE_MAX_UPLOAD_BYTES Bytes groß sein, ohne Angabe 1073741824 (1 GB); darüber hinaus hat ein selbst gehosteter Workspace kein Speicherlimit außer Ihrem Speicherplatz. Für viele oder große Dateien legen Sie sie in einem S3-kompatiblen Bucket ab, etwa bei AWS S3, MinIO, Cloudflare R2 oder im Object Storage von Hetzner oder OVH:

  • FILE_STORAGE_DRIVER=s3 sowie S3_BUCKET, S3_ACCESS_KEY_ID und S3_SECRET_ACCESS_KEY eines privaten Buckets.
  • S3_ENDPOINT für jeden Anbieter außer AWS, zum Beispiel https://fsn1.your-objectstorage.com, und S3_REGION, wenn der Anbieter eine verlangt (ohne Angabe us-east-1).
  • S3_FORCE_PATH_STYLE=true für MinIO und andere Server, die den Bucket im Pfad statt im Hostnamen adressieren.

API-Clients müssen S3-Dateien über 25 MiB an die Upload-URL senden, die /v1/files/uploads zurückgibt. Die Content-Route der App nimmt kleinere S3-Dateien an und überträgt Datenbank-Uploads bis zum konfigurierten Dateilimit als Stream.

Browser senden neue Dateien direkt an den Bucket, deshalb müssen seine CORS-Regeln PUT mit den Headern Content-Type und If-None-Match von Ihrer BASE_URL erlauben. Ein Upload-Link schreibt eine Datei nur einmal: Der Bucket lehnt einen zweiten Upload darüber ab, sodass sich eine Datei nach dem Speichern nicht ersetzen lässt. AWS S3, MinIO und Cloudflare R2 unterstützen das; ein Speicher, der If-None-Match ignoriert, nimmt einen zweiten Upload an, bis der Link nach 15 Minuten abläuft. Downloads laufen immer über die App, die zuerst den Zugriff prüft. Eine Datei bleibt dort, wo sie hochgeladen wurde: Nach einem Wechsel zu s3 bleiben frühere Dateien in der Datenbank und lassen sich weiter öffnen, und die S3-Variablen müssen gesetzt bleiben, solange der Bucket Dateien enthält. Sichern Sie den Bucket zusammen mit der Datenbank. Der tägliche Aufruf von /api/cron/lifecycle löscht Dateien, die vor 30 Tagen aus Datensätzen entfernt wurden, und Uploads, die nie gespeichert wurden, siehe Welche geplanten Aufgaben muss ich ausführen?.

Wie erstelle ich Datenbank-Backups und stelle sie wieder her?

Alle Daten liegen in der PostgreSQL-Datenbank: Datensätze, Nutzer, Einstellungen, das Aktivitätsprotokoll und die Warteschlange der Hintergrund-Jobs, dazu die Dateien der Dateifelder, sofern FILE_STORAGE_DRIVER nicht s3 ist (dann sichern Sie auch den Bucket). Der App-Container speichert keine Daten. Sichern Sie die Datenbank mit pg_dump über den Postgres-Container, dessen Port die Compose-Datei nicht veröffentlicht:

(
  umask 077
  mkdir -p backups && chmod 700 backups
  docker compose exec -T postgres sh -c 'pg_dump -U "$POSTGRES_USER" "$POSTGRES_DB"' \
    | gzip > backups/customermates-$(date +%Y%m%d).sql.gz
)

Führen Sie den Befehl täglich über einen Planer wie cron aus und kopieren Sie die Dateien auf getrennten oder externen Speicher. Bewahren Sie eine Kopie von .env sicher und außerhalb der Versionsverwaltung dazu auf: Eine Wiederherstellung mit einem anderen BETTER_AUTH_SECRET meldet alle ab.

Zur Wiederherstellung stoppen Sie die App, ersetzen die Datenbank durch eine leere, spielen den Dump ein und starten neu:

docker compose stop app
docker compose exec -T postgres sh -c 'dropdb -U "$POSTGRES_USER" "$POSTGRES_DB" && createdb -U "$POSTGRES_USER" "$POSTGRES_DB"'
gunzip -c backups/customermates-20260923.sql.gz \
  | docker compose exec -T postgres sh -c 'psql -v ON_ERROR_STOP=1 -U "$POSTGRES_USER" "$POSTGRES_DB"'
docker compose up -d

Testen Sie die Wiederherstellung regelmäßig auf einem separaten Rechner. Auch der Wechsel auf eine neue PostgreSQL-Hauptversion, die die Compose-Datei auf 16 festlegt, braucht einen Dump und eine Wiederherstellung in die neue Version.

Welche geplanten Aufgaben muss ich ausführen?

Die Cloud ruft zwei Wartungs-Endpunkte nach Zeitplan auf; eine Self-Hosted-Instanz führt sie nur aus, wenn Sie sie aufrufen. Ohne den täglichen Aufruf werden Elemente im Papierkorb nach ihren 30 Tagen nie endgültig gelöscht.

  • /api/cron/lifecycle, einmal täglich: löscht Papierkorb-Elemente, deren 30 Tage abgelaufen sind, Dateien, die vor 30 Tagen aus Datensätzen entfernt wurden, und nie gespeicherte Uploads, und erledigt die übrigen täglichen Wartungsaufgaben. Auf einer Self-Hosted-Instanz überspringt der Aufruf die Cloud-Aufgaben für Testphasen, Abonnements und verbundene Konten.
  • /api/cron/record-deliveries, alle 5 Minuten (optional): startet fällige Webhook-Zustellungen, Ereignisverarbeitung, das Auslesen von Text aus angehängten Dateien und andere Hintergrundarbeit an Datensätzen, die kein laufender Job übernommen hat, etwa nach einem Neustart des Containers.

Beide antworten mit HTTP 401, außer die App hat ein CRON_SECRET und die Anfrage sendet es als Authorization: Bearer <secret>. Die Vorlage enthält neben BETTER_AUTH_SECRET eine auskommentierte Zeile CRON_SECRET, und die Compose-Datei reicht den Wert an die App weiter; listet Ihre docker-compose.yml CRON_SECRET noch nicht auf, holen Sie die Datei erneut mit dem curl-Befehl aus den Installationsschritten. Entfernen Sie das Kommentarzeichen oder ergänzen Sie die Zeile in einer älteren .env, setzen Sie einen langen Zufallswert, etwa aus openssl rand -hex 32, führen Sie docker compose up -d aus und planen Sie die Aufrufe auf dem Host, etwa mit crontab -e:

0 3 * * * curl -fsS -H "Authorization: Bearer YOUR_CRON_SECRET" http://localhost:4000/api/cron/lifecycle
*/5 * * * * curl -fsS -H "Authorization: Bearer YOUR_CRON_SECRET" http://localhost:4000/api/cron/record-deliveries

Setzen Sie statt 4000 Ihren APP_PORT ein. Ein erfolgreicher Aufruf antwortet mit {"ok":true,...}. Scheitert eine seiner Aufgaben, antwortet der Aufruf mit HTTP 500, und das Log der App nennt die gescheiterte Aufgabe; mit curl -fsS meldet cron den Fehler dann.

Wie setze ich alle Daten zurück?

docker compose down -v
docker compose up -d

-v löscht das PostgreSQL-Volume mit allen Workspaces, Nutzern und Datensätzen der Instanz. Das lässt sich nicht rückgängig machen; erstellen Sie vorher ein Backup, falls Sie die Daten noch brauchen könnten. Der nächste Start legt eine leere Datenbank an, und die erste Person, die sich registriert, erstellt einen neuen Workspace.

Weiter