
🌐 Aussi en: English · Deutsch · Español
Aujourd’hui, j’ai déménagé un Nextcloud de production depuis une douillette installation native sur bare metal vers une rutilante pile K3s/conteneurs. Sur le papier : récupérer une sauvegarde, la restaurer, basculer le DNS, terminé. En pratique : une série de petites mines, chacune invisible jusqu’à ce qu’on marche dessus. Voici le journal de campagne. 🪖
Étape 1 : pelleter les données 🚚
Le gros du travail, c’était un rsync d’environ 360 Go de données utilisateurs plus un dump SQL d’environ 330 Mo, de la machine de sauvegarde vers le nouveau nœud. Comme j’avais pré-synchronisé plus tôt dans la journée, le dernier passage n’a transféré que le delta – au final, tout s’est résumé à une copie incrémentale rapide. Leçon vieille comme le monde : pré-remplir d’abord, et la synchro de bascule devient minuscule.
Étape 2 : le parcours du combattant des versions majeures 🎮
La sauvegarde était en Nextcloud 32.x. La pile cible tourne en 34.x. Nextcloud a une règle d’airain : on ne peut pas sauter de version majeure. Donc on marche, une étape à la fois :
32.0.14 → 33.0.9 → 34.0.4 (occ upgrade at every step)La subtilité côté conteneurs : le code vit dans un volume persistant, et Nextcloud refuse de faire tourner un code plus ancien sur une version de données plus récente (« downgrade not supported »). Restaurer une base v32 sur un pod qui avait encore le code v34 = refus immédiat. La solution est délicieusement bricolée : épingler l’image sur la version correspondante, bidouiller le version.php du volume de code pour que l’entrypoint redépose le bon code, et laisser chaque occ upgrade aller au bout (sa readiness probe bloque jusqu’à la fin de la migration de la base – patience de rigueur).
Les mines invisibles 💣
1. Le socket antivirus maudit
Après la restauration, l’application Fichiers renvoyait un « Internal Server Error » tout nu juste après la connexion – et le log restait vide, parce que le plantage se produisait en dessous du logger de Nextcloud lui-même. Cause : la configuration restaurée avait réactivé files_antivirus en mode socket, pointant vers un socket du démon ClamAV qui existe sur l’ancienne machine native mais pas dans le conteneur. Correctif : désactiver l’application (cette pile fait plutôt des scans par lots la nuit). Diagnostiqué uniquement en remarquant le problème, pas grâce à une ligne de log. Sournois.
2. La prison Let’s Encrypt 🔒
Le DNS ne pointait pas encore vers la nouvelle machine, donc cert-manager avait passé des jours à échouer à émettre des certificats. Quand j’ai enfin basculé le DNS, les certificats ne venaient toujours pas. Pourquoi ? Après suffisamment d’échecs, cert-manager gare le Certificate dans un back-off exponentiel stocké dans le statut de l’objet – prochaine tentative dans environ 24 heures. Redémarrer le contrôleur ? Ignoré. Supprimer le secret ? Ignoré. Ce qui marche vraiment : supprimer l’objet Certificate et laisser l’ingress-shim le recréer à neuf, sans historique d’échecs → émission immédiate.
Piège bonus : la vérification HTTP-01 de Let’s Encrypt préfère IPv6. Si vous ne déplacez que l’enregistrement A et oubliez le AAAA, la validation continue de tomber sur l’ancien hôte et d’échouer, alors que tout « semble » basculé. Déplacez les deux. Toujours les deux.
L’événement principal : la case qui mentait ☑️
C’est celle-ci qui m’a fait le plus me gratter la tête. Collabora (Nextcloud Office) refusait d’ouvrir les documents : « Nextcloud Office n’a pas pu être chargé. » Pendant ce temps, le test de connectivité Office du panneau d’administration était rassurant et bien vert : « Collabora Online server is reachable. » Côté serveur, tout était en ordre – discovery 200, capabilities 200, certificats valides, URL WOPI correcte, en-têtes WebSocket présents, à l’identique d’un hôte de référence qui fonctionne.
La différence s’est avérée être un seul réglage hérité de l’ancien serveur : « Désactiver la vérification des certificats (non sécurisé) » était coché (disable_certificate_verification=yes dans richdocuments). Sur l’ancienne machine, ça se justifiait – certificats auto-signés. Mais maintenant, avec des certificats Let’s Encrypt valides, ce mode « non sécurisé » casse la session de document WOPI dans richdocuments 11.x – tout en laissant le test de connectivité de l’administration parfaitement vert, parce que ce test emprunte un autre chemin de code que le véritable handshake du document.
Pourquoi je pense que c’était le coupable ? Parce que le correctif était exactement celui-là : décocher la case (supprimer la clé), réessayer, et les documents se sont ouverts immédiatement. Ma théorie : le chemin sans vérification et l’échange de jetons WOPI ne s’entendent plus dès qu’un vrai TLS entre en jeu, et l’échec n’apparaît que dans la session de l’éditeur – le grand classique « le health check est vert mais la fonctionnalité est morte » qui garde les sysadmins humbles. Deux autres clés héritées de la prod (public_wopi_url, doc_format) sont parties avec, pour coller à la configuration minimale de l’hôte de référence.
La fausse piste : « pourquoi tant d’applications sont-elles désactivées ? » 🎣
Après la mise à jour, une pile d’applications apparaissait comme désactivée (tableau de bord, commentaires, stockage externe, …) et je me préparais à un marathon de réconciliation. Puis j’ai comparé l’ensemble actuel des applications avec l’état de l’ancien serveur (extrait du dump de la base d’avant la mise à jour). Rebondissement : elles étaient aussi désactivées sur l’ancienne machine. La nouvelle instance correspondait déjà à la production. La seule vraie victime était metadata, qui n’a tout simplement aucune version compatible avec Nextcloud 34. Parfois, le diff effrayant, c’est juste… la vérité.
Natif ou conteneurs : le moment de vérité ⚖️
La nouvelle instance paraît un poil plus lente que l’ancienne native. La RAM et le CPU s’ennuient (beaucoup de marge), et les temps de réponse côté serveur sont de 50 à 210 ms – parfaitement sains. La différence est architecturale : les conteneurs ajoutent des sauts (proxy d’ingress, réseau overlay, PHP-FPM↔nginx via le réseau des pods, base de données à travers la frontière pod/hôte). Par requête, ce sont des millisecondes ; sur les nombreuses requêtes que déclenche une page Nextcloud, ça finit par donner « un peu moins vif ». C’est la taxe à payer pour la reproductibilité, l’isolation et des mises à jour sans douleur – et pour une petite organisation, c’est un échange qui en vaut la peine.
Le grand ménage de la base de données 🗃️
Restaurer une base de données depuis un ancien serveur, c’est traîner toute son histoire avec. L’index du filecache transportait les chemins de fichiers de l’ancien serveur sous des ID de stockage périmés – plus un paquet OnlyOffice supprimé depuis longtemps, dont les 276k lignes d’index avaient survécu à l’application. Résultat net : une table oc_filecache de 1 053 373 lignes, dont environ 93 % de poids mort, et une base de 1 Go.
Le nettoyage, fait avec soin, sauvegarde et valeurs de contrôle à l’appui (les vrais fichiers utilisateurs et le nombre d’utilisateurs ne doivent pas changer) : supprimer les 7 tables OnlyOffice orphelines, supprimer les deux stockages périmés (l’ancien chemin natif et un mapping de conteneur antérieur) après avoir vérifié qu’ils ne contenaient aucun vrai fichier – uniquement des données d’applications régénérables –, puis OPTIMIZE pour rendre les pages libérées au système de fichiers. Résultat :
oc_filecache rows: 1,053,373 → 72,580
database size: 1 GB → 282 MB (-72%)
real user files: 56,183 → 56,183 (unchanged ✔)
users: 151 → 151 (unchanged ✔)Et sur le disque : 27 Go de miniatures d’aperçu orphelines, dont l’index venait d’être supprimé. Les aperçus sont un pur cache régénérable – supprimer l’arborescence orpheline récupère l’espace, et Nextcloud reconstruit les miniatures à la demande. (La leçon d’une répétition générale précédente : ne jamais réindexer des aperçus orphelins – ça ne fait que repelleter environ 500k lignes mortes dans la table qu’on vient de nettoyer. Supprimez-les plutôt.)
Gremlin bonus : les graphiques réseau vides 📉
node_exporter remontait joyeusement le CPU, la RAM et le disque à Grafana – mais chaque panneau Network Traffic restait obstinément vide. La métrique node_network_receive_bytes_total n’existait tout simplement pas. L’indice se trouvait dans node_scrape_collector_success{collector="netdev"} 0 et dans cette ligne de log :
netdev: "couldn't get netstats: socket: address family not supported by protocol"Les versions récentes de node_exporter lisent les statistiques des cartes réseau via un socket netlink – et le durcissement systemd du service contenait RestrictAddressFamilies=AF_INET AF_INET6, qui interdit discrètement AF_NETLINK. Le collecteur échoue en silence, la métrique n’apparaît jamais, le graphique reste vide. Ajoutez AF_NETLINK à la liste autorisée, reload, restart – et les courbes de trafic prennent vie. (Déjà-vu bonus : exactement le même oubli d’AF_NETLINK avait déjà mordu le durcissement de MariaDB plus tôt dans ce projet. Le sandboxing donne la sécurité et reprend le netlink.)
Regarder la machine réfléchir 📊

Deux opérations de ménage liées ont complété le tout. D’abord, j’ai supprimé les 27 Go de miniatures d’aperçu orphelines (leur index avait déjà disparu avec le nettoyage de la base ci-dessus) – les aperçus sont un pur cache, donc aucun dégât. Ensuite, plutôt que de faire payer à chaque premier testeur la taxe « miniature en cours de génération, veuillez patienter », j’ai lancé une régénération complète avec occ preview:generate-all. Ce job a mouliné toute la nuit, en avalant environ 35 000 images, PDF et vidéos – le tableau de bord ci-dessus le montre en pleine action, et le scanner ci-dessous est ce même passage qui reconstruit les aperçus fichier par fichier.

Ce qui m’amène à un petit aveu : je n’avais jamais eu de tableau de bord comme celui-ci. Voir le CPU, la charge, la mémoire et – maintenant que le gremlin netlink est réglé – le débit réseau en temps réel est étrangement satisfaisant. Je suis sincèrement curieux de voir à quoi ça ressemblera au quotidien : aurai-je de jolis pics bien nets quand quelqu’un téléversera une séance photo entière, ou qu’une équipe téléchargera toute une galerie ? Je ne sais pas encore – et c’est la moitié du plaisir. Reposez-moi la question dans une semaine, quand j’aurai assez fixé ces graphiques pour avoir des opinions sur des percentiles dont j’ignorais tout. 😄
Post-scriptum : des probes qui redémarrent vraiment un pod bloqué 🩺
Un jour plus tard, je suis revenu corriger un point qui me chiffonnait. Les pods avaient des readiness probes – Kubernetes savait quand ne pas envoyer de trafic –, mais leur test de liveness était un kill -0 1 édenté, qui ne remarque quelque chose qu’une fois le PID 1 déjà mort (et à ce stade, le conteneur a de toute façon disparu). Un processus qui tourne encore mais qui est bloqué – FPM coincé, qui n’accepte plus rien sur son socket – resterait simplement là, en panne, jusqu’à ce qu’un humain s’en aperçoive. Sur un pod à une seule réplique, c’est précisément le cas qu’on veut voir traité automatiquement.
La solution est une vraie configuration à trois probes sur chaque conteneur : une startupProbe qui retient la liveness et la readiness jusqu’à ce que l’application écoute vraiment (jusqu’à environ 10 minutes de grâce, pour qu’un occ upgrade lent ou une rafale d’aperçus au démarrage ne déclenche jamais une boucle de redémarrages), une readinessProbe qui régule le trafic, et une livenessProbe serrée – une vraie connexion tcpSocket, pas un test de PID – qui ne redémarre le conteneur que lorsqu’il cesse réellement de répondre. La barrière de démarrage est la pièce maîtresse : sans elle, une liveness probe assez stricte pour détecter un blocage exécuterait aussi un conteneur qui démarre simplement lentement. Le blog tourne sur la même pile, il a donc reçu exactement le même traitement, par souci de cohérence.
Post-scriptum : réglages au plus près du métal 🎛️
Une fois la poussière retombée, la partie amusante a commencé : observer les nouveaux tableaux de bord Grafana et régler en fonction de ce qu’ils montraient réellement plutôt que de ce que j’avais deviné. La machine s’est révélée avoir bien plus de marge que prévu – 8 cœurs, 23 Go de RAM, environ 16 Go libres au repos –, alors j’ai arrêté d’être radin. Collabora est passé de 4 à 6 processus enfants pré-forkés, et ses limites de 3 à 6 Go et de 2 à 4 cœurs, ce qui lisse le petit hoquet de démarrage à froid quand quelqu’un ouvre le premier document après une période d’inactivité. Quelques autres finitions sont arrivées en même temps : des systèmes de fichiers racine en lecture seule pour les sidecars, un niveau de log plus raisonnable (fatal-only masque justement les avertissements qu’on veut voir), et node_exporter a appris à ne plus remonter trois douzaines d’interfaces virtuelles de conteneurs, pour que le graphique réseau montre la seule carte réseau qui compte. Tout cela vit dans le même dépôt Ansible – reproductible, versionné, et ennuyeux dans le meilleur sens du terme.
Enfiler le chapeau noir 🕵️
Avant de déclarer le travail terminé, j’ai fait ce que tout admin paranoïaque devrait faire : j’ai attaqué ma propre instance depuis l’extérieur. Pas de scans de ports – à part les ports web et un port SSH à clé uniquement, le pare-feu rejette tout en silence, il n’y a donc pas grand-chose à trouver –, juste les requêtes qu’un vrai opportuniste envoie sur un Nextcloud tout neuf, à la recherche d’un point faible. En bref : il n’y en avait pas.
Chaque chemin sensible a répondu par un 403 sec – config/, data/, .htaccess, .git/, 3rdparty/, tout le lot. WebDAV et remote.php exigent une authentification (401) avant de dire quoi que ce soit. L’en-tête Server est un laconique nginx sans version, pas de X-Powered-By, aucune empreinte PHP à confronter à une liste de CVE. Le TLS est en 1.3 avec un certificat Let’s Encrypt valide ; 1.0 et 1.1 sont refusés d’emblée. L’ensemble complet des en-têtes de sécurité est en place – HSTS avec preload, une CSP à base de nonce, nosniff, des politiques frame et referrer, une permissions policy verrouillée, et des cookies __Host-. Le contrôle d’intégrité du code de Nextcloud lui-même signale zéro fichier modifié.
La partie qui me tient le plus à cœur : la limitation contre la force brute est active, et – parce que la chaîne de reverse proxies transmet correctement la vraie IP du client – elle freine l’attaquant, pas mon propre ingress. C’est le mode de défaillance qui neutralise discrètement le rate limiting sur bien des installations derrière un proxy, donc ça vaut la peine de le vérifier plutôt que de le supposer. Rien de tout cela n’est exotique ; ce sont surtout les bons réglages par défaut, ennuyeux, plus quelques touches délibérées. C’est tout l’intérêt – la chose la plus décourageante qu’un attaquant puisse trouver, c’est un mur sans joints. 🧱
Jour trois : boucler les derniers détails 🧹
Le marathon des aperçus évoqué plus haut s’est enfin terminé à l’aube du troisième jour, après bien plus d’une journée à mâcher toute la bibliothèque. Comme plus rien n’écrivait dans l’index des fichiers, le ménage différé a pu tourner – et une vraie vérification de bout en bout ensuite a mis au jour deux gremlins qui se cachaient juste sous notre nez.
Les tables « vides » qui ne l’étaient pas
Mes propres notes disaient que les tables restantes d’applications disparues depuis longtemps (Talk, Polls, Collectives, Maps) étaient vides et prêtes à être supprimées. Avant de supprimer quoi que ce soit, j’ai quand même compté les lignes. Elles n’étaient pas vides : quelques dizaines de pages de wiki, quelques sondages avec des votes, plusieurs dizaines de salons de discussion. Personne ne peut les voir en ce moment, parce que les applications ne sont pas installées – mais réinstallez une application et elle récupère aussitôt ses anciennes tables. Donc elles restent. Ce qui est parti : les derniers restes d’OnlyOffice sur le disque (archivés d’abord, puis supprimés), plus un OPTIMIZE complet qui a ramené la fragmentation de 59 Mo à 2 Mo en 17 secondes. Leçon : faites confiance au nombre de lignes, pas à votre propre résumé de la veille.
Push plutôt que poll : notify_push 📡
Les clients de synchronisation de Nextcloud demandent normalement au serveur toutes les 30 secondes environ : « Du nouveau ? » L’application notify_push (Client Push) inverse la logique – un petit démon Rust garde un WebSocket ouvert, et le serveur annonce les changements à l’instant où ils se produisent. (Rien à voir avec les notifications sur votre téléphone ; celles-ci passent par le proxy push de Nextcloud et ont toujours fonctionné.) Sur une machine native, c’est trois éléments : l’application, un service systemd et un location /push/ dans le serveur web. Sur K3s, ce sont les trois mêmes éléments dans d’autres habits : l’application, son propre Deployment et une route Ingress.
J’avais construit la partie Ansible la veille, désactivée. En la relisant avant d’actionner l’interrupteur, j’ai trouvé quatre bugs qui auraient produit le pire résultat possible – un playbook qui passe au vert et une fonctionnalité morte, parce que chaque étape était réglée pour tolérer les échecs. occ tournait en root (Nextcloud refuse). Le binaire était choisi avec un glob – et par ordre alphabétique, aarch64 passe avant x86_64. Le conteneur tournait en root avec toutes les capabilities supprimées, ce qui paraît sûr jusqu’à ce qu’on réalise que root sans CAP_DAC_OVERRIDE ne peut pas lire un fichier 0640 appartenant à www-data – comme config.php. Et l’ingress transmettait /push/… tel quel, alors que le démon attend /ws à sa racine.
Même avec les quatre corrigés, le premier autotest a renvoyé un 404 – rendu par Nextcloud. La route existait ; nginx ne la choisissait simplement jamais. Les routes Nextcloud sont une regex fourre-tout, location ~ "^/", et dans nginx une regex l’emporte sur un simple préfixe. La route push a donc dû devenir une regex elle aussi – et elle perdait encore, parce que le contrôleur d’ingress émet les locations regex dans l’ordre alphabétique du nom de l’Ingress, et nginx prend la première correspondance. notify-push-routes se trie après nextcloud-routes ; renommée en nextcloud-push-routes (« p » avant « r »), elle gagne. Pas mon correctif le plus glorieux, mais un correctif documenté. Les six autotests sont passés au vert, et la première session de navigateur est passée en WebSocket (HTTP 101) et a reçu ses premiers événements de fichiers. Verdict honnête : pour une poignée de clients de bureau, c’est un plus agréable – les changements arrivent en quelques secondes au lieu de jusqu’à une demi-minute –, pas une révolution.
Gremlin n° 1 : le durcissement qui a fait tomber la base de données
Pendant qu’un playbook tournait, Nextcloud a brièvement répondu 500. MariaDB avait été arrêté – proprement, sans plantage. Le journal racontait l’histoire : deux secondes plus tôt, le rôle de durcissement du système avait supprimé le paquet rsync comme « inutilisé sur cet hôte ». C’était vrai sur l’hôte du blog, où ça avait été vérifié. Sur l’hôte Nextcloud, le paquet MariaDB-server dépend de rsync – ses outils de cluster Galera embarquent un script de transfert d’état basé sur rsync, même sur un nœud unique qui ne rejoindra jamais un cluster. Donc dnf a consciencieusement supprimé MariaDB au passage, ce qui arrête le service ; un rôle ultérieur a réinstallé les deux, et un autre a redémarré la base. Effet net : deux à trois minutes d’indisponibilité à chaque exécution du playbook, invisibles à moins de regarder au bon moment. Le correctif tient en une seule garde : ne supprimer rsync que si rpm -e --test rsync indique que rien n’en dépend. « Vérifié sur l’hôte A » ne veut pas dire « vrai sur l’hôte B ».
Gremlin n° 2 : les alertes qui ne pouvaient jamais se déclencher
Les règles d’alerte Grafana – disque, mémoire, CPU, nœud hors ligne – avaient l’air parfaitement correctes dans l’interface. Dans le log, chacune d’elles échouait à chaque évaluation avec condition must not be empty : le code de provisionnement n’avait jamais dit à Grafana quelle requête décide de l’alerte. Et comme les règles traitaient les erreurs d’évaluation comme « OK », rien ne passait jamais au rouge. Un champ manquant, "condition": "B", et elles s’évaluent correctement désormais. Une alerte qui ne peut pas se déclencher ressemble exactement à un système en bonne santé – le vert le plus dangereux qui soit. (Des alertes mail basiques via Monit étaient en place depuis le début, donc la machine ne volait pas complètement à l’aveugle.)
Gremlin n° 3 : le bannissement pour scan de ports qui n’existait pas
Une version antérieure de cet article même affirmait qu’un scan de ports « vous vaut une mise à l’écart fail2ban immédiate ». Ce n’était pas le cas. fail2ban ne réagit qu’aux lignes de log, et le pare-feu journalise les paquets rejetés à un débit volontairement minuscule, si bien qu’un scan disparaissait simplement dans la politique de rejet par défaut. Inoffensif, mais personne n’était banni non plus. Le pare-feu avait même une paire d’ensembles étiquetés « port-scanner blocklist », et rien de lié aux scans ne les a jamais remplis. Le correctif vit maintenant dans le noyau : la dernière règle avant le rejet final compte les tentatives de connexion par source, et elle ne voit que des paquets destinés à des ports fermés, donc le trafic normal ne compte jamais. Une source qui continue de frapper est rejetée sur tous les ports pendant un moment. Pas de logs, pas de démon, pas de parsing.
Le déploiement a fait apparaître un piège de plus. Le service nftables standard recharge avec flush ruleset, et sur l’hôte Nextcloud, kube-proxy, flannel et Calico gardent eux aussi leurs règles dans nftables. Chaque modification du pare-feu effaçait donc aussi le réseau de Kubernetes, pendant jusqu’à une minute et demie, le temps qu’il se resynchronise. Désormais, le jeu de règles ne remplace que sa propre table, en une seule étape atomique.
Jours quatre à six : lire les logs comme un détective 🔎
Une fois la poussière retombée, le travail est passé de « faire marcher » à « garder un œil dessus ». Pendant quelques jours, la routine était simple : lire nextcloud.log, les logs des conteneurs et le log de l’ingress, et pour chaque ligne qui semblait bizarre, demander pourquoi jusqu’à obtenir une réponse. Une condition préalable a rendu cela possible : le niveau de log était passé de 4 (fatal uniquement) à 2 (avertissements). Au niveau 4, rien de ce qui suit ne serait apparu. Un log silencieux n’est pas toujours un serveur en bonne santé. Parfois, c’est juste un serveur qu’on a rendu muet.
Gremlin n° 4 : la jail qui surveillait un fichier mort
fail2ban tournait, les jails nginx étaient « actives », et la liste des bannis était vide. Pendant des jours. Semaine calme ou jail cassée ? Le contrôleur d’ingress écrit son log d’accès dans un fichier dont le nom se termine par un compteur (…/0.log, 1.log, …), et chaque redémarrage du conteneur en commence un nouveau. fail2ban développe le glob de logpath uniquement au démarrage de la jail. Après un redémarrage, fail2ban se lance avant le conteneur d’ingress, s’accroche à l’ancien fichier, puis suit fidèlement un log dans lequel plus personne n’écrit, sur les deux hôtes. Le correctif est un petit timer systemd qui remarque quand l’ensemble des fichiers de log de l’ingress change (et une fois après chaque démarrage) et recharge uniquement les jails nginx. Un outil de sécurité qui annonce « tout est calme » parce qu’il regarde le mauvais fichier est pire que rien, parce que vous croyez être couvert.
Gremlin n° 5 : le ping-pong des propriétaires
À chaque exécution du playbook, le status.php de Nextcloud renvoyait brièvement une erreur et le répertoire de données échouait à son propre test d’écriture, avant de se rétablir tout seul plus loin dans la même exécution. Les coupables : deux rôles en désaccord. Le rôle SELinux créait les répertoires hostPath en root:root, et le rôle de déploiement les passait en 33:33 (www-data). Chaque exécution faisait basculer le propriétaire dans un sens puis dans l’autre, et jusqu’à ce que le rôle suivant le rétablisse, Nextcloud se retrouvait face à un répertoire de données dans lequel il n’avait pas le droit d’écrire. Désormais, les deux rôles définissent le même propriétaire et le même mode, et le ping-pong est terminé. Deux rôles qui font chacun quelque chose de « correct » peuvent quand même s’additionner en un bug.
Gremlin n° 6 : les aperçus Office et le mur de 1 Mo
Les petits fichiers Word et Excel avaient de jolies miniatures dans la liste des fichiers, les plus gros non. Aucune erreur dans l’interface, juste l’icône générique. Le log de l’ingress avait la réponse : 413 Request Entity Too Large à chaque appel convert-to vers Collabora. Nextcloud génère les aperçus Office en téléversant le document vers Collabora, et la limite par défaut de nginx pour le corps des requêtes est de 1 Mo. La route Nextcloud avait une limite généreuse, mais la route Collabora n’en avait jamais reçu. Une annotation (client-max-body-size: 2g) a réglé le problème, et les aperçus concernés peuvent simplement être régénérés. Les quelques erreurs convert-to restantes concernent des documents que Collabora ne peut vraiment pas ouvrir, ce qui est un problème de fichier et non de configuration.
Gremlin n° 7 : la photo de 108 mégapixels
Certaines photos restaient sans miniature, et l’application recevait sans cesse un 404 pour leurs aperçus. Toutes venaient d’un même téléphone, avec 108 mégapixels et 12 032 pixels de large. Décoder une image de cette taille demande environ 430 Mo de RAM, et le preview_max_memory de Nextcloud est par défaut de 256 Mo. Au-delà, Nextcloud n’essaie même pas. Il saute l’aperçu en silence. Relevé à 512 (la limite propre de PHP est bien au-dessus), et les photos ont enfin eu leurs miniatures.
Au passage, les photos HEIC d’iPhone ont aussi eu droit à des aperçus. C’est une ligne dans enabledPreviewProviders, avec un piège : définir cette clé remplace la liste par défaut intégrée au lieu de l’étendre, donc il faut répéter les valeurs par défaut (PNG, JPEG, GIF, texte, OpenDocument…), sinon on se retrouve avec des aperçus HEIC et plus aucun pour les JPEG. Ce qui reste sans aperçu relève désormais des données, pas de la configuration : quelques dizaines de restes de 400 octets d’un téléversement raté il y a des années, quelques fichiers vides et une poignée de PNG de plus de 50 Mo.
Gremlin n° 8 : la configuration qui n’est jamais arrivée
En vérifiant le correctif précédent après une exécution du playbook, j’ai découvert pire. Les nouvelles valeurs figuraient dans la ConfigMap Kubernetes, mais le custom.config.php à l’intérieur du pod en cours d’exécution datait de la veille. Le fichier est monté avec subPath, et un montage subPath est copié une seule fois au démarrage du conteneur et jamais mis à jour. Kubernetes met à jour à chaud les volumes ConfigMap normaux, mais pas ceux en subPath. Chaque changement de configuration dans le dépôt ne prenait donc effet qu’au prochain redémarrage accidentel du pod. (Le correctif des aperçus n’a fonctionné entre-temps que parce qu’il avait aussi été appliqué via occ.)
La solution standard est une annotation checksum/config sur le template du pod : un SHA-256 des templates de configuration rendus. Changez la configuration et le hash change, ce qui pousse Kubernetes à redéployer le pod. Ne touchez pas à la configuration et rien ne se passe. Le blog a reçu le même traitement pour ses configurations nginx et PHP. Leçon : « le playbook est passé au vert » et « le réglage est actif » sont deux affirmations différentes, alors vérifiez le fichier à l’intérieur du conteneur.
À quoi ressemble un log en bonne santé 🩺
Après tout cela, la revue quotidienne est devenue ennuyeuse, et c’est bien le but. Une journée typique compte maintenant une poignée de lignes dans nextcloud.log : un aperçu qui n’a pas pu être ouvert, une notice PHP. Le reste, c’est du bruit qu’il vaut la peine de connaître par cœur, pour qu’il ne vous envoie pas à la chasse chaque matin :
- Collabora « Broken pipe » / « Connection reset », environ 60 par heure, même à 3 heures du matin : l’autre côté a fermé la connexion avant que Collabora ait fini d’écrire. Inoffensif.
- nginx « access forbidden » sur
/data/.ncdataet/.env: le premier est le propre contrôle de sécurité de Nextcloud, qui vérifie que le répertoire de données n’est pas accessible depuis le web, le second est un bot. Les deux sont bloqués volontairement. - « config differs from the latest version of this image » à chaque démarrage de pod : le rôle gère lui-même ces fichiers (Redis avec mot de passe, par exemple), ils sont donc censés différer.
- notify_push « Self test failed » juste après un redémarrage : le test TLS échoue une fois avec une erreur de certificat (très probablement l’ingress qui sert encore son certificat par défaut pendant quelques secondes avant que le vrai soit chargé). Une minute plus tard, l’autotest est à 6/6 au vert.
L’intérêt de cette liste, ce ne sont pas les entrées elles-mêmes. Une fois que vous savez à quoi ressemble l’inoffensif, la seule ligne qui ne l’est pas saute immédiatement aux yeux. C’est tout le secret de la surveillance des logs. Pas de magie, juste connaître la ligne de base.
Verdict ✅
C’est en ligne. Certificats valides, édition Office fonctionnelle, données intactes, et les utilisateurs n’y ont vu que du feu. La migration elle-même, c’était les 20 % faciles ; les 80 % restants ont été une chasse au trésor pour débusquer des réglages et des comportements qui ne déraillent que dans exactement les nouvelles conditions. Comme toujours : le serveur n’a jamais été le problème – ce sont les suppositions qui l’étaient. 😄
Le troisième jour y a ajouté une note de bas de page. Les bugs les plus vicieux n’étaient pas ceux qui plantaient – c’étaient ceux qui restaient verts : une case à cocher, un playbook tolérant aux échecs, une règle d’alerte incapable de se déclencher. Si cette migration m’a appris une habitude, c’est de vérifier la chose elle-même, pas ce qui rend compte d’elle. 🔍
Une dernière chose : ce n’était pas mon seul rodéo Nextcloud. J’administre aussi bénévolement une seconde instance de production – plus petite, avec moins de données et moins d’utilisateurs inscrits –, et tout ce que j’ai appris ici s’est révélé vraiment utile. C’est donc elle la prochaine : je la migrerai sur la même pile K3s dans les deux semaines à venir, en espérant marcher sur bien moins de ces mines la seconde fois. 🤞
La pile Ansible complète derrière tout cela – les rôles WordPress et Nextcloud, l’ingress, cert-manager, la supervision et chaque astuce de durcissement mentionnée plus haut – se trouve sur GitHub, sans les secrets ni les détails propres aux hôtes : github.com/aptupgrademe/www_k3s. Cette migration correspond au jalon v3.0.0, vous pouvez donc parcourir exactement l’état décrit dans cet article plutôt que ce que HEAD se trouve être aujourd’hui.




