From d5d0ed1f350409114cbc7e70d31cd5609795aba0 Mon Sep 17 00:00:00 2001 From: Loki Date: Fri, 14 Aug 2026 21:47:57 +0000 Subject: [PATCH] Conteneurisation : image CUDA autonome (moteur + UI, un seul conteneur) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- .dockerignore | 10 ++++ .env.example | 19 ++++++++ Dockerfile | 91 +++++++++++++++++++++++++++++++++++++ docker-compose.unraid.yml | 42 +++++++++++++++++ docker-compose.yml | 38 ++++++++++++++++ docker-entrypoint.sh | 34 ++++++++++++++ internal/loki/cli_config.go | 56 +++++++++++++++++++++++ internal/loki/run.go | 2 + 8 files changed, 292 insertions(+) create mode 100644 .dockerignore create mode 100644 .env.example create mode 100644 Dockerfile create mode 100644 docker-compose.unraid.yml create mode 100644 docker-compose.yml create mode 100644 docker-entrypoint.sh create mode 100644 internal/loki/cli_config.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..69adb94 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,10 @@ +.git +.github +data/ +models/ +docs/ +dist/ +*.md +!README.md +.env +.env.* diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..bdf9094 --- /dev/null +++ b/.env.example @@ -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= diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..19d5a05 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/docker-compose.unraid.yml b/docker-compose.unraid.yml new file mode 100644 index 0000000..8b8884e --- /dev/null +++ b/docker-compose.unraid.yml @@ -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://: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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..5734cd3 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..0178e17 --- /dev/null +++ b/docker-entrypoint.sh @@ -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}" diff --git a/internal/loki/cli_config.go b/internal/loki/cli_config.go new file mode 100644 index 0000000..4916c3f --- /dev/null +++ b/internal/loki/cli_config.go @@ -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…]") +} diff --git a/internal/loki/run.go b/internal/loki/run.go index e576889..48141ce 100644 --- a/internal/loki/run.go +++ b/internal/loki/run.go @@ -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":