Conteneurisation : image CUDA autonome (moteur + UI, un seul conteneur)

- Dockerfile 3 étapes : llama.cpp CUDA (flags de backend_build.go, sauf
  GGML_NATIVE=OFF — l'image est bâtie sur un runner, pas sur la machine
  cible), binaire Go statique, runtime CUDA léger (tini, git, nodejs pour
  les serveurs MCP npx).
- docker-entrypoint.sh : sème BIN/HOST/PORT (+ MODEL/CTX/NGL depuis l'env,
  au premier boot seulement), lance le moteur en supervision PID puis
  'loki web' au premier plan. UI seule exposée (8090) ; le moteur (8080,
  non authentifié) reste interne.
- Nouvelle commande 'loki config [get|set]' : configuration non interactive
  (la config vit dans bbolt, pas dans un fichier plat).
- docker-compose.yml (build local, réservation GPU) + variante Unraid
  (image GHCR, runtime nvidia) ; volumes /data (LOKI_HOME) et /models.

Un seul conteneur car llm_client.go joint le moteur sur localhost (hérité
de l'amont).
This commit is contained in:
Loki committed 2026-08-14 21:47:57 +00:00
1 parent 6c4a8a3cf5
commit d5d0ed1f35
8 files changed
+292

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
.git
.github
data/
models/
docs/
dist/
*.md
!README.md
.env
.env.*
+19
View File
@@ -0,0 +1,19 @@
# ── Loki — configuration du build/run Docker ────────────────────────────
# (fork d'AJEAN : https://github.com/nathaninline/ajean)
# Port de l'interface web (identique dedans/dehors).
LOKI_WEB_PORT=8090
# Architectures CUDA compilées (build local uniquement).
# 75=Turing (GTX 16xx / RTX 20xx), 86=Ampere (RTX 30xx), 89=Ada (RTX 40xx).
# Restreindre à sa carte divise le temps de compilation.
CUDA_ARCHS=75;86;89
# Optionnel — semé au PREMIER démarrage seulement (modifiable ensuite dans
# l'UI, qui garde la main) :
# Modèle initial : nom d'un .gguf présent dans ./models ou chemin complet.
LOKI_MODEL=
# Taille de contexte (défaut moteur : 32768)
LOKI_CTX=
# Couches déportées sur le GPU (défaut : 999 = tout)
LOKI_NGL=
+91
View File
@@ -0,0 +1,91 @@
# ── Loki — image GPU autonome (fork d'AJEAN, https://github.com/nathaninline/ajean)
#
# Trois étapes :
# 1. compilation de llama.cpp avec CUDA (mêmes flags que backend_build.go,
# sauf GGML_NATIVE=OFF : l'image est bâtie sur un runner GitHub, pas sur
# la machine qui l'exécutera — des instructions CPU « natives » du runner
# provoqueraient un Illegal instruction ailleurs) ;
# 2. compilation du binaire Go `loki` (UI embarquée via go:embed) ;
# 3. runtime CUDA léger : llama-server + loki + entrypoint.
#
# Pré-requis d'exécution : NVIDIA Container Toolkit sur l'hôte.
# docker build -t loki --build-arg CUDA_ARCHS=86 .
# CUDA_ARCHS : 75=Turing, 86=Ampere, 89=Ada — restreindre à sa carte divise
# le temps de build et la taille de l'image.
# ── Étape 1 : llama.cpp CUDA ────────────────────────────────────────────
FROM nvidia/cuda:12.6.3-devel-ubuntu24.04 AS llamacpp
RUN apt-get update && apt-get install -y --no-install-recommends \
git cmake build-essential ca-certificates \
&& rm -rf /var/lib/apt/lists/*
# Épingler LLAMACPP_REF sur un tag (ex. b4991) pour des builds reproductibles.
ARG LLAMACPP_REF=master
ARG CUDA_ARCHS=75;86;89
RUN git clone --depth 1 --branch "${LLAMACPP_REF}" \
https://github.com/ggml-org/llama.cpp /src/llama.cpp
RUN cmake -S /src/llama.cpp -B /src/llama.cpp/build \
-DCMAKE_BUILD_TYPE=Release \
-DGGML_CUDA=ON \
-DGGML_CUDA_F16=ON \
-DGGML_NATIVE=OFF \
-DCMAKE_CUDA_ARCHITECTURES="${CUDA_ARCHS}" \
-DLLAMA_CURL=OFF \
-DLLAMA_BUILD_TESTS=OFF \
-DLLAMA_BUILD_EXAMPLES=OFF \
-DLLAMA_BUILD_UI=OFF \
-DLLAMA_USE_PREBUILT_UI=OFF \
&& cmake --build /src/llama.cpp/build --target llama-server -j"$(nproc)" \
&& mkdir -p /opt/llama.cpp \
&& find /src/llama.cpp/build -name 'llama-server' -type f -exec cp {} /opt/llama.cpp/ \; \
&& find /src/llama.cpp/build -name '*.so*' -exec cp -P {} /opt/llama.cpp/ \;
# ── Étape 2 : binaire Go loki ───────────────────────────────────────────
FROM golang:1.25 AS gobuild
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY cmd/ cmd/
COPY internal/ internal/
COPY tools/ tools/
RUN CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/loki ./cmd/loki
# ── Étape 3 : runtime ───────────────────────────────────────────────────
FROM nvidia/cuda:12.6.3-runtime-ubuntu24.04 AS runtime
# Marqueur de build (sha court), injecté par le workflow GitHub.
ARG LOKI_VERSION=dev
LABEL org.opencontainers.image.revision="${LOKI_VERSION}" \
org.opencontainers.image.source="https://github.com/R0m1k3/Loki" \
org.opencontainers.image.description="Loki — fork conteneurisé d'AJEAN (github.com/nathaninline/ajean, MIT)"
# curl pour le HEALTHCHECK ; git pour les outils de l'agent ; nodejs/npm pour
# les serveurs MCP lancés via npx ; libgomp1 pour llama-server ; tini en PID 1.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl git libgomp1 tini nodejs npm \
&& rm -rf /var/lib/apt/lists/*
COPY --from=llamacpp /opt/llama.cpp /opt/llama.cpp
COPY --from=gobuild /out/loki /usr/local/bin/loki
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh && mkdir -p /data /models
ENV LOKI_CONTAINER=1 \
LOKI_HOME=/data \
LOKI_MODEL_DIRS=/models \
LD_LIBRARY_PATH=/opt/llama.cpp \
NVIDIA_VISIBLE_DEVICES=all \
NVIDIA_DRIVER_CAPABILITIES=compute,utility
# 8090 : interface web (seule à exposer). Le moteur (8080) reste interne au
# conteneur — il n'est pas authentifié par défaut.
EXPOSE 8090
HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 \
CMD curl -fsS "http://localhost:${LOKI_WEB_PORT:-8090}/" >/dev/null || exit 1
WORKDIR /data
ENTRYPOINT ["tini", "--", "docker-entrypoint.sh"]
+42
View File
@@ -0,0 +1,42 @@
# ───────────────────────────────────────────────────────────────────────────
# Loki — docker-compose pour Unraid (image préconstruite par GitHub Actions)
#
# Loki est un fork conteneurisé d'AJEAN (github.com/nathaninline/ajean, MIT) :
# moteur llama.cpp CUDA + interface web dans UN conteneur autonome.
# Aucun build ni git nécessaire sur Unraid — l'image est publiée sur GHCR à
# chaque push sur main.
#
# MISE EN PLACE :
# 1) Plugin « Nvidia Driver » (Apps) installé, GPU visible (nvidia-smi).
# 2) Crée les dossiers de données (terminal Unraid) :
# mkdir -p /mnt/user/appdata/loki/data /mnt/user/appdata/loki/models
# 3) Plugin « Compose Manager » -> nouvelle stack -> colle ce fichier.
# 4) Compose Up. Interface : http://<ip-unraid>:8090
#
# Les modèles se téléchargent directement depuis l'UI (catalogue intégré),
# ou dépose tes .gguf dans /mnt/user/appdata/loki/models.
#
# Mise à jour : Compose Down puis Up avec « pull image », ou
# « docker compose pull » avant Up.
# ───────────────────────────────────────────────────────────────────────────
services:
loki:
image: ghcr.io/r0m1k3/loki:latest
container_name: loki
runtime: nvidia
ports:
# Pour changer de port : modifie 8090 à gauche ET LOKI_WEB_PORT… ou
# seulement à gauche (ex. "8717:8090") si tu veux garder l'ancien port.
- "8090:8090"
environment:
- NVIDIA_VISIBLE_DEVICES=all
# Optionnel : modèle initial (sinon, choisis-le dans l'UI au premier
# lancement). Ex. LOKI_MODEL=Qwen3-14B-Q4_K_M.gguf après dépôt du
# fichier dans /mnt/user/appdata/loki/models.
- LOKI_MODEL=
volumes:
# /data : config, base, mémoire, workspace, modèles téléchargés par l'UI
- /mnt/user/appdata/loki/data:/data
# /models : dossier de .gguf déposés à la main (gros fichiers)
- /mnt/user/appdata/loki/models:/models
restart: unless-stopped
+38
View File
@@ -0,0 +1,38 @@
# Loki — fork conteneurisé d'AJEAN (https://github.com/nathaninline/ajean, MIT)
# Un seul service : le conteneur embarque le moteur llama.cpp (CUDA) ET l'UI.
#
# cp .env.example .env # ajuste si besoin
# docker compose up --build # build local (30-45 min : compilation CUDA)
#
# Interface : http://localhost:8090
# Pré-requis GPU : NVIDIA Container Toolkit sur l'hôte.
services:
loki:
build:
context: .
args:
# Restreins à ta carte pour un build plus court : 75=Turing (GTX 16xx/RTX 20xx),
# 86=Ampere (RTX 30xx), 89=Ada (RTX 40xx).
CUDA_ARCHS: ${CUDA_ARCHS:-75;86;89}
container_name: loki
ports:
- "${LOKI_WEB_PORT:-8090}:${LOKI_WEB_PORT:-8090}"
environment:
- LOKI_WEB_PORT=${LOKI_WEB_PORT:-8090}
# Semé UNE fois au premier démarrage (modifiable ensuite dans l'UI) :
- LOKI_MODEL=${LOKI_MODEL:-}
- LOKI_CTX=${LOKI_CTX:-}
- LOKI_NGL=${LOKI_NGL:-}
volumes:
# /data = LOKI_HOME : config, base bbolt, mémoire, workspace, modèles téléchargés
- ./data:/data
# /models : dossier(s) de modèles GGUF supplémentaires (LOKI_MODEL_DIRS)
- ./models:/models
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: ["gpu"]
restart: unless-stopped
+34
View File
@@ -0,0 +1,34 @@
#!/bin/sh
# Entrypoint du conteneur Loki : sème la configuration au premier démarrage,
# lance le moteur (supervision PID, sans systemd), puis l'UI au premier plan.
set -eu
: "${LOKI_HOME:=/data}"
mkdir -p "$LOKI_HOME"
# Le chemin du moteur est imposé par l'image : on le (re)pose à chaque boot,
# une mise à jour de l'image ne doit pas laisser un BIN obsolète en base.
loki config set "BIN=/opt/llama.cpp/llama-server"
# Les autres clés ne sont semées QUE si absentes : ce que l'utilisateur règle
# ensuite dans l'UI (modèle, contexte…) est conservé d'un redémarrage à l'autre.
seed() { # seed CLÉ VALEUR
[ -n "$2" ] || return 0
cur=$(loki config get "$1")
[ -n "$cur" ] || loki config set "$1=$2"
}
seed HOST "${LOKI_ENGINE_HOST:-127.0.0.1}"
seed PORT "${LOKI_ENGINE_PORT:-8080}"
seed MODEL "${LOKI_MODEL:-}"
seed CTX "${LOKI_CTX:-}"
seed NGL "${LOKI_NGL:-}"
# Moteur : démarrage best-effort. Sans MODEL configuré, on laisse l'UI monter
# quand même — le moteur partira au premier modèle choisi/téléchargé dans l'UI.
if [ -n "$(loki config get MODEL)" ]; then
loki start || echo "[entrypoint] moteur non démarré (voir loki logs) — l'UI reste accessible"
else
echo "[entrypoint] aucun MODEL configuré — télécharge un modèle depuis l'UI pour démarrer le moteur"
fi
exec loki web "${LOKI_WEB_PORT:-8090}"
+56
View File
@@ -0,0 +1,56 @@
package loki
// cli_config.go — lecture/écriture non interactive de la configuration du
// moteur (l'équivalent scriptable de `loki edit`). Ajout du fork Loki : le
// docker-entrypoint s'en sert pour semer BIN/MODEL/CTX/NGL au premier
// démarrage du conteneur, où aucun $EDITOR n'est disponible.
import (
"fmt"
"sort"
"strings"
)
// cmdConfigKV :
//
// loki config affiche toute la configuration (KEY=VALUE)
// loki config get KEY affiche la valeur (vide si absente)
// loki config set K=V [K=V…] définit des clés ; une valeur vide supprime
func cmdConfigKV(args []string) error {
if len(args) == 0 {
cfg := ReadConfig()
keys := make([]string, 0, len(cfg))
for k := range cfg {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s=%s\n", k, cfg[k])
}
return nil
}
switch args[0] {
case "get":
if len(args) != 2 {
return fmt.Errorf("usage : loki config get KEY")
}
fmt.Println(ReadConfig()[args[1]])
return nil
case "set":
if len(args) < 2 {
return fmt.Errorf("usage : loki config set KEY=VALUE [KEY=VALUE…]")
}
for _, kv := range args[1:] {
k, v, ok := strings.Cut(kv, "=")
k = strings.TrimSpace(k)
if !ok || k == "" {
return fmt.Errorf("argument invalide %q — attendu KEY=VALUE", kv)
}
if err := SetConfigKey(k, strings.TrimSpace(v)); err != nil {
return err
}
}
return nil
}
return fmt.Errorf("usage : loki config [get KEY | set KEY=VALUE…]")
}
+2
View File
@@ -54,6 +54,8 @@ func Main() {
mustExit(serviceLogs())
case "edit":
mustExit(editConfig())
case "config":
mustExit(cmdConfigKV(args))
case "set-api-key":
mustExit(cmdSetAPIKey(args))
case "set-web-key":