From fb908b180b03bc8506cee4e664de3b51572146a5 Mon Sep 17 00:00:00 2001 From: Michael SCHAL Date: Tue, 18 Aug 2026 21:57:45 +0200 Subject: [PATCH] =?UTF-8?q?Perte=20de=20donn=C3=A9es=20en=20conteneur=20:?= =?UTF-8?q?=20alerte=20si=20/data=20n'est=20pas=20mont=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Modèles, discussions et fichiers disparaissaient à chaque recréation du conteneur quand /data n'était pas un volume : tout vivait dans la couche éphémère. Loki le détecte maintenant (mountinfo) et le dit — bandeau rouge dans l'UI (/api/status warn), avertissement en tête du journal, et l'entrypoint exporte LOKI_HOME pour ses sous-commandes. Co-Authored-By: Claude Opus 5 --- docker-entrypoint.sh | 9 +++++ internal/loki/sys_datavolume.go | 67 +++++++++++++++++++++++++++++++++ internal/loki/web_api.go | 12 +++++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 internal/loki/sys_datavolume.go diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 18cd143..2200bac 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -4,8 +4,17 @@ set -eu : "${LOKI_HOME:=/data}" +export LOKI_HOME # les sous-commandes loki doivent voir le MEME dossier mkdir -p "$LOKI_HOME" +# Garde-fou persistance : si /data n'est pas un point de montage, tout ce que +# l'utilisateur fera (modèles, discussions, fichiers) mourra avec le conteneur. +# On le dit au premier écran du journal — l'UI l'affiche aussi en bandeau. +if ! grep -qs " $LOKI_HOME " /proc/self/mountinfo; then + echo "[entrypoint] ⚠️ $LOKI_HOME n'est pas un volume monté : les données seront PERDUES à la recréation du conteneur." + echo "[entrypoint] Ajoute « -v /chemin/hote/data:/data » (docker run) ou le mapping volumes du docker-compose." +fi + # Le chemin du moteur est imposé par l'image (llama-server précompilé de # l'image officielle llama.cpp, dans /app) : on le (re)pose à chaque boot, # une mise à jour de l'image ne doit pas laisser un BIN obsolète en base. diff --git a/internal/loki/sys_datavolume.go b/internal/loki/sys_datavolume.go new file mode 100644 index 0000000..162ab56 --- /dev/null +++ b/internal/loki/sys_datavolume.go @@ -0,0 +1,67 @@ +package loki + +// sys_datavolume.go — garde-fou contre la PERTE DE DONNÉES en conteneur. +// +// Le symptôme vécu : modèles, discussions et fichiers disparus après un +// redémarrage du serveur. La cause, dans tous les cas observés : /data n'est +// PAS un volume monté (template Docker sans mapping, `docker run` sans -v, +// chemin hôte qui change d'un lancement à l'autre). Tout vit alors dans la +// couche d'écriture du conteneur, qui meurt avec lui à la première recréation +// (mise à jour d'image, compose down/up, redémarrage de l'array Unraid). +// +// Loki ne peut pas monter le volume à la place de l'utilisateur, mais il peut +// le VOIR et le DIRE : au démarrage en conteneur, si LOKI_HOME n'est pas un +// point de montage, on le journalise en rouge et l'UI l'affiche en bandeau. + +import ( + "bufio" + "os" + "path/filepath" + "strings" +) + +// dataDirIsMounted dit si LokiHome() est un point de montage (bind mount ou +// volume Docker). Lecture de /proc/self/mountinfo — Linux seulement, or c'est +// le seul endroit où Loki tourne en conteneur. Hors conteneur : sans objet. +// +// Renvoie (monté, connu) : connu=false quand on n'a pas pu lire mountinfo — +// on ne crie pas au loup sur une simple erreur de lecture. +func dataDirIsMounted() (mounted, known bool) { + f, err := os.Open("/proc/self/mountinfo") + if err != nil { + return false, false + } + defer f.Close() + target := filepath.Clean(LokiHome()) + sc := bufio.NewScanner(f) + for sc.Scan() { + // Format mountinfo : champs séparés par des espaces, le 5ᵉ (index 4) + // est le point de montage. Les espaces dans les chemins sont encodés + // \040 — décodés avant comparaison. + fields := strings.Fields(sc.Text()) + if len(fields) < 5 { + continue + } + mp := strings.ReplaceAll(fields[4], `\040`, " ") + if filepath.Clean(mp) == target { + return true, true + } + } + return false, sc.Err() == nil +} + +// dataVolumeWarning renvoie le message d'alerte si les données sont en périple +// ("" si tout va bien). Évalué au démarrage du serveur web et servi par +// /api/status pour le bandeau de l'UI. +func dataVolumeWarning() string { + if os.Getenv("LOKI_CONTAINER") == "" { + return "" // hors conteneur : l'utilisateur gère ses chemins + } + mounted, known := dataDirIsMounted() + if !known || mounted { + return "" + } + return "⚠️ " + LokiHome() + " n'est PAS un volume monté : modèles, discussions et fichiers seront PERDUS à la recréation du conteneur (mise à jour, redémarrage du serveur). " + + "Ajoute un mapping de volume — compose : « - /mnt/user/appdata/loki/data:/data » (et « …/models:/models ») ; " + + "docker run : « -v /chemin/hote/data:/data ». Voir docker-compose.unraid.yml du dépôt." +} diff --git a/internal/loki/web_api.go b/internal/loki/web_api.go index 29fdb0a..8826354 100644 --- a/internal/loki/web_api.go +++ b/internal/loki/web_api.go @@ -44,6 +44,16 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { if !health { loadErr = modelLoadError() } + // Alerte de persistance : /data non monté en conteneur = données perdues à + // la prochaine recréation. Même bandeau que les autres avertissements. + warn := appWarning() + if dv := dataVolumeWarning(); dv != "" { + if warn != "" { + warn += " — " + dv + } else { + warn = dv + } + } sendJSON(w, 200, map[string]any{ "state": state, "active": active, @@ -51,7 +61,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) { "port": LLMPort(), "ctx": ctx, "version": Version, - "warn": appWarning(), // ex. App Translocation macOS — vide si tout va bien + "warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien "load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon }) }