Perte de données en conteneur : alerte si /data n'est pas monté

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 <noreply@anthropic.com>
This commit is contained in:
MichaelandClaude Opus 5 committed 2026-08-18 21:58:04 +02:00
1 parent 11a4aebf37
commit fb908b180b
3 files changed
+87 -1

No files matched your search

+9
View File
@@ -4,8 +4,17 @@
set -eu set -eu
: "${LOKI_HOME:=/data}" : "${LOKI_HOME:=/data}"
export LOKI_HOME # les sous-commandes loki doivent voir le MEME dossier
mkdir -p "$LOKI_HOME" 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 # 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, # 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. # une mise à jour de l'image ne doit pas laisser un BIN obsolète en base.
+67
View File
@@ -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."
}
+11 -1
View File
@@ -44,6 +44,16 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
if !health { if !health {
loadErr = modelLoadError() 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{ sendJSON(w, 200, map[string]any{
"state": state, "state": state,
"active": active, "active": active,
@@ -51,7 +61,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
"port": LLMPort(), "port": LLMPort(),
"ctx": ctx, "ctx": ctx,
"version": Version, "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 "load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon
}) })
} }