Avanzato 22 minDocker

Distribuire un LLM in produzione con Docker Compose

Per far girare un LLM in locale sul proprio computer bastano dieci minuti. Distribuire un LLM in produzione con Docker Compose per un team — con un URL in HTTPS, monitoraggio, backup e una sicurezza effettiva — è un altro lavoro. Questa guida riunisce uno stack completo (Ollama, Open WebUI, Qdrant, n8n, Traefik) in un unico docker-compose.yml commentato, pronto per essere installato su un VPS o un server on-premise.

Di Mohamed Meguedmi·Agg. 2026-08-27·Testato su Windows, macOS e Linux

#Perché questo stack

L'obiettivo è soddisfare le quattro esigenze concrete di una PMI o di un team tecnico che adotta l'IA locale: un motore di inferenza (Ollama), un'interfaccia utente web (Open WebUI), un database vettoriale per il RAG (Qdrant) e un livello di automazione no-code (n8n). Il tutto viene gestito tramite Traefik come reverse proxy, che si occupa di HTTPS tramite Let's Encrypt e del routing.

Ogni servizio viene eseguito nel proprio container isolato, con i suoi volumi persistenti. Puoi disattivarne uno senza compromettere gli altri, aggiornare un singolo componente o replicare lo stack in un ambiente di staging con un solo comando.

i
Ciò che questa guida non è
Questa non è una guida a Kubernetes. Per un team di 10–50 persone su un solo server (fino a circa 10 utenti simultanei su una GPU), Docker Compose è più che sufficiente. Oltre questa scala, o per l'alta disponibilità su più nodi, considera k3s o Nomad.

#Architettura target

Il kit Agenti Locali

Agenti che agiscono sulla tua macchina: Cline agentico, MCP, n8n + Ollama, automazioni locali.

  • Spazio online a vita
  • PDF + file
  • Aggiornamenti a vita
Traefik (porte 80/443)
Reverse proxy + terminazione TLS automatica tramite Let's Encrypt. Tutto il traffico esterno passa attraverso di esso.
Open WebUI (interno)
Interfaccia chat tipo ChatGPT, disponibile su chat.votre-domaine.fr. Autenticazione locale o OIDC.
Ollama (interno)
Daemon di inferenza. NON è esposto pubblicamente, accessibile soltanto dal network Docker.
Qdrant (interno)
Database vettoriale per il RAG. Archiviazione degli embedding dei tuoi documenti.
n8n
Automazione senza codice, accessibile su n8n.votre-domaine.fr.
Prometheus + Grafana
Opzionali, esposti internamente per il monitoraggio GPU/CPU/RAM.

#Prerequisiti

Un server Linux
Ubuntu 22.04 LTS o Debian 12, accesso SSH come root, almeno 16 GB di RAM, 200 GB di spazio su disco.
Una GPU NVIDIA consigliata
RTX 3090 24 GB o RTX 4090 per eseguire comodamente modelli da 14B a 32B, oppure RTX 4070 12 GB per modelli da 7B. Senza GPU, resta su modelli da 3B eseguiti sulla CPU.
Un nome di dominio
Con accesso al DNS per creare sottodomini. Indispensabile per Let's Encrypt.
Docker Engine + Compose v2
Installazione tramite lo script ufficiale get.docker.com. Il plugin compose è incluso dal 2022.
NVIDIA Container Toolkit
Se hai una GPU, è questo che permette a Docker di esporla ai container.

#1. Preparare il server

Partiamo da un'installazione nuova di Ubuntu 22.04. Tre cose da installare: Docker, il NVIDIA Container Toolkit (se si usa una GPU) e un firewall configurato correttamente.

Installazione 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
Verificare che Docker veda la GPU
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
Firewall UFW minimale
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
!
Non esporre mai Ollama direttamente
La porta 11434 di Ollama non prevede alcuna autenticazione. Se la esponi su Internet, chiunque può sfruttare la tua GPU ed esfiltrare i tuoi modelli tramite l'API. Tutto passa attraverso Traefik con autenticazione.

#2. docker-compose.yml commentato

Crea una cartella di lavoro e il file principale. È il cuore del deployment — ogni sezione è commentata in modo che tu possa adattarla senza compromettere tutto.

Struttura delle cartelle
mkdir -p /opt/llm-stack/{traefik,ollama,open-webui,qdrant,n8n}
cd /opt/llm-stack
touch docker-compose.yml .env
.env (da completare)
DOMAIN=votre-domaine.fr
ACME_EMAIL=admin@votre-domaine.fr
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
→
Perché due reti
La rete internal non ha accesso al reverse proxy. Ollama e Qdrant sono gli unici presenti su questa rete — un attaccante che passasse attraverso Traefik non avrebbe modo di raggiungere direttamente la loro API. Open WebUI è collegato a entrambe le reti: riceve il traffico pubblico tramite il proxy e comunica con Ollama tramite internal.

#3. Configurare Traefik e il DNS

Prima di eseguire docker compose up per la prima volta, crea tre record DNS di tipo A che puntino all'IP pubblico del tuo server:

chat.votre-domaine.fr
L'interfaccia utente Open WebUI.
n8n.votre-domaine.fr
L'editor di workflow n8n.
traefik.votre-domaine.fr
La dashboard di Traefik (protetta da autenticazione di base).

Genera l'hash per l'autenticazione HTTP Basic della dashboard Traefik. Attenzione: nel file .env, raddoppia i $ (escape per docker-compose).

Hash per l'autenticazione di base
sudo apt install apache2-utils
htpasswd -nb admin VotreMotDePasse | sed -e 's/\$/\$\$/g'
!
Let's Encrypt e limiti di richieste
Let's Encrypt impone un limite di 50 certificati per dominio a settimana. In fase di test, usa --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory per puntare al server di staging. Cambierai server quando tutto funzionerà.

#4. Primo avvio e verifiche

  1. 01
    Avviare lo stack
    Da /opt/llm-stack, esegui docker compose up -d. Traefik negozierà i certificati Let's Encrypt in pochi secondi — monitora i log.
  2. 02
    Verifica lo stato dei servizi
    docker compose ps doit afficher tous les services en running. Aucun ne doit redémarrer en boucle.
  3. 03
    Scaricare un primo modello
    docker compose exec ollama ollama pull qwen3.5:9b (6,6 GB, 256k di contesto, il modello tuttofare da 8 GB di riferimento nel 2026). Per un modello più potente su una GPU da 24 GB: qwen3.8:27b (≈18 GB di VRAM, 262k di contesto, licenza Apache 2.0).
  4. 04
    Testare l'interfaccia
    Apri https://chat.votre-domaine.fr. Il primo account creato = amministratore. ENABLE_SIGNUP=false blocca ogni iscrizione pubblica successivamente.
  5. 05
    Verificare che la GPU sia utilizzata
    docker compose exec ollama ollama ps : la colonne PROCESSOR doit afficher 100% GPU.
Log di Traefik in tempo reale
docker compose logs -f traefik | grep -i acme

#5. Rafforzare la sicurezza

Uno stack pubblico mal configurato viene scansionato nel giro di un'ora. Ecco le sei regole non negoziabili per un deployment di LLM in produzione con Docker Compose che non finirà nei risultati di ricerca di qualcuno su shodan.io.

Password forti e uniche
Genera i tuoi segreti con openssl rand -base64 32. Mai usare la password di esempio del .env.
Disabilitare l'iscrizione pubblica
ENABLE_SIGNUP=false su Open WebUI. Altrimenti, chiunque può creare un account e consumare le risorse della tua GPU.
Intestazioni di sicurezza tramite middleware Traefik
Aggiungi un middleware secureHeaders con HSTS, frame-deny, content-type-nosniff. Tre righe, un impatto enorme.
Limitare le risorse per contenitore
Sotto deploy.resources.limits, imposta memory e cpus. Evita che un servizio crashato metta tutto il server in crisi.
Contenitori in sola lettura quando possibile
Aggiungi read_only: true su Traefik e Qdrant. Tmpfs per le directory di scrittura temporanee.
Aggiornamenti mensili
watchtower automatizza i pull, ma preferisci un cron + git tag esplicito per mantenere il controllo delle versioni in produzione.
Middleware per gli header di sicurezza
# À 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 davanti a Traefik
Per bloccare gli attacchi di brute force sul basic auth (dashboard Traefik, n8n), installa fail2ban con un filtro che analizza i log di Traefik. 5 tentativi → 1 ora di blocco IP. Configurazione in 10 righe.

#6. Monitoraggio Prometheus + Grafana

Tre metriche da monitorare assolutamente in produzione: la VRAM utilizzata da Ollama, la latenza delle richieste Open WebUI e lo stato dei certificati Let's Encrypt (Traefik espone tutto su /metrics).

Blocco da aggiungere al 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']

In Grafana, importa il dashboard con ID 14574 (NVIDIA GPU Exporter) e quello con ID 17346 (Traefik v3). In 5 minuti hai una panoramica completa: VRAM, temperature della GPU, frequenza delle richieste HTTP per sottodominio, latenze P95.

#7. Backup e aggiornamenti

Cinque volumi contengono tutti i tuoi dati critici: ollama_data (modelli), open_webui_data (account + conversazioni + RAG), qdrant_data (vettori), n8n_data (workflow), traefik_certs (certificati).

/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 quotidiano alle 3 del mattino
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
Perché non viene eseguito il backup di ollama_data
I modelli arrivano rapidamente a occupare decine di GB e possono essere scaricati di nuovo con ollama pull. Non è necessario includerli nei backup giornalieri — salva invece l'elenco: docker compose exec ollama ollama list > models.txt.

Per gli aggiornamenti, vai sul sicuro: fissa le versioni (image: ollama/ollama:0.5.4 anziché :latest), crea uno snapshot del VPS prima di ogni aggiornamento e testa prima nell'ambiente di staging.

Aggiornamento pulito
cd /opt/llm-stack
docker compose pull
docker compose up -d
docker image prune -f

#Risoluzione dei problemi

Traefik non genera un certificato
Verifica che le porte 80 e 443 siano aperte E che il DNS punti correttamente al server. docker compose logs traefik | grep -i error rivela il 90% dei casi (DNS non propagato, rate limit, challenge fallito).
Open WebUI mostra 'No models found'
Non hai ancora effettuato il pull del modello in Ollama. docker compose exec ollama ollama pull qwen3.5:9b. Verifica anche OLLAMA_BASE_URL=http://ollama:11434 (nome del servizio, non localhost).
GPU non rilevata nel container
nvidia-container-toolkit non installato o Docker non riavviato dopo la configurazione. Prova con docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi.
n8n reindirizza continuamente verso HTTP
WEBHOOK_URL e N8N_PROTOCOL=https devono essere definiti. Altrimenti, n8n crede di essere in HTTP e il browser si confonde.
Grafana non accessibile dietro Traefik
Aggiungi GF_SERVER_ROOT_URL=https://grafana.${DOMAIN} e GF_SERVER_SERVE_FROM_SUB_PATH=false nella sua sezione environment.
Latenza che esplode con più utenti
OLLAMA_NUM_PARALLEL troppo alto per la VRAM. Riducilo a 1 o 2 in base alla tua scheda. Controlla ollama ps sotto carico.

#Per approfondire

Il tuo stack è in funzione, viene monitorato e ne vengono eseguiti i backup. Tre direzioni naturali per renderlo ancora più professionale:

Collegare il RAG ai tuoi documenti
La guida al RAG con ChromaDB e Mistral si adatta senza difficoltà a Qdrant: stessa logica di embedding e chunking, database vettoriale diverso.
Automatizzare workflow aziendali
La guida Automatizza con n8n e Ollama descrive dieci ricette concrete (traduzione di email, classificazione di lead, riepilogo RSS) che sfruttano direttamente il tuo stack.
Gestire la conformità
La guida su LLM locali e GDPR descrive in dettaglio gli obblighi legali (registro, periodo di conservazione, diritti degli utenti) da documentare per un'implementazione in azienda.
Questa guida ti è stata utile?

Un feedback, un errore, una precisazione? Facci sapere, così la guida migliora per tutti.