Fehlersuche
Was /healthz bedeutet, wie du eine hängende von einer leeren Warteschlange unterscheidest, wo die
Logs wirklich liegen, und die Fehler, die hier schon jemandem passiert sind.
/healthz
GET /healthz
{
"status": "ok",
"checks": { "database": true, "queue": true, "workers": true }
}
Unauthentifiziert, weil Docker-Healthchecks und Uptime-Monitore keine Sitzung haben — und deshalb
bewusst vage. Es beantwortet nur das, was ein solcher Aufrufer ohnehin durch einen Versuch mit der
Anwendung herausfinden könnte: keine Zahlen, keine Adressen, keine Kontonamen, keine Version. Die
Zahlen liegen unter /admin, hinter ROLE_ADMIN, und ein Test sichert diese Form ab, sodass eine
Ergänzung, die etwas preisgibt, die Suite scheitern lässt.
| Feld | Bedeutung |
|---|---|
status |
ok mit 200 oder error mit 503 |
database |
ob die Datenbank erreichbar ist. Das Einzige, was den Statuscode bestimmt |
queue |
true, solange über alle Warteschlangen hinweg weniger als 5000 Nachrichten warten, false darüber, null, wenn es sich nicht ermitteln ließ |
workers |
true, wenn jeder Prozess, der jemals einen Heartbeat gemeldet hat, weiter schlägt, false, wenn einer veraltet ist, null, wenn es überhaupt keine Heartbeats gibt |
503 nur, wenn die Datenbank unten ist, denn das ist der eine Fehler, bei dem Ausliefern unmöglich ist. Eine gestaute Warteschlange bleibt absichtlich bei 200: Mail ist verspätet, nicht verloren, und den Container neu zu starten würde nicht helfen — es würde eine funktionierende Instanz aus der Rotation nehmen und die gerade laufende Arbeit obendrein verlieren. Die Schwelle von 5000 ist aus demselben Grund großzügig, aus dem der erste Abgleich eines großen Postfachs legitim Tausende Jobs in die Warteschlange stellt und nicht als Störung gelesen werden darf.
null ist nicht false. Ein Monitor muss "die Warteschlange ist gestaut" von "ich konnte nicht
nachsehen" unterscheiden können, und eine Installation, die noch nie einen Worker laufen hatte, hat
keine Heartbeats — nichts ist fehlgeschlagen, es gibt schlicht noch nichts zu melden.
Der HEALTHCHECK des Images zeigt hierher, mit einer Startphase von 60 Sekunden. Früher fragte er
Caddys Metrics-Port ab, der antwortet, sobald der Webserver lauscht — lange bevor PHP die Datenbank
erreichen kann —, sodass ein Stack mit unerreichbarer Datenbank sich selbst als gesund meldete und
depends_on: service_healthy auf nichts wartete. Die Worker-Dienste haben keinen HTTP-Server und
schalten den Healthcheck vollständig ab; ihre Lebendigkeit erreicht denselben Endpunkt über
Heartbeats.
Der typische Fehlerfall ist, allein auf den Statuscode zu alarmieren. Ein toter Scheduler und
eine stehende Warteschlange antworten beide mit 200. Beobachte das checks-Objekt, nicht nur den
HTTP-Status.
Die Warteschlange
Acht Transports auf dem Doctrine-Transport, abgearbeitet von sieben Prozessen im Worker-Container:
| Warteschlange | Prozess | Was darauf liegt |
|---|---|---|
export |
worker-export |
Alles, was plMail verlässt — Versand, Flag-Pushes, Gmail-Label-Änderungen, Mail aus dem Notifier. Die einzige Warteschlange, auf die jemand wartet |
ingest |
worker-ingest |
Mail abholen, die gerade ankommt, und sonst nichts: Konto- und Postfachabgleiche, Gmail- und Graph-Nachrichten-Batches, Kalenderabgleiche |
ingest_backlog |
worker-backlog |
Ein erster Import: Seiten aus der Vorgeschichte eines Postfachs, die neuesten zuerst. Lang, solange ein großes Konto hereingeholt wird, und so ist es gedacht — nichts, was gerade ankommt, wartet darauf |
enrich_live |
worker-live |
Was auf Mail folgt, die gerade eben in einem Konto angekommen ist, dessen erster Import abgeschlossen ist: Einsortieren durch den Assistenten, Terminerkennung, Insight-Karten, Lesebestätigungen. Eigener Prozess, damit neue Mail nie hinter einem Import steht |
release |
worker-release |
Beendet das Warten für Mail, die für den Assistenten zurückgehalten wird. Fast immer leer; eigener Prozess, damit nie etwas davor steht |
enrich |
worker-enrich |
Dieselbe Arbeit für aktuelle Mail aus einem Import, dazu die Suchindizierung |
enrich_backlog |
worker-enrich |
Alte Mail, die der Assistent nach einem Import einsortiert. Wird nur angefasst, wenn enrich leer ist |
maintenance |
worker-maintenance |
Nachträgliche Verarbeitungen, Regelläufe über vorhandene Mail, "Jetzt ausführen"-Knöpfe im Administrationsbereich, Registrierung von Kalender-Push |
bulk |
worker-bulk |
Mark-Read- und Archiv-Läufe über eine ganze Ansicht. Jemand schaut dabei auf eine Fortschrittsanzeige, deshalb reihen sie sich nicht hinter einer nachträglichen Verarbeitung ein |
async |
worker-maintenance |
Stillgelegt. Wird geleert, damit vor der Aufteilung eingestellte Envelopes noch einen Konsumenten haben; hierher wird nichts mehr geroutet |
Vier Prozesse statt vier Transports in einem Worker, weil ein Worker, der bereits in einem langen Handler steckt, nichts anderes mehr annehmen kann, wie auch immer die Warteschlangen priorisiert sind — weshalb ein Klick auf Senden früher darauf wartete, dass ein Gmail-Batch fertig wurde.
Vom Terminal aus:
docker compose exec php php bin/console messenger:stats
docker compose exec php php bin/console messenger:failed:show
Das Warteschlangen-Panel im Administrationsbereich ist die bessere Sicht, wenn keine Mail mehr ankommt, weil es die zwei Zustände unterscheidet, die eine Zahl nicht unterscheiden kann: Es nennt die Nachrichten, die ein Worker gerade jetzt hält — den Handler, seine Nutzdaten und wie lange er sie schon hält — oberhalb der Liste all dessen, was noch wartet. Eine hängende Warteschlange sieht damit anders aus als eine bloß tiefe. Siehe Administration.
Wiederholungen sind begrenzt und je Warteschlange verschieden: export gibt fünf Versuche über rund
anderthalb Minuten, weil seine Fehler ein Relay sind, das eine Verbindung ablehnt, und weil jemand
zusieht; ingest und maintenance geben fünf Versuche über etwa achteinhalb Minuten, weil ihre
Fehler Ratenbegrenzungen Dritter sind, deren Abklingen Minuten dauert. Was sie erschöpft, landet im
Transport failed, den das Panel im Administrationsbereich wiederholen oder verwerfen kann.
Der typische Fehlerfall ist eine Warteschlange ohne Konsumenten. Nichts meldet einen Fehler —
Nachrichten sammeln sich an, und die Anwendung sieht gesund aus. Das ist schon passiert: Ein
Entwicklungs-Stack, dessen Override noch den Dienst messenger-worker von vor der Aufteilung
definierte, ließ einen Worker laufen, der nur die stillgelegte Warteschlange async konsumierte —
Versand und Abgleiche stauten sich, ohne dass irgendwo ein Fehler auftauchte.
Der Scheduler
Die wiederkehrenden Jobs stehen in App\Infrastructure\Scheduler\MaintenanceSchedule und werden von
einem Container ausgelöst, der scheduler_default konsumiert. Ohne diesen Container wird keiner
von ihnen ausgelöst.
| Takt | Befehl |
|---|---|
| jede Minute | app:mail:wake-snoozed |
| jede Minute | app:calendar:alerts |
*/15 |
app:mail:sync |
7-59/15 |
app:calendar:sync --stale |
11-59/15 |
app:contacts:sync --stale |
| stündlich um :20 | app:calendar:push |
| täglich 03:50 | app:calendar:materialise |
| täglich 04:00 | app:push:renew --repair |
| täglich 04:30 | app:monitoring:prune |
| stündlich um :37 | app:updates:check |
| sonntags 05:00 | app:prune:blobs |
docker compose exec php php bin/console debug:scheduler
Der Zeitplan ist zustandsbehaftet, ein Worker, der über einen geplanten Lauf hinweg unten war, holt ihn also nach, statt den Tag zu überspringen — und nachgeholt wird nur der zuletzt versäumte Lauf, weil das alles idempotente Durchläufe sind und den Rückstand von gestern fünfmal abzuarbeiten Verschwendung wäre. Die Zeiten sind über die frühen Morgenstunden verteilt statt auf Mitternacht gestapelt, weil sie sich einen Worker teilen.
Der typische Fehlerfall ist die Symptomliste, nicht ein Fehler. Kein Mail-Polling, keine zurückgestellte Konversation, die je aufwacht, kein Kalenderabgleich, keine Erinnerungen, kein Aufräumen der Protokolle, kein Aufräumen der Blobs — alles auf einmal und alles lautlos. In diesem Zustand war das Projekt, bevor es den Scheduler gab, mit unbegrenzt wachsenden Logs und verwaisten Blobs. Wenn mehrere zusammenhanglose Dinge "nicht mehr funktionieren", prüfe zuerst, ob es diesen Container gibt.
Worker und Heartbeats
Lebendigkeit wird über Heartbeats bestimmt und nicht über Prozesse, weil der Web-Container nicht in
die anderen hineinsehen kann. Jeder dauerhaft laufende Prozess schreibt eine Zeile, geschlüsselt
über APP_CONTAINER_NAME, und wann etwas als veraltet gilt, hängt vom Typ ab:
| Prozesstyp | Veraltet nach |
|---|---|
imap-idle |
2100s — knapp über der 29-minütigen IDLE-Erneuerung |
imap-supervise |
300s |
messenger-worker |
120s — der Listener schlägt alle 30s |
| alles andere | 600s |
Eine Zeile, die das Vierfache ihrer Schwelle überschritten hat, wird ganz entfernt — weit genug gefasst, dass ein kurzzeitig verklemmter Prozess auf dem Dashboard rot erscheint, bevor er verschwindet.
Worker beenden sich bei --time-limit=3600 und werden von Compose neu gestartet, eine Heartbeat-
Lücke von ein, zwei Sekunden um die volle Stunde ist also normal. Administration → System →
Worker neu starten bittet sie alle, sich am Ende ihrer aktuellen Nachricht zu beenden; die
Anwendung hat keinen Docker-Socket und braucht auch keinen, denn bei restart: unless-stopped
ist Beenden gleich Neustarten.
Der typische Fehlerfall ist ein nicht gesetztes APP_CONTAINER_NAME. Der Schlüssel fällt auf
den Hostnamen zurück, der sich bei jedem Neuerzeugen eines Containers ändert, sodass sich das
Dashboard mit toten Workern füllt, die nie wirklich tot waren.
Wo die Logs liegen
Containerausgabe. In prod schreibt Monolog JSON nach php://stderr, alles steht also in
docker compose logs:
docker compose logs -f php
docker compose logs -f worker
Alles, was nicht der Webserver ist, protokolliert über den Worker: die Warteschlangen-Consumer, der
Scheduler, der IMAP-Supervisor und der Hub. Jeder JSON-Eintrag nennt den Prozess, von dem er stammt,
ein Filter auf worker-ingest oder scheduler findet also genau einen davon.
Der Haupt-Handler ist fingers_crossed auf error mit einem Puffer von 50 Nachrichten, das heißt,
gewöhnliche Info-Zeilen werden verworfen — bis etwas fehlschlägt, und dann werden die 50
vorangegangenen Nachrichten mit ausgegeben. Deshalb kommt ein Fehler im Log meist mit seinem eigenen
Kontext an, und deshalb findest du keine stille Spur einer Operation, die geklappt hat.
Deprecations gehen auf einem eigenen Kanal nach stderr. In dev geht dieselbe Ausgabe nach
var/log/dev.log.
Der Protokollbrowser im Administrationsbereich. Ein zweiter Handler schreibt in die Datenbank,
und Administration → Protokolle durchsucht sie: 100 Einträge pro Seite, filterbar nach
Mindeststufe von Info bis Critical, mit dem Containernamen in jeder Zeile. Die niedrigste
aufbewahrte Stufe ist APP_DB_LOG_LEVEL, standardmäßig warning. Alles ab Warnstufe, das noch
niemand gelesen hat, umrandet das Benutzermenü — bernsteinfarben oder rot — auf jeder Seite, und
zwar nur für Administratoren. Den Browser zu öffnen ist das, was sie als gesehen markiert.
app:monitoring:prune bewahrt standardmäßig 14 Tage Protokolleinträge und 30 Tage Heartbeats auf,
nächtlich. Außerdem entfernt es fertige Hintergrundjobs nach 30 Tagen (--job-days=N) und vertraute
Geräte, sobald sie abgelaufen sind.
Der typische Fehlerfall ist, in der Produktion nach einer Spur auf Info-Ebene zu suchen.
fingers_crossed bedeutet, dass sie nie geschrieben wurde, sofern kein Fehler folgte. Senke
APP_DB_LOG_LEVEL vorübergehend auf info, wenn die Datenbankseite mehr aufbewahren soll, und setz
es zurück — auf debug wächst die Tabelle schnell.
Ein bestimmtes Konto untersuchen
docker compose exec php php bin/console app:mail:test-connection # IMAP/SMTP probe
docker compose exec php php bin/console app:imap:test --account=ID # connection and folder listing
docker compose exec php php bin/console app:graph:diagnose # what Graph actually permits
docker compose exec php php bin/console app:mail:sync ACCOUNT_ID # dispatch a sync by hand
Speziell für Gmail-Push zeigt das Panel Gmail-Webhooks im Administrationsbereich das Topic, den exakten Endpunkt, den Pub/Sub aufrufen muss, und warum eine Benachrichtigung abgewiesen wurde.
Der typische Fehlerfall ist, aus dem falschen Container heraus zu testen. Über
docker compose exec php sind diese Befehle in Ordnung, denn das lädt die erzeugten Geheimnisse
über Composers Autoload-Dateien. Ein blankes docker run gegen das Image ist es nicht — es hat kein
Secrets-Volume, hält also einen anderen Verschlüsselungsschlüssel und kann keine einzige
gespeicherte Zugangsberechtigung lesen.
Fehler, die tatsächlich vorgekommen sind
| Symptom | Ursache |
|---|---|
| Anhang-Downloads enden mit 404, Blob-Daten verschwinden, wenn ein Container neu erzeugt wird | Die Blob-Verzeichnisse liegen auf keinem gemeinsamen Volume. Siehe Docker Compose |
Drei von sechs Containern starten nie, in den Logs steht column … already exists |
Gleichzeitige Migrationen beim Start. Behoben durch den Advisory-Lock in app:db:migrate; wenn du das heute siehst, führt irgendetwas doctrine:migrations:migrate direkt aus |
| Ein Container verweigert den Start: "APP_ENCRYPTION_KEY cannot decrypt the credentials already stored" | Ein Dienst ohne den Mount app_secrets, oder ein Schlüssel, der sich unter laufendem Stack geändert hat |
| Der Entrypoint bricht ab, bevor irgendetwas startet, nach einer Meldung über ACLs | Ein altes Image: ein Fehlschlag von setfacl auf ZFS, NFS oder einer Docker-Desktop-Freigabe unter set -e. Seit 0.3.0 setzt der Entrypoint keine ACLs mehr |
| Jeder Start dauert eine Minute oder länger, bevor der Webserver antwortet, und das Log schweigt nach "Statement statistics are enabled" | Ein Image älter als 0.3.0, das als root auf langsamen Platten läuft: Der Entrypoint setzte bei jedem Start ACLs über var/cache, was jede Datei des Caches in die Schicht des Containers kopiert. Aktualisiere. Weitere dreißig Sekunden gingen mit dem Warten auf die erste Zustandsprüfung der Datenbank verloren — wenn du eine eigene Compose-Datei pflegst, gib der Zustandsprüfung von database ein interval: 5s |
| Ein Stack meldet sich als gesund, während die Datenbank unerreichbar ist | Der alte Healthcheck fragte Caddys Metrics-Port ab. /healthz hat ihn abgelöst |
| Alles funktioniert, aber nichts aktualisiert sich je von selbst | MERCURE_PUBLIC_URL zeigt dorthin, wo der Browser nicht hinkommt. Siehe Hinter einem Reverse-Proxy |
| Gmail-Push liefert für jede Benachrichtigung 403 | GMAIL_PUBSUB_VERIFICATION_TOKEN nicht gesetzt oder passt nicht zum ?token= am Pub/Sub-Abonnement. Es scheitert nach der sicheren Seite |
| Kalender-Push registriert sich nie, Warnung bei der Registrierung | APP_PUBLIC_URL ohne HTTPS, oder ein Loopback-Host, oder ein Google-Cloud-Projekt, dessen Domain-Verifizierung noch aussteht |
| Überhaupt keine Erinnerungen, für niemanden, jede Minute aufs Neue | Ein Konto, dessen Anzeigeadresse ein IMAP-Benutzername statt einer Adresse war, warf beim Bau der Erinnerungsmail eine Exception, beendete damit den ganzen Durchlauf und verlor die in diesem Stapel bereits beanspruchten Erinnerungen. An beiden Enden behoben — die Nachricht wird innerhalb des eigenen try des Kanals gebaut, und der Deliverer überlebt einen werfenden Kanal ohnehin |
| Versand und Abgleiche stauen sich und nichts arbeitet sie ab, nirgends ein Fehler | Ein Worker, der eine Warteschlange konsumiert, in die nichts geroutet wird. Prüfe messenger:stats gegen die vier Transports oben |
Der typische Fehlerfall, der fast der ganzen Tabelle gemeinsam ist, ist Stille. Nahezu keiner davon erzeugt einen Fehler auf einer Seite; sie erzeugen eine Abwesenheit. Genau deshalb meldet das Panel im Administrationsbereich Warteschlangentiefe und Heartbeats und nicht nur Fehler.
Fallstricke
Ein 200 von /healthz bedeutet "weiter ausliefern", nicht "alles funktioniert". Nur die
Datenbank bestimmt den Statuscode. Hinter einem 200 kann jeder Worker tot sein — absichtlich, weil
die Anwendung bereits abgeglichene Mail weiterhin ausliefert.
workers: null auf einer frischen Installation ist richtig. Es hat noch nichts geschlagen. Der
Wert wird erst aussagekräftig, nachdem die Worker einmal gelaufen sind.
Die Warteschlangenschwelle liegt aus gutem Grund bei 5000. Sie zu senken, um Probleme früher zu bemerken, führt dazu, dass der erste Abgleich eines großen Postfachs sich selbst als Störung meldet und ein Orchestrator einen gesunden Container neu startet.
Ein fehlender scheduler-Container ist in jedem Panel unsichtbar, das Fehler zeigt. Nichts
schlägt fehl; die Dinge passieren bloß nie. debug:scheduler und die Heartbeat-Liste sind die
Stellen, an denen es sichtbar wird.
app:reset --full ist kein Schritt der Fehlersuche. Es geht zurück in den Zustand vor dem
ersten Start: jede Tabelle, jeder Benutzer, die gespeicherten Dateien. Es existiert, damit eine
Installation, deren unlesbare Daten wirklich entbehrlich sind, neu anfangen kann, und die Prüfung
des Verschlüsselungsschlüssels lässt es bewusst zu — siehe
CONTRIBUTING.
Logs aus docker compose logs php sind nur die des Web-Containers. Die interessante Zeile steht
meist in dem Worker-Prozess, dem die Warteschlange gehört, auf der die Arbeit lag, und die
protokollieren alle über docker compose logs worker.
This page is generated from docs/de/install/troubleshooting.md. Edit it there — changes made here are overwritten on the next push to main.
plMail
Using plMail
- Accounts and aliases
- Account health
- Templates
- Filters
- Calendar
- Invitations and events from mail
- Reminders
- Connected calendars
- Sharing and booking
- Contacts
- Files and integrations
- Security
- Other clients
- Appearance
- Administration
Installing and running
- Docker Compose
- Platform notes
- Behind a reverse proxy
- Configuration reference
- Backup and restore
- Configuration backup
- Upgrading
- Demo mode
- Troubleshooting
Providers
How it works