
🌐 Auch auf: English · Français · Español
Zeit für ein Geständnis: Die produktive Nextcloud aus dem Migrations-Kriegstagebuch läuft seit einer Woche auf K3s, das Repo hat seit Monaten ein Playbook nextcloud-update.yml, und ich hatte es noch nie wirklich ausgeführt. Apps wurden auf die altmodische Art aktualisiert, per Klick in der Admin-Oberfläche, oder gar nicht. Heute hat sich das geändert. Der Lauf selbst dauerte 2 Minuten 45 Sekunden. Spannend war alles drumherum.
Ablauf wie immer: Ich entscheide, mein KI-Co-Admin (Claude) prüft, baut und verifiziert. Und das Allererste, was er fand, war kein Bug in der Update-Logik, sondern eine geladene Waffe, gerichtet auf den eigenen Fuß, in Zeile drei.
🧭 Wozu überhaupt ein eigenes Update-Playbook?
Zuerst eine Frage, die ich tatsächlich stellen musste: Wenn ich das Haupt-Playbook laufen lasse, werden dann meine Apps aktualisiert? Antwort: nein, und das ist Absicht. Es gibt zwei voneinander unabhängige Gründe, und das Haupt-Playbook deckt bewusst keinen davon ab.
Grund 1: gleicher Tag, neueres Image
Ein Image-Tag wie nextcloud:34.0.4-fpm ist nur ein Name, der auf ein Image zeigt. Offizielle Images werden unter demselben Namen neu gebaut, sobald darunter etwas gepatcht wird: ein Debian-Sicherheitsfix, ein PHP-Point-Release, OpenSSL. Die Nextcloud-Version bleibt 34.0.4, der Inhalt nicht.
Meine Pods laufen mit imagePullPolicy: IfNotPresent: Liegt ein Image mit diesem Namen schon im lokalen containerd-Store, fragt K3s nie wieder bei der Registry nach. Solange sich also der Tag im Inventory nicht ändert, spielt das Haupt-Playbook identische Manifeste ein, K3s sieht nichts zu tun, und der alte Build läuft bis in alle Ewigkeit weiter. Das Update-Playbook erzwingt den Abgleich mit crictl pull und startet danach neu. Dasselbe gilt für Collabora.
Grund 2: Die Apps stecken nicht im Image
Das ist der gewichtigere Grund. Nextclouds Code-Verzeichnis /var/www/html ist ein HostPath-Volume auf dem Server (/srv/nextcloud-www), und Apps aus dem App Store werden genau dort installiert, direkt neben dem Core. Ein neues Image bringt einen neuen Core und die mitgelieferten Apps. Apps von Drittanbietern wie audioplayer, drawio oder Deck fasst es nie an. Die bewegen sich nur mit occ app:update (oder einem Klick in der Admin-Oberfläche). Das Haupt-Playbook erzwingt nur das App-Layout: diese an, diese aus, Fehlendes installieren. Es gibt einen Opt-in-Schalter (nextcloud_apps_update), Standard false.
Drei Ebenen, drei Hebel
| Was ändert sich? | Beispiel | Hebel |
|---|---|---|
| Nextcloud-Version | 34.0.4 → 34.0.5 oder → 35 | Tag im Inventory ändern, nextcloud-k3s.yml ausführen |
| Neu gebautes Image, gleicher Tag | Sicherheitsfix in PHP oder Debian | nextcloud-update.yml (Pull + Neustart) |
| Apps aus dem App Store | audioplayer 3 → 4 | nextcloud-update.yml (occ app:update --all) |
Warum nicht einfach alles in einen Lauf packen? Weil das Haupt-Playbook idempotent und vorhersehbar sein soll: Es läuft auf einen deklarierten Zustand zu, und ein zweiter Lauf ändert nichts mehr. Updates sind das Gegenteil: Sie holen, was gerade neu im Internet liegt, und tauschen laufenden Code aus. Das verdient einen eigenen Moment, einen eigenen Backup-Check und hinterher einen eigenen Blick in die Logs. Sonst würde jeder kleine Konfigurationslauf nebenbei still und leise Apps aktualisieren, und wenn danach etwas kaputtgeht, wüsstest du nie, ob es deine Konfigurationsänderung war oder das Release von jemand anderem.
🔫 Der Schuss in den Fuß: hosts: nextcloud
Das Play zielt auf die Inventory-Gruppe nextcloud. Die enthält derzeit die Produktivinstanz, einen Testhost und eine zweite Instanz, die mitten in einer Migration steckt: Ihre Datenbank wird bis zum Umzugstag bei jedem Sync überschrieben, und sie ist auf exakt die Version des alten Servers festgenagelt. Einmal --limit vergessen, und das Update rollt über alle drei, startet Pods auf einer Maschine neu, die eigentlich mucksmäuschenstill dasitzen soll.
Der Fix besteht aus vier Zeilen und sorgt dafür, dass das Playbook ohne Limit gar nicht erst startet:
- name: Update | Refuse to run without --limit
ansible.builtin.assert:
that: ansible_limit is defined
fail_msg: "Run with --limit <host>, e.g. --limit cvjm - this playbook restarts Nextcloud."
run_once: trueansible_limit ist eine Magic Variable, die nur existiert, wenn --limit übergeben wurde. Billig, explizit, und aus „Ups“ wird eine Fehlermeldung. Erster Task des heutigen Laufs: „All assertions passed“. 🎉
✅ Pre-Flight: vier Checks, zwei Minuten
- Backup frisch? Der tägliche Pull von der Backup-Kiste war um 12:30 Uhr mit DB-Dump und Daten fertig. Ein Update ohne frischen Wiederherstellungspunkt ist Glücksspiel, keine Wartung.
- Was ändert sich tatsächlich?
occ app:update --all --showonlyist der Trockenlauf. Ergebnis: genau eine App, audioplayer, springt auf 4.0.0. - Gesunde Ausgangslage?
occ status: kein Wartungsmodus, kein ausstehendes DB-Upgrade, alle Pods laufen, 190 GB frei. Dinge, die du vorher wissen willst, damit du hinterher sagen kannst, ob du etwas kaputt gemacht hast oder ob es schon kaputt war. - Wer ist online? Fünf Nutzer in den letzten 15 Minuten aktiv. Zwei Nextcloud-Neustarts und ein Collabora-Neustart heißen: Offene Office-Dokumente verlieren die Verbindung. Notiert, akzeptiert, es ist eine ruhige Dienstagsmittagspause.
⚙️ Anatomie des Laufs
| Schritt | Dauer | Warum er da ist |
|---|---|---|
crictl pull Nextcloud + Collabora | 51 s | Die Pods nutzen imagePullPolicy: IfNotPresent, also zieht K3s einen Tag nie von selbst neu. Hier ist der Tag auf ein exaktes Patch-Release festgenagelt, also hat das vor allem bestätigt, dass das festgenagelte Image immer noch das festgenagelte Image ist. |
| Nextcloud neu starten | 42 s | Übernimmt ein frisches Image, falls es eins gibt. Das Deployment nutzt die Strategie Recreate: Nur ein Pod darf die HostPath-Volumes anfassen, also gibt es eine kurze Lücke statt einer rollierenden Übergabe. |
| Collabora neu starten | 20 s | Gleiche Idee für den Office-Server. |
occ upgrade | 2 s | Der Container-Entrypoint führt das beim Start schon aus; das hier ist das Sicherheitsnetz. |
occ app:update --all | 2 s | audioplayer → 4.0.0. Der App Store bietet nur Releases an, die zur laufenden Major-Version passen, also kann das nichts hereinziehen, was Nextcloud 34 kaputt macht. |
| Nextcloud noch einmal neu starten | 43 s | Der interessante Teil, siehe unten. |
🧠 Warum zweimal neu starten? OPcache und eine Einstellung, die nichts verzeiht
Die PHP-Konfiguration läuft mit opcache.validate_timestamps=0. Das ist eine Performance-Einstellung: PHP kompiliert jede Datei einmal und prüft die Datei auf der Platte für die gesamte Lebensdauer des Prozesses nie wieder. Super für die Geschwindigkeit, aber mit einer fiesen Folge für Updates.
Der erste Neustart passiert, bevor occ irgendetwas schreibt; er ist dazu da, Images zu übernehmen. Dann überschreibt app:update die PHP-Dateien der App auf dem persistenten Volume, aber das laufende PHP-FPM liefert weiter den alten Bytecode aus seinem Cache aus. Das Update sieht erledigt aus, app:list zeigt die neue Versionsnummer, und der alte Code läuft weiter bis zum nächsten Neustart, der aus ganz anderen Gründen kommt. Bei einem Sicherheitsfix ist das die schlimmste Sorte „erledigt“. Daher der zweite Neustart, und zwar nur, wenn sich tatsächlich etwas geändert hat.
Nettes Detail: Das trifft nur aktualisierte Apps. Eine frisch installierte App war nie im Cache, also wird sie frisch kompiliert. Es sind gezielt Updates, die still und leise nicht greifen.
🐛 Bug Nr. 2: ein Task, der immer „changed“ sagte
Der Task occ upgrade meldete changed, obwohl es nichts zu aktualisieren gab. Sein changed_when prüfte auf das Fehlen der Phrase „already at the latest version“, und Nextcloud 34 gibt diese Phrase nicht mehr aus. Bei einem Lauf ohne Änderungen druckt es jetzt nur noch einen Hinweis auf die Upgrade-Doku. Das Fehlen eines Strings, den es nicht mehr gibt, ist immer wahr, also behauptete der Task immer eine Änderung.
# before: true forever on NC 34
changed_when: "'already at the latest version' not in occ_upgrade.stdout"
# after: only a real upgrade prints this
changed_when: "'Update successful' in occ_upgrade.stdout"Heute harmlos, weil das App-Update den zweiten Neustart sowieso ausgelöst hat. Aber ein Playbook, das über Änderungen lügt, erzieht dich dazu, seine Ausgabe zu ignorieren, und eines Tages kommt es genau auf diese Ausgabe an. Lektion: Auf das positive Signal matchen, nicht auf das Fehlen eines alten.
🔍 Post-Flight: Vertrauen ist gut, Kontrolle ist besser
- audioplayer 4.0.0 aktiviert; die Liste der deaktivierten Apps unverändert, also wurde nichts hinter meinem Rücken an- oder ausgeschaltet.
- nextcloud.log seit dem Lauf: null Warnungen, null Fehler. Keine PHP-Fehler im Container.
- Collabora: Discovery antwortet, die Nextcloud-Seite der Verbindung ist intakt.
- notify_push-Selbsttest: alle drei Checks grün. Der Push-Server hat sich nach den Neustarts von selbst wieder verbunden.
- 5xx am Ingress: fünf Requests, alle von Handys und Sync-Clients in den Sekunden, in denen der Pod neu startete. Mit Recreate zu erwarten; die Clients versuchen es automatisch erneut.
🎯 Fazit
Das Update selbst war der unspektakulärste Teil: eine App, unter drei Minuten, saubere Logs. Der Wert lag an den Rändern. Eine Leitplanke, die den gefährlichen Standardfall unmöglich macht, ein Neustart, den es wegen einer einzigen Cache-Einstellung gibt, und ein Statuscheck, der seit dem letzten Major-Release still und leise gelogen hatte. Infrastructure as Code macht Updates nicht magisch. Es macht sie langweilig, und langweilig ist genau das, was du von dem Ding willst, das an die Produktion geht. 🧰




