-
Notifications
You must be signed in to change notification settings - Fork 0
De Install Upgrading
plMail migriert seine eigene Datenbank beim Start. Diese eine Entscheidung prägt alles auf dieser Seite: Sie ist der Grund, warum ein Upgrade aus zwei Befehlen besteht, warum der Image-Tag, den du ziehst, mehr zählt als sonst, warum die Kopfzeile im Administrationsbereich überhaupt eine Build-Nummer zeigt und warum Zurückrollen bedeutet, eine Sicherung einzuspielen, statt den vorigen Tag zu ziehen.
frankenphp/docker-entrypoint.sh wartet auf die Datenbank und führt dann in jedem
Anwendungscontainer app:db:migrate aus — im Webserver und im Worker, und in jedem Container eines
älteren Aufbaus, der einen Prozess pro Container betrieb.
Dieser Befehl ist doctrine:migrations:migrate --all-or-nothing --no-interaction mit einer
Ergänzung: Er hält für den gesamten Lauf einen Postgres-Advisory-Lock.
Der Lock ist keine Zierde. Die Anwendungscontainer starten im Abstand von Millisekunden gegen eine
Datenbank, alle lesen das Migrationsregister, bevor einer von ihnen hineingeschrieben hat, und alle
entscheiden, dass dieselbe Migration aussteht. Ohne den Lock gewinnt einer, die übrigen blockieren
an seiner Tabellensperre, werden bei seinem Commit freigegeben und sterben an einem Schema, das
sich bereits bewegt hat — SQLSTATE[42701]: Duplicate column. Unter set -e sind das Dienste, die
nie starten. Es waren fünf, solange jeder Prozess seinen eigenen Container hatte. Mit dem Lock warten die Verlierer, lesen ein bereits aktuelles
Register und beenden sich, ohne etwas gefunden zu haben.
Daraus folgen drei Dinge, und sie sind der Grund für diese Seite:
- Es gibt kein Zeitfenster, in dem du die Migration vor ihrem Lauf begutachten könntest. Das neue Image zu starten ist ihr Lauf.
- Eine fehlgeschlagene Migration reißt den Container mit, weil der Entrypoint bei einem Fehler anhält. Das ist gewollt: Ein Container, der Anfragen gegen ein halb migriertes Schema bedient, ist schlimmer als ein Container, der gar nicht erst gestartet ist.
-
Das Warten auf den Lock hat eine Frist. Standardmäßig fünf Minuten, überschreibbar mit
--lock-timeout. Danach verweigert der Befehl die Migration, statt anzunehmen, der Halter sei tot — was auch immer den Lock hält, könnte noch mitten in der Migration stecken, und eine zweite Migration obendrauf ist genau das, was der Lock verhindern soll.
Das CHANGELOG nennt die Schemaänderung in jeder Version, die eine hat, ganz oben im Eintrag, zusammen mit der Angabe, ob sie rein additiv ist. Lies das, bevor du ziehst; es ist die einzige Vorschau, die du bekommst.
Der typische Fehlerfall ist ein Stack, in dem ein Container beim Halten des Locks feststeckt. Alles andere wartet fünf Minuten, meldet "timed out waiting for another container to finish running migrations" und beendet sich. Finde den festhängenden Container, bevor du die übrigen neu startest.
Der Release-Workflow veröffentlicht ein Multi-Architektur-Manifest und richtet mehrere Tags darauf:
| Tag | Wem er folgt |
|---|---|
latest |
der jüngsten Veröffentlichung — er bewegt sich nur, wenn ein v*-Tag gepusht wird |
main |
der Spitze des Standard-Branches, veröffentlicht oder nicht |
0.0.16 |
einer Veröffentlichung, festgenagelt. Ohne führendes v: Der Git-Tag heißt v0.0.16, der Image-Tag nicht |
0.0 |
dem neuesten Patch einer Minor-Linie |
sha-1a2b3c |
einem Commit, exakt festgenagelt |
latest folgte früher dem Standard-Branch, wodurch jeder Merge direkt auf den Tag ging, den die
README den Leuten zum Ziehen nennt — einschließlich unfertiger Arbeit und Migrationen, die beim
Start laufen. Genau deswegen wurde das geändert.
Wähle bewusst. latest ist für die meisten Installationen richtig. main ist dafür da, das laufen
zu lassen, woran gerade gearbeitet wird, und das Changelog wird es noch nicht beschreiben. Eine
festgenagelte Version ist richtig, wenn du Upgrades lieber planst als empfängst.
Der typische Fehlerfall ist pull_policy: always auf einem beweglichen Tag.
truenas.compose.yaml setzt das, ein Redeploy von der NAS-App-Seite holt sich also, worauf dieser
Tag inzwischen zeigt — bequem bei latest und überraschend bei main.
docker compose pull
docker compose up -d --waitJeder Anwendungsdienst läuft mit demselben Image, alle werden also neu erzeugt und kommen mit dem
neuen Code hoch. --wait blockiert, bis die Healthchecks bestehen, was in einem Skript etwas wert
ist: Der Healthcheck des php-Containers ist /healthz, und das antwortet erst, wenn PHP die
Datenbank erreichen kann.
Prüfe danach zwei Dinge:
- Die Kopfzeile im Administrationsbereich, um zu sehen, welcher Build tatsächlich läuft.
-
/healthz, um zu sehen, ob die Worker zurückgekommen sind — siehe Fehlersuche.
Solltest du jemals auf einem anderen Weg als einem Container-Neustart migrieren, starte die Worker
danach über Administration → System neu. Ein Worker hält Doctrines Metadaten für seine gesamte
Lebensdauer im Cache, kann nach einer Migration also weiter Spalten abfragen, die es nicht mehr
gibt, bis ihn etwas neu startet. Dieser Knopf schreibt einen Zeitstempel in einen Cache-Pool, den
sich die Container teilen; jeder Messenger-Worker vergleicht ihn in jeder Schleife mit seiner
eigenen Startzeit und beendet sich, und restart: unless-stopped bringt ihn zurück. Der
IMAP-Supervisor ist kein Messenger-Worker und liest dasselbe Signal selbst.
Der typische Fehlerfall ist, einen einzelnen Dienst zu aktualisieren. Alle teilen sich das Image
und alle migrieren; ein Stack, in dem nur php neu erzeugt wurde, hat Worker, die den Code von
gestern gegen das Schema von heute ausführen.
Als Administrator angemeldet trägt die Kopfzeile jeder Administrations-Seite einen Chip mit der Referenz, aus der das Image gebaut wurde, und den ersten sieben Zeichen des Commits. Er steht in der Kopfzeile und nicht in einem Panel, weil die Frage, die er beantwortet, gestellt wird, während man etwas anderes ansieht.
Er stammt aus zwei Build-Argumenten, APP_VERSION und APP_COMMIT, die der Release-Workflow aus
der Metadata-Action und dem Commit-SHA einstempelt. Das Image hat kein .git, das man fragen könnte
— der Quelltext wird hineinkopiert, und die Historie bleibt draußen —, ein Image, das niemand
gestempelt hat, weiß also ehrlicherweise nicht, was es ist. In diesem Fall fällt AppVersion auf
git describe --tags --always --dirty zurück, sofern es aus einem Checkout läuft, und meldet
andernfalls development; dann wird der Chip lieber gar nicht gerendert, als auf jeder Seite
Mobiliar zu zeigen.
Also: Ein Chip bedeutet ein gebautes, getaggtes Image. Kein Chip bedeutet entweder ein lokal
gebautes Image ohne Build-Argumente oder einen Checkout. Beides ist legitim; keines von beidem ist
das, was docker compose pull liefert.
Der typische Fehlerfall ist, dem Tag statt dem Chip zu glauben. Zwei Images können sich beide
main nennen. Der Commit im Chip ist das Einzige, was sie unterscheidet, und er ist der Grund,
warum die Bezeichnung allein nicht genügt.
Admin → Aktualisierungen prüft jede Stunde, ob ein neuerer Build veröffentlicht ist, und sagt es den Administratoren, sobald es einen findet: mit einer Benachrichtigung auf jedem Gerät, das sie für Push angemeldet haben, und mit einem Hinweis neben der Build-Anzeige in der Kopfzeile des Administrationsbereichs.
Welche Builds zählen, entscheidet der Kanal:
| Kanal | Folgt | Richtig für |
|---|---|---|
| Veröffentlichungen | dem Image latest: jeder getaggten Veröffentlichung |
die meisten Installationen |
| Main | dem Image main: jedem Build des Standardzweigs |
wer betreiben will, woran gerade gearbeitet wird |
| Aus | nichts | eine Installation, die von sich aus keine Anfrage senden soll |
Bis ein Administrator einen wählt, ist der Kanal der, aus dem der laufende Build stammt: Ein Image
einer Veröffentlichung folgt den Veröffentlichungen, ein main-Image folgt main, und ein Build ohne
eingestempelte Version prüft nichts.
Eine Prüfung fragt die Image-Registry nach dem neuesten Image des Kanals, also dem, das
docker compose pull holen würde, und liest, aus welchem Commit es gebaut wurde. Dann fragt sie
GitHub, wie dieser Commit zum laufenden steht; erst das unterscheidet neuer von bloß anders: Ein
main-Build, der den Veröffentlichungen folgt, ist der neuesten Veröffentlichung voraus und wird
nicht aufgefordert, zu ihr zurückzugehen. Die Commits dazwischen stehen auf der Seite als das, was
neu ist. Über die Installation wird dabei nichts gesendet außer den Anfragen selbst.
Welches Image die Prüfungen lesen, wird beim Bauen als APP_IMAGE eingestempelt (zum Beispiel
ghcr.io/karatektus/pl_mail), neben APP_VERSION und APP_COMMIT. Ein Fork, der sein eigenes
Image veröffentlicht, prüft also sein eigenes. Ein gesetztes APP_IMAGE in der Umgebung hat Vorrang.
Die Seite installiert noch nichts; das erledigen die Befehle unter Das Upgrade oben.
Der Fehlerfall ist eine Prüfung, die die Registry nicht erreicht. Eine Installation hinter einer Firewall bekommt keine Antwort. Die Seite sagt das und behält, was die letzte beantwortete Prüfung gefunden hat, und der stündliche Befehl zählt es nicht als Fehler. GitHubs anonyme API erlaubt sechzig Anfragen pro Stunde und Adresse, was eine viel genutzte gemeinsame Adresse aufbrauchen kann; die Seite nennt diesen Fall, wenn er eintritt.
Die semantische Suche vergleicht zwei Vektoren mit plmail_embed_distance(). Das ausgelieferte
Datenbank-Image hat keine vector-Erweiterung, also ist diese Funktion eine plpgsql-Schleife:
korrekt, und rund 0,107 ms pro Vergleich bei 1024 Dimensionen. Über die 2.000 Kandidaten, die eine
Suche betrachtet, sind das etwa 0,63 s Rechenzeit pro semantischer Suche.
pgvector ersetzt die Schleife durch eine SIMD-Variante. Gemessen an 74.000 Zeilen mit 1024 Dimensionen, PostgreSQL 18.4:
| 20.000 Zeilen | 74.000 Zeilen | |
|---|---|---|
| plpgsql-Schleife | 2,14 s | 7,70 s |
pgvector <=>
|
1,23 s | 4,51 s |
1,75× — nicht die Größenordnung, die das Wort „SIMD" nahelegt. Die Vektoren liegen als real[],
also zahlt jede Zeile ein Detoast und eine Umwandlung real[] → vector, bevor überhaupt gerechnet
wird, und diese Umwandlung ist der größte Teil der Kosten. Es sind echte 0,63 s → 0,36 s im
semantischen Zweig einer Suche, und es ist nicht das Tausendfache, das ein Index wäre — warum der
Index eine eigene, größere Aufgabe ist, steht in
den KI-Interna.
Das ist optional. Ohne pgvector wird nichts schlechter, und danach muss nichts neu indiziert werden.
Weil dieses Image auf Debian beruht und dieser Stack auf Alpine, und weil das eine gegen das andere
zu tauschen die libc unter einem Datenträger austauscht, auf dem die Datenbank bereits jeden Index
geschrieben hat. Das wurde getestet: Index-Scans liefern falsche Zeilen, bei leerem Protokoll. musl
hinterlegt keine Collation-Version, also ist datcollversion NULL, also greift Postgres' eigene
Prüfung nie — und jeder collation-abhängige Index, 48 in plMails eigenem Schema, davon 24 UNIQUE,
und 84 samt der System-Kataloge, wird
stillschweigend mit dem falschen Vergleicher durchlaufen.
docker/postgres/Dockerfile baut FROM postgres:${POSTGRES_VERSION}-alpine und fügt pgvector
hinzu. Gleiche libc, gleiche Server-Binaries, gleiches Format auf der Platte; das Image ist 1 MB
größer und sonst identisch. Es gibt nichts neu zu bauen und keinen Dump-and-Restore einzuplanen.
Nimm ab jetzt eine Datei in jeden compose-Befehl auf:
docker compose -f compose.yaml -f compose.pgvector.yaml pull
docker compose -f compose.yaml -f compose.pgvector.yaml up -d --waitDer erste Lauf kompiliert pgvector — etwa 30 Sekunden, einmal, danach aus dem Cache. Behalte beide
-f-Flags in jedem späteren Befehl. docker compose up -d allein liest nur compose.yaml und
setzt das Standard-Image zurück; das ist kein Fehler und erzeugt keine Warnung.
Prüfe, ob es gegriffen hat:
docker compose -f compose.yaml -f compose.pgvector.yaml exec database \
psql -U app -d app -c "SELECT extversion FROM pg_extension WHERE extname = 'vector'"Die Migration, die den Funktionskörper umstellt, fragt die Datenbank, ob pgvector verfügbar ist —
und sie fragt einmal, bei dem Start, an dem sie zuerst läuft. Machst du beides in einem Befehl,
findet sie die Erweiterung bereits vor. Aktualisierst du erst und tauschst das Datenbank-Image
nächste Woche, hat sie schon mit „nein" geantwortet, ihre Zeile ins Verzeichnis geschrieben und
schaut nie wieder nach: migrate meldet „already at the latest version", und die Schleife bleibt.
Wenn du an diesem Punkt bist, stelle die Frage neu:
docker compose -f compose.yaml -f compose.pgvector.yaml exec php \
php bin/console doctrine:migrations:execute --down \
'DoctrineMigrations\Version20260901120000' --no-interaction
docker compose -f compose.yaml -f compose.pgvector.yaml exec php \
php bin/console doctrine:migrations:migrate --no-interactionDiese eine Migration zurückzunehmen ist von Bauart sicher: Ihr down() installiert die
plpgsql-Schleife, die ohnehin schon installiert ist. Es wird nichts verworfen und keine Daten
angefasst.
Lass -f compose.pgvector.yaml weg und fahre den Stack hoch. Die Datenbank kehrt auf demselben
Datenträger zum Standard-Image zurück — wieder dieselbe libc, also wieder nichts neu zu bauen — und
plmail_embed_distance() behält den Körper, den sie gerade hat. Eine Funktion, die <=> gegen
einen Server aufruft, der diesen Operator nicht mehr hat, scheitert zur Abfragezeit; nimm die
Migration also in derselben Sitzung zurück:
docker compose exec php php bin/console doctrine:migrations:execute --down \
'DoctrineMigrations\Version20260901120000' --no-interaction
docker compose exec php php bin/console doctrine:migrations:migrate --no-interactionEs gibt keinen Downgrade-Pfad, und der Grund steht im ersten Abschnitt dieser Seite. Die Migrationen liefen beim Start, bis du weißt, dass du die vorige Version willst, hat sich das Schema also bereits bewegt.
Jede Migration in diesem Repository liefert zwar ein down() mit, eine bewusste Umkehrung mit
doctrine:migrations:migrate unter Nennung einer früheren Version ist also möglich. Sie ist
selten das, was du willst: down() verwirft, was up() hinzugefügt hat, also auch die Daten darin,
und der nächste Start eines neueren Images migriert direkt wieder nach oben.
Der unterstützte Weg zurück ist:
- Fahr den Stack herunter.
- Stell die Datenbank aus der Sicherung wieder her, die vor dem Upgrade angefertigt wurde — siehe Sichern und Wiederherstellen.
- Nagle das Image auf den Tag fest, auf dem du warst:
ghcr.io/karatektus/pl_mail:0.0.15, nichtlatest. - Bring es hoch und bestätige, dass der Chip die festgenagelte Version zeigt.
Alles, was zwischen der Sicherung und dem Zurückrollen passiert ist, ist weg — das ist der ehrliche Preis und der Grund, unmittelbar vor einem Upgrade eine Sicherung anzufertigen und nicht bloß nächtlich.
Der typische Fehlerfall ist, den alten Tag zu ziehen und sonst nichts. Der alte Code startet gegen eine Datenbank, die ihm voraus ist: Doctrine meldet Migrationsversionen, von denen es nie gehört hat, und die Anwendung fragt ein Schema ab, das für eine Version geformt ist, die sie nicht ist. Nichts davon lässt sich aus dem Symptom heraus diagnostizieren.
Ein neues Image zu starten heißt, seine Migrationen anzuwenden. Es gibt keinen getrennten Schritt, keine Bestätigung und keinen Probelauf. Sichere vorher; die gesamte Seite Sichern und Wiederherstellen ist die Voraussetzung für diese hier.
latest bewegt sich nur bei Veröffentlichungen, main bei jedem Merge. Wählst du main, um
"aktuell zu bleiben", lässt du dich auf unveröffentlichte Schemaänderungen ein — auf einem Stack,
der automatisch migriert.
Eine fehlgeschlagene Migration ist ein Container, der nicht startet, keine kaputte Anwendung. Das ist so beabsichtigt. Lies die Logs des Containers, der den Lock hielt; die anderen sagen nur, dass sie gewartet haben.
Bei einem ungestempelten Build fehlt der Versions-Chip, er steht nicht auf "unbekannt". Ein Checkout, der nie aus einem Tag gebaut wurde, hat keine Version, und das schlicht zu sagen ist besser als ein Wort, das klingt, als sei etwas schiefgegangen. Lies einen fehlenden Chip nicht als kaputte Installation.
Worker halten Doctrines Metadaten für ihre gesamte Lebensdauer im Cache. Alles, was das Schema ändert, ohne die Container neu zu erzeugen, braucht anschließend Administration → System → Worker neu starten, sonst fragen die Worker weiter nach Spalten, die es nicht mehr gibt.
Über pgvector wird einmal entschieden, bei dem Start, der seine Migration zuerst ausführt. Das Datenbank-Image danach zu tauschen ändert von sich aus nichts — die zwei Befehle, die die Frage neu stellen, stehen unter pgvector einschalten.
-f compose.pgvector.yaml wegzulassen setzt das Datenbank-Image stillschweigend zurück. Compose
warnt nicht, wenn ein Dienst, den es neu erzeugt, aus einer Datei stammt, die du nicht mehr
übergeben hast. Ist plmail_embed_distance() dann noch der pgvector-Körper, scheitert jede
semantische Suche an einem fehlenden Operator.
POSTGRES_VERSION ist nichts, was man während eines Upgrades nebenbei anhebt. Das Image liefert
postgresql-client-18 mit, und pg_dump weigert sich, einen Server zu sichern, der neuer ist als
es selbst — ein über die Client-Version hinaus angehobenes Postgres macht also app:backup in dem
Moment kaputt, in dem du es das nächste Mal brauchst.
This page is generated from docs/de/install/upgrading.md. Edit it there — changes made here are overwritten on the next push to main.
Using plMail
- Accounts and aliases
- Account health
- Filters
- Calendar
- Invitations and events from mail
- Reminders
- Connected calendars
- Sharing and booking
- 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