Avancé 22 minDocker

Déployer un LLM en production avec Docker Compose

Faire tourner un LLM en local sur son poste, c'est dix minutes. Déployer un LLM en production avec Docker Compose pour une équipe — avec une URL en HTTPS, du monitoring, des sauvegardes et une vraie sécurité — c'est un autre travail. Ce guide assemble une stack complète (Ollama, Open WebUI, Qdrant, n8n, Traefik) dans un seul docker-compose.yml commenté, prêt à être posé sur un VPS ou un serveur on-premise.

Par Mohamed Meguedmi·Màj 2026-05-23·Testé sur Windows, macOS, Linux

#Pourquoi cette stack

L'objectif est de couvrir les quatre besoins concrets d'une PME ou d'une équipe technique qui adopte l'IA locale : un moteur d'inférence (Ollama), une interface utilisateur web (Open WebUI), une base vectorielle pour le RAG (Qdrant), et une couche d'automatisation no-code (n8n). On enveloppe le tout avec Traefik en reverse proxy, qui s'occupe du HTTPS via Let's Encrypt et du routage.

Chaque service tourne dans son conteneur isolé, avec ses volumes persistants. Vous pouvez en désactiver un sans casser les autres, mettre à jour un seul composant, ou répliquer la stack sur un environnement de staging en une commande.

i
Ce que ce guide n'est pas
Ce n'est pas un guide Kubernetes. Pour une équipe de 10–50 personnes sur un seul serveur (jusqu'à ~10 utilisateurs concurrents sur un GPU), Docker Compose suffit largement. Au-delà, ou pour de la haute dispo multi-nœuds, regardez vers k3s ou Nomad.

#Architecture cible

Traefik (port 80/443)
Reverse proxy + terminaison TLS automatique via Let's Encrypt. Tout le trafic externe passe par lui.
Open WebUI (interne)
Interface chat type ChatGPT, sert sur chat.votre-domaine.fr. Auth locale ou OIDC.
Ollama (interne)
Daemon d'inférence. N'est PAS exposé publiquement, accessible uniquement depuis le réseau Docker.
Qdrant (interne)
Base vectorielle pour le RAG. Stockage des embeddings de vos documents.
n8n
Automatisation no-code, accessible sur n8n.votre-domaine.fr.
Prometheus + Grafana
Optionnels, exposés en interne pour le monitoring GPU/CPU/RAM.

#Prérequis

Un serveur Linux
Ubuntu 22.04 LTS ou Debian 12, root SSH, 16 Go de RAM minimum, 200 Go de disque.
Un GPU NVIDIA recommandé
RTX 3090 24 Go ou RTX 4090 pour du 14B–32B confortable, ou RTX 4070 12 Go pour du 7B. Sans GPU, restez sur des modèles 3B en CPU.
Un nom de domaine
Avec accès au DNS pour créer des sous-domaines. Indispensable pour Let's Encrypt.
Docker Engine + Compose v2
Installation via le script officiel get.docker.com. Le plugin compose est inclus depuis 2022.
NVIDIA Container Toolkit
Si vous avez un GPU, c'est ce qui permet à Docker de l'exposer aux conteneurs.

#1. Préparer le serveur

On part d'un Ubuntu 22.04 frais. Trois choses à installer : Docker, le NVIDIA Container Toolkit (si GPU), et un firewall propre.

Installation Docker
curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
newgrp docker
docker compose version
NVIDIA Container Toolkit
distribution=$(. /etc/os-release; echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
  sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
  sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
Tester que Docker voit le GPU
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
Firewall UFW minimal
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp comment 'SSH'
sudo ufw allow 80/tcp comment 'HTTP - redir vers HTTPS'
sudo ufw allow 443/tcp comment 'HTTPS Traefik'
sudo ufw enable
!
N'exposez jamais Ollama directement
Le port 11434 d'Ollama n'a aucune authentification. Si vous l'ouvrez sur Internet, n'importe qui peut consommer votre GPU et exfiltrer vos modèles via l'API. Tout passe par Traefik avec auth.

#2. Le docker-compose.yml commenté

Créez un dossier de travail et le fichier principal. C'est le cœur du déploiement — chaque section est commentée pour que vous puissiez l'adapter sans tout casser.

Structure de dossiers
mkdir -p /opt/llm-stack/{traefik,ollama,open-webui,qdrant,n8n}
cd /opt/llm-stack
touch docker-compose.yml .env
.env (à compléter)
DOMAIN=votre-domaine.fr
[email protected]
N8N_BASIC_AUTH_USER=admin
N8N_BASIC_AUTH_PASSWORD=changez-moi-vraiment
TRAEFIK_DASHBOARD_AUTH=admin:$apr1$xxxxxxx  # generé avec htpasswd
docker-compose.yml
name: llm-stack

networks:
  proxy:      # réseau public exposé par Traefik
  internal:   # réseau privé Ollama <-> WebUI <-> Qdrant

volumes:
  ollama_data:
  open_webui_data:
  qdrant_data:
  n8n_data:
  traefik_certs:

services:

  # --- Traefik : reverse proxy + Let's Encrypt automatique ---
  traefik:
    image: traefik:v3.2
    restart: unless-stopped
    command:
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      - --entrypoints.websecure.address=:443
      - --certificatesresolvers.le.acme.email=${ACME_EMAIL}
      - --certificatesresolvers.le.acme.storage=/certs/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      - --api.dashboard=true
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik_certs:/certs
    networks: [proxy]
    labels:
      - traefik.enable=true
      - traefik.http.routers.dashboard.rule=Host(`traefik.${DOMAIN}`)
      - traefik.http.routers.dashboard.service=api@internal
      - traefik.http.routers.dashboard.entrypoints=websecure
      - traefik.http.routers.dashboard.tls.certresolver=le
      - traefik.http.routers.dashboard.middlewares=dashboard-auth
      - traefik.http.middlewares.dashboard-auth.basicauth.users=${TRAEFIK_DASHBOARD_AUTH}

  # --- Ollama : moteur d'inférence, JAMAIS exposé en public ---
  ollama:
    image: ollama/ollama:latest
    restart: unless-stopped
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - OLLAMA_HOST=0.0.0.0:11434
      - OLLAMA_KEEP_ALIVE=24h
      - OLLAMA_NUM_PARALLEL=2
    networks: [internal]
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: all
              capabilities: [gpu]

  # --- Open WebUI : interface chat, exposée en HTTPS ---
  open-webui:
    image: ghcr.io/open-webui/open-webui:main
    restart: unless-stopped
    depends_on: [ollama]
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - WEBUI_AUTH=true
      - ENABLE_SIGNUP=false
    volumes:
      - open_webui_data:/app/backend/data
    networks: [proxy, internal]
    labels:
      - traefik.enable=true
      - traefik.http.routers.chat.rule=Host(`chat.${DOMAIN}`)
      - traefik.http.routers.chat.entrypoints=websecure
      - traefik.http.routers.chat.tls.certresolver=le
      - traefik.http.services.chat.loadbalancer.server.port=8080

  # --- Qdrant : base vectorielle pour le RAG ---
  qdrant:
    image: qdrant/qdrant:latest
    restart: unless-stopped
    volumes:
      - qdrant_data:/qdrant/storage
    networks: [internal]

  # --- n8n : automatisation no-code ---
  n8n:
    image: docker.n8n.io/n8nio/n8n:latest
    restart: unless-stopped
    environment:
      - N8N_BASIC_AUTH_ACTIVE=true
      - N8N_BASIC_AUTH_USER=${N8N_BASIC_AUTH_USER}
      - N8N_BASIC_AUTH_PASSWORD=${N8N_BASIC_AUTH_PASSWORD}
      - N8N_HOST=n8n.${DOMAIN}
      - N8N_PROTOCOL=https
      - WEBHOOK_URL=https://n8n.${DOMAIN}/
    volumes:
      - n8n_data:/home/node/.n8n
    networks: [proxy, internal]
    labels:
      - traefik.enable=true
      - traefik.http.routers.n8n.rule=Host(`n8n.${DOMAIN}`)
      - traefik.http.routers.n8n.entrypoints=websecure
      - traefik.http.routers.n8n.tls.certresolver=le
      - traefik.http.services.n8n.loadbalancer.server.port=5678
Pourquoi deux réseaux
Le réseau internal n'a pas d'accès au reverse proxy. Ollama et Qdrant y sont seuls — aucun moyen pour un attaquant qui passerait par Traefik d'atteindre directement leur API. Open WebUI a un pied dans les deux : il reçoit le trafic public via proxy et parle à Ollama via internal.

#3. Configurer Traefik et le DNS

Avant le premier docker compose up, créez trois enregistrements DNS de type A pointant vers l'IP publique de votre serveur :

chat.votre-domaine.fr
L'interface utilisateur Open WebUI.
n8n.votre-domaine.fr
L'éditeur de workflows n8n.
traefik.votre-domaine.fr
Le dashboard Traefik (protégé par basic auth).

Générez le hash pour le basic auth du dashboard Traefik. Attention : dans le .env, doublez les $ (échappement docker-compose).

Hash basic auth
sudo apt install apache2-utils
htpasswd -nb admin VotreMotDePasse | sed -e 's/\$/\$\$/g'
!
Let's Encrypt et rate limits
Let's Encrypt limite à 50 certificats par domaine et par semaine. En phase de test, utilisez --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory pour pointer vers le serveur de staging. Vous changerez quand tout fonctionnera.

#4. Premier lancement et vérifications

  1. 01
    Démarrer la stack
    Depuis /opt/llm-stack, lancez docker compose up -d. Traefik va négocier les certificats Let's Encrypt en quelques secondes — surveillez les logs.
  2. 02
    Vérifier la santé des services
    docker compose ps doit afficher tous les services en running. Aucun ne doit redémarrer en boucle.
  3. 03
    Télécharger un premier modèle
    docker compose exec ollama ollama pull mistral:7b-instruct-q4_K_M (environ 4 Go). Pour du 14B : qwen2.5:14b-instruct-q4_K_M (≈9 Go de VRAM).
  4. 04
    Tester l'interface
    Ouvrez https://chat.votre-domaine.fr. Premier compte créé = administrateur. ENABLE_SIGNUP=false bloque toute inscription publique ensuite.
  5. 05
    Vérifier que le GPU est utilisé
    docker compose exec ollama ollama ps : la colonne PROCESSOR doit afficher 100% GPU.
Logs Traefik en direct
docker compose logs -f traefik | grep -i acme

#5. Durcir la sécurité

Une stack publique mal configurée se fait scanner dans l'heure. Voici les six règles non négociables pour un déploiement de LLM en production avec Docker Compose qui ne finira pas dans le shodan.io de quelqu'un.

Mots de passe forts et uniques
Générez vos secrets avec openssl rand -base64 32. N'utilisez jamais le mot de passe d'exemple du .env.
Désactiver l'inscription publique
ENABLE_SIGNUP=false sur Open WebUI. Sans ça, n'importe qui peut créer un compte et consommer votre GPU.
Headers de sécurité via middleware Traefik
Ajoutez un middleware secureHeaders avec HSTS, frame-deny, content-type-nosniff. Trois lignes, énormément d'impact.
Limiter les ressources par conteneur
Sous deploy.resources.limits, fixez memory et cpus. Empêche un service crashé de mettre tout le serveur à genoux.
Conteneurs en read-only quand possible
Ajoutez read_only: true sur Traefik et Qdrant. Tmpfs pour les répertoires d'écriture transitoires.
Mises à jour mensuelles
watchtower automatise les pulls, mais préférez un cron + git tag explicite pour rester maître des versions en prod.
Middleware headers de sécurité
# À ajouter sur le service open-webui en labels:
- traefik.http.routers.chat.middlewares=secure-headers
- traefik.http.middlewares.secure-headers.headers.stsSeconds=31536000
- traefik.http.middlewares.secure-headers.headers.stsIncludeSubdomains=true
- traefik.http.middlewares.secure-headers.headers.frameDeny=true
- traefik.http.middlewares.secure-headers.headers.contentTypeNosniff=true
- traefik.http.middlewares.secure-headers.headers.browserXssFilter=true
Fail2ban devant Traefik
Pour bloquer les bruteforce sur le basic auth (dashboard Traefik, n8n), installez fail2ban avec un filter qui parse les logs Traefik. 5 tentatives → 1 heure de ban IP. Configuration en 10 lignes.

#6. Monitoring Prometheus + Grafana

Trois métriques à surveiller absolument en production : la VRAM utilisée par Ollama, la latence des requêtes Open WebUI, et l'état des certificats Let's Encrypt (Traefik expose tout en /metrics).

Bloc à ajouter au docker-compose.yml
  prometheus:
    image: prom/prometheus:latest
    restart: unless-stopped
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus_data:/prometheus
    networks: [internal]

  nvidia-exporter:
    image: utkuozdemir/nvidia_gpu_exporter:latest
    restart: unless-stopped
    devices: [/dev/nvidiactl, /dev/nvidia0]
    volumes:
      - /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1:ro
      - /usr/bin/nvidia-smi:/usr/bin/nvidia-smi:ro
    networks: [internal]

  grafana:
    image: grafana/grafana:latest
    restart: unless-stopped
    volumes:
      - grafana_data:/var/lib/grafana
    networks: [proxy, internal]
    labels:
      - traefik.enable=true
      - traefik.http.routers.grafana.rule=Host(`grafana.${DOMAIN}`)
      - traefik.http.routers.grafana.entrypoints=websecure
      - traefik.http.routers.grafana.tls.certresolver=le
      - traefik.http.services.grafana.loadbalancer.server.port=3000
prometheus.yml
global:
  scrape_interval: 15s
scrape_configs:
  - job_name: 'traefik'
    static_configs:
      - targets: ['traefik:8080']
  - job_name: 'nvidia'
    static_configs:
      - targets: ['nvidia-exporter:9835']
  - job_name: 'qdrant'
    static_configs:
      - targets: ['qdrant:6333']

Côté Grafana, importez le dashboard ID 14574 (NVIDIA GPU Exporter) et le 17346 (Traefik v3). Vous avez une vision complète en 5 minutes : VRAM, températures GPU, taux de requêtes HTTP par sous-domaine, latences P95.

#7. Sauvegardes et mises à jour

Cinq volumes contiennent toutes vos données critiques : ollama_data (modèles), open_webui_data (comptes + conversations + RAG), qdrant_data (vecteurs), n8n_data (workflows), traefik_certs (certificats).

/usr/local/bin/backup-llm-stack.sh
#!/bin/bash
set -e
BACKUP_DIR=/var/backups/llm-stack
TS=$(date +%Y%m%d-%H%M)
mkdir -p "$BACKUP_DIR"

for vol in open_webui_data qdrant_data n8n_data traefik_certs; do
  docker run --rm \
    -v llm-stack_${vol}:/data \
    -v "$BACKUP_DIR":/backup \
    alpine tar czf "/backup/${vol}-${TS}.tar.gz" -C /data .
done

find "$BACKUP_DIR" -name '*.tar.gz' -mtime +30 -delete
Cron quotidien 3h du matin
sudo chmod +x /usr/local/bin/backup-llm-stack.sh
echo "0 3 * * * root /usr/local/bin/backup-llm-stack.sh" | sudo tee /etc/cron.d/llm-stack-backup
i
Pourquoi ollama_data n'est pas sauvegardé
Les modèles pèsent vite des dizaines de Go et sont re-téléchargeables avec ollama pull. Inutile de les inclure dans des backups quotidiens — sauvegardez plutôt la liste : docker compose exec ollama ollama list > models.txt.

Pour les mises à jour, jouez la sécurité : épinglez les versions (image: ollama/ollama:0.5.4 plutôt que :latest), faites un snapshot du VPS avant chaque update, et testez sur staging d'abord.

Mise à jour propre
cd /opt/llm-stack
docker compose pull
docker compose up -d
docker image prune -f

#Dépannage

Traefik ne génère pas de certificat
Vérifiez que les ports 80 et 443 sont ouverts ET que le DNS pointe bien vers le serveur. docker compose logs traefik | grep -i error révèle 90% des cas (DNS non propagé, rate limit, challenge échoué).
Open WebUI affiche 'No models found'
Vous n'avez pas encore pull de modèle dans Ollama. docker compose exec ollama ollama pull mistral. Vérifiez aussi OLLAMA_BASE_URL=http://ollama:11434 (nom du service, pas localhost).
GPU non détecté dans le conteneur
nvidia-container-toolkit pas installé ou Docker pas redémarré après config. Testez avec docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi.
n8n redirige en boucle vers HTTP
WEBHOOK_URL et N8N_PROTOCOL=https doivent être définis. Sans ça, n8n croit qu'il est en HTTP et le navigateur s'embrouille.
Grafana inaccessible derrière Traefik
Ajoutez GF_SERVER_ROOT_URL=https://grafana.${DOMAIN} et GF_SERVER_SERVE_FROM_SUB_PATH=false dans son environment.
Latence qui explose à plusieurs utilisateurs
OLLAMA_NUM_PARALLEL trop élevé pour la VRAM. Baissez à 1 ou 2 selon votre carte. Surveillez ollama ps pendant la charge.

#Pour aller plus loin

Votre stack tourne, monitorée et sauvegardée. Trois directions naturelles pour la professionnaliser davantage :

Connecter le RAG à vos documents
Le guide RAG avec ChromaDB et Mistral s'adapte sans difficulté à Qdrant — même logique d'embeddings et de chunking, base vectorielle différente.
Automatiser des workflows métier
Le guide Automatiser avec n8n et Ollama décrit dix recettes concrètes (traduction d'emails, classification de leads, résumé RSS) qui exploitent directement votre stack.
Couvrir la conformité
Le guide LLM local et RGPD détaille les obligations légales (registre, durée de rétention, droits des utilisateurs) à documenter pour un déploiement en entreprise.
Ce guide vous a aidé ?

Un retour, une erreur, une précision ? Faites-nous signe, ça améliore le guide pour tout le monde.