⚠️ Disclaimer

La méthode que je présente ici correspond à ma propre démarche d’apprentissage. Elle peut contenir des approximations ou des erreurs, car j’apprends et je progresse chaque jour un peu plus. Ne prenez donc pas ce que je fais comme une référence absolue, mais plutôt comme un retour d’expérience personnel.

Contexte et objectifs

Depuis que j’ai commencé ma reconversion en cybersécurité, j’ai voulu explorer l’intersection entre IA et souveraineté des données. Le déclencheur a été simple : je cherchais un moyen de poser des questions sur ma documentation homelab sans copier-coller des configs sensibles dans ChatGPT.

L’idée était de monter une solution équivalente à ChatGPT, mais qui tourne entièrement sur mon infrastructure locale. Pas de requête vers OpenAI, Google ou autre service cloud. Tout reste chez moi, dans le homelab.

Objectifs du projet :

  • Déployer un LLM (Large Language Model) local fonctionnel
  • Implémenter du RAG (Retrieval Augmented Generation) pour interroger mes propres documents
  • Démontrer une maîtrise de l’architecture IA locale, un différenciateur pour mon profil reconversion
  • Avoir un cas d’usage concret pour le portfolio : conformité RGPD by design

Contexte homelab :

Mon infrastructure tourne principalement sur trois mini-PC sous Proxmox (96 Go RAM chacun), avec un pfSense pour le réseau et un NAS Synology pour le stockage. Pour ce projet spécifique, j’ai utilisé mon Mac M4 Pro (64 Go RAM) comme serveur IA, principalement pour profiter du Neural Engine optimisé pour l’inférence de modèles de langage.


Architecture et choix techniques

Vue d’ensemble

L’architecture repose sur trois containers Docker orchestrés via Docker Compose :

  • Ollama : le runtime qui fait tourner le modèle de langage (Mistral 7B dans mon cas)
  • OpenWebUI : une interface web type ChatGPT, open source, qui gère l’authentification et le RAG
  • ChromaDB : la base vectorielle qui stocke les embeddings des documents pour la recherche sémantique

Le tout communique via réseau Docker interne. Seuls les ports d’interface sont exposés (3001 pour OpenWebUI, 11434 pour l’API Ollama).

Pourquoi ces choix plutôt que d’autres ?

Ollama vs solutions cloud : Ollama permet de faire tourner des modèles open source localement, avec optimisation Apple Silicon. C’est le seul runtime qui exploite vraiment le Neural Engine du Mac M4 Pro. J’ai testé llama.cpp avant, mais les performances étaient nettement inférieures.

OpenWebUI vs interface custom : J’aurais pu coder une interface from scratch, mais OpenWebUI gère déjà l’authentification multi-utilisateurs, l’historique des conversations, et l’intégration RAG native avec ChromaDB. Pourquoi réinventer la roue ?

ChromaDB vs alternatives (Pinecone, Weaviate) : ChromaDB est léger, open source, et ne nécessite aucune configuration complexe. Pinecone est cloud-based donc hors sujet pour un projet local. Weaviate est plus lourd et surdimensionné pour un homelab.

Mac M4 Pro vs VM Proxmox : Le Neural Engine fait toute la différence. Sur Mistral 7B, j’obtiens 40 tokens/sec sur Mac contre environ 15-20 tokens/sec sur une VM x86 classique. Le projet est par ailleurs totalement portable : migration vers Linux possible en 30 minutes via Docker Compose.


Mise en œuvre

Étape 1 : Stack de base (Ollama + OpenWebUI)

La première étape a consisté à monter la stack Docker minimale : Ollama pour le runtime LLM, OpenWebUI pour l’interface.

Le fichier docker-compose.yml définit les deux services avec leurs dépendances. J’ai mappé le port OpenWebUI sur 3001 au lieu de 3000 (port par défaut) car j’ai déjà Grafana qui tourne sur ce port.

1
2
3
4
5
6
openwebui:
  image: ghcr.io/open-webui/open-webui:main
  ports:
    - "3001:8080"  # Port modifié pour éviter conflit avec Grafana
  environment:
    - OLLAMA_BASE_URL=http://ollama:11434

Le téléchargement de Mistral 7B (environ 4 Go) a pris une dizaine de minutes. Première déception : j’ai essayé Llama 3.3 70B, mais avec ses 42 Go en RAM, le Mac saturait complètement. Retour à Mistral 7B, qui tient largement dans les 64 Go disponibles.

Difficultés rencontrées :

Erreur 401 lors du premier accès au panneau admin après création du compte. Le container OpenWebUI était dans un état incohérent. Un simple docker restart openwebui a résolu le problème. J’ai appris que les containers peuvent avoir des états de session persistants qui survivent aux redémarrages normaux.

Étape 2 : Ajout de ChromaDB et configuration RAG

L’ajout de ChromaDB au docker-compose.yml était simple en soi, mais j’ai passé un moment à comprendre comment OpenWebUI gère les collections.

Problème : les collections créées via script Python (avec des noms explicites comme homelab_docs) n’apparaissaient pas dans OpenWebUI. Et inversement, les collections créées via l’interface web avaient des noms en UUID incompréhensibles.

Explication : OpenWebUI utilise deux couches. Une base SQLite interne qui stocke les métadonnées (noms lisibles des “Knowledge Bases”), et ChromaDB qui stocke les vecteurs avec des UUID générés automatiquement. Le mapping entre les deux est géré par OpenWebUI. Résultat : impossible d’accéder directement aux collections OpenWebUI depuis un script externe.

Solution : maintenir deux collections séparées. Une pour les tests manuels via OpenWebUI, une autre (homelab_docs) pour l’automatisation via scripts Python. Pas élégant, mais fonctionnel.

1
2
3
4
5
6
7
# Inspection des collections existantes
import chromadb
client = chromadb.HttpClient(host="localhost", port=8000)

for coll in client.list_collections():
    print(f"Collection : {coll.name}")
    # Les collections OpenWebUI ont des noms type "file-9e023747-0d4d-4a0d-aa68-ba34801e4367"

Étape 3 : Script d’ingestion automatique

L’objectif était de scanner un dossier de documentation et d’indexer automatiquement les nouveaux fichiers dans ChromaDB.

Le principe du RAG repose sur le chunking : découper les documents en morceaux de 500 caractères environ, avec un chevauchement de 50 caractères pour éviter de couper des phrases importantes. Chaque chunk est ensuite transformé en vecteur (embedding) par ChromaDB.

J’ai testé plusieurs tailles de chunks (300, 500, 800 caractères). 500 donnait le meilleur équilibre : assez de contexte pour que le modèle comprenne, mais pas trop pour éviter de diluer l’information pertinente.

Le script scan le dossier, détecte les nouveaux fichiers, les découpe, et les ingère dans ChromaDB. Ajout d’un cron job pour exécution nocturne automatique.

Difficultés rencontrées :

Temps de réponse incohérents au début : parfois 5 secondes, parfois 2 minutes pour la même question. Cause identifiée : le paramètre “Top K” dans OpenWebUI était trop élevé (10 chunks récupérés au lieu de 3). Résultat : trop de contexte envoyé à Ollama, qui met du temps à traiter. Réduction à Top K = 3, temps stabilisé à 10-15 secondes.


Résultat final

Le système fonctionne de bout en bout. Je peux poser des questions en langage naturel sur mes documents, et l’IA répond en se basant sur le contenu indexé.

Exemple concret :

Sans RAG activé :

Question : “Quels ports utilise mon homelab ?”
Réponse : “Les homelabs utilisent généralement les ports 80, 443, 8080 pour les services web…”

Avec RAG activé (Knowledge “Logs Homelab”) :

Question : “Quels ports utilise mon homelab ?”
Réponse : “Selon ta documentation, ton homelab utilise Grafana sur le port 3000, OpenWebUI sur 3001, Ollama API sur 11434, et ChromaDB sur 8000.”

La différence est significative. L’IA ne devine plus, elle se base sur mes fichiers.

Interface OpenWebUI en action avec RAG activé

Métriques concrètes :

  • Temps de réponse moyen avec RAG : 10-15 secondes
  • Tokens générés par seconde : ~40 (Mistral 7B sur Mac M4 Pro)
  • Documents indexés actuellement : ~10 fichiers, 50 chunks
  • RAM utilisée par Ollama : ~8 Go (Mistral 7B chargé en mémoire)

Cas d’usage réels :

  • Retrouver rapidement une commande Docker oubliée sans chercher dans 15 fichiers README
  • Poser des questions sur mes configurations pfSense sans relire toute la doc
  • Analyser les logs collectés par Loki (prochaine étape : intégration automatique)

Ce que j’ai appris

Sur le plan technique

Architecture microservices : J’ai compris concrètement comment orchestrer plusieurs services qui communiquent entre eux. OpenWebUI appelle Ollama via réseau Docker, récupère des données de ChromaDB, tout ça de façon transparente.

Bases vectorielles et embeddings : Avant ce projet, les embeddings étaient un concept abstrait. Maintenant je comprends pourquoi on transforme du texte en vecteurs mathématiques : pour faire de la recherche sémantique. Deux phrases avec des mots différents mais un sens proche auront des vecteurs proches dans l’espace vectoriel.

Optimisation des LLM : J’ai appris que le modèle n’est qu’une partie du problème. La vraie valeur vient du contexte fourni (RAG), de la taille des chunks, du nombre de résultats récupérés. Un bon prompt engineering fait toute la différence.

Sur le plan méthodologique

Toujours tester avant d’automatiser : J’ai voulu scripter l’ingestion automatique dès le départ. Grosse erreur. J’ai perdu du temps à déboguer un script alors que le problème venait de ma compréhension du fonctionnement d’OpenWebUI. Depuis, je teste manuellement d’abord, je comprends, puis j’automatise.

La documentation technique n’est pas toujours à jour : La doc OpenWebUI mentionnait une intégration ChromaDB “simple”. Dans la réalité, il y a eu des changements d’architecture entre versions. J’ai dû consulter les issues GitHub et le code source pour comprendre le fonctionnement réel.

Erreurs à éviter

Ne pas sous-estimer la RAM nécessaire : J’ai perdu une journée à essayer de faire tourner Llama 3.3 70B (42 Go) sur un Mac avec 64 Go de RAM. Ça sature complètement le système. Rester sur des modèles 7B-13B pour un usage homelab.

Attention aux mappings de ports Docker : J’ai oublié que Grafana tournait déjà sur le port 3000. Résultat : conflit au démarrage d’OpenWebUI. Toujours vérifier les ports utilisés avant (lsof -i :PORT).

ChromaDB et collections OpenWebUI ne communiquent pas directement : Si vous créez une collection via script Python, elle ne sera pas visible dans l’interface OpenWebUI, et vice-versa. Prévoir deux workflows séparés ou utiliser uniquement l’API OpenWebUI.


Ressources et liens

Code source et documentation technique :

Ressources qui m’ont aidé :

Fichiers projet sur GitHub :

  • docker-compose.yml - stack complète
  • scripts/loki_to_chromadb_bis.py - script d’ingestion des logs Loki
  • scripts/ingest_docs.py - script d’ingestion générique de documents
  • README.md - guide complet d’installation et troubleshooting

Prochaines étapes

Court terme (en cours) :

Automatiser complètement l’ingestion des logs. J’ai déjà un script qui récupère les logs depuis Loki et les pousse dans ChromaDB. Prochaine étape : configurer un cron job pour exécution automatique deux fois par jour.

Moyen terme :

Tester d’autres modèles. Mistral 7B fonctionne bien, mais je veux évaluer Qwen 2.5 Coder 32B, spécialisé dans l’analyse technique et les logs. Il nécessite environ 24 Go de RAM, ce qui rentre dans les capacités du Mac.

Implémenter la génération automatique de rapports quotidiens. L’idée : un script qui interroge l’IA chaque matin avec un prompt type “Analyse les logs des dernières 24h et génère un rapport structuré”, puis envoie le résultat par email. Gain de temps potentiel énorme pour du monitoring SOC.

Long terme :

Migration test vers Proxmox. Le Mac M4 Pro fonctionne très bien, mais je veux comparer les performances ARM vs x86 sur une VM Linux. L’architecture Docker rend ça trivial : copier le docker-compose.yml, relancer la stack, benchmarker.

Intégration avec Wazuh SIEM. J’ai déjà Wazuh qui tourne sur Proxmox pour la centralisation des logs de sécurité. L’objectif serait de faire analyser automatiquement les alertes Wazuh par l’IA et de générer des rapports d’incidents en langage naturel pour un RSSI.


Tags : IA Docker RAG ChromaDB Ollama homelab RGPD LLM