Avanzado 22 minDocker

Desplegar un LLM en producción con Docker Compose

Ejecutar un LLM localmente en tu computadora lleva diez minutos. Desplegar un LLM en producción con Docker Compose para un equipo —con una URL en HTTPS, monitoreo, respaldos y verdadera seguridad— es otro trabajo. Esta guía reúne una pila completa (Ollama, Open WebUI, Qdrant, n8n, Traefik) en un único archivo docker-compose.yml comentado, listo para instalarse en un VPS o un servidor on-premise.

Por Mohamed Meguedmi·Actualización 2026-08-27·Probado en Windows, macOS y Linux

#¿Por qué esta pila de software?

El objetivo es cubrir las cuatro necesidades concretas de una pyme o de un equipo técnico que adopte la IA local: un motor de inferencia (Ollama), una interfaz de usuario web (Open WebUI), una base de datos vectorial para el RAG (Qdrant) y una capa de automatización sin código (n8n). Todo ello se integra con Traefik como proxy inverso, que se encarga del HTTPS mediante Let's Encrypt y del enrutamiento.

Cada servicio funciona en su contenedor aislado, con sus volúmenes persistentes. Puedes desactivar uno sin romper los demás, actualizar un solo componente o replicar la pila en un entorno de staging con un solo comando.

i
Lo que esta guía no es
Esta no es una guía de Kubernetes. Para un equipo de 10–50 personas en un solo servidor (hasta unos 10 usuarios simultáneos en una GPU), Docker Compose es más que suficiente. Para necesidades mayores o para alta disponibilidad con varios nodos, considera k3s o Nomad.

#Arquitectura objetivo

El kit de Agentes Locales

Agentes que actúan en tu máquina: Cline agéntico, MCP, n8n + Ollama, automatizaciones locales.

  • Espacio en línea de por vida
  • PDF + archivos
  • Reembolsado 30 j
Traefik (puertos 80/443)
Proxy inverso + terminación TLS automática mediante Let's Encrypt. Todo el tráfico externo pasa por él.
Open WebUI (interno)
Interfaz de chat tipo ChatGPT, disponible en chat.votre-domaine.fr. Autenticación local o OIDC.
Ollama (interno)
Daemon de inferencia. NO está expuesto públicamente, accesible únicamente desde la red Docker.
Qdrant (interno)
Base vectorial para el RAG. Almacenamiento de los embeddings de tus documentos.
n8n
Automatización sin código, disponible en n8n.votre-domaine.fr.
Prometheus + Grafana
Opcionales, expuestos internamente para el monitoreo de GPU/CPU/RAM.

#Prerrequisitos

Un servidor Linux
Ubuntu 22.04 LTS o Debian 12, acceso SSH como root, 16 GB de RAM mínimo, 200 GB de disco.
Un GPU NVIDIA recomendado
RTX 3090 de 24 GB o RTX 4090 para usar cómodamente modelos de 14B–32B, o RTX 4070 de 12 GB para modelos de 7B. Sin GPU, limítate a modelos de 3B en CPU.
Un nombre de dominio
Con acceso al DNS para crear subdominios. Indispensable para Let's Encrypt.
Docker Engine + Compose v2
Instalación a través del script oficial get.docker.com. El plugin compose está incluido desde 2022.
NVIDIA Container Toolkit
Si tienes una GPU, es lo que permite a Docker ponerla a disposición de los contenedores.

#1. Preparar el servidor

Se parte de una instalación nueva de Ubuntu 22.04. Tres cosas que hay que instalar: Docker, el NVIDIA Container Toolkit (si hay GPU) y un firewall bien configurado.

Instalación 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
Probar que Docker ve el GPU
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
Firewall UFW mínimo
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
!
Nunca expongas directamente Ollama
El puerto 11434 de Ollama no tiene autenticación. Si lo abres en Internet, cualquiera puede usar tu GPU y extraer tus modelos a través de la API. Todo pasa por Traefik con autenticación.

#2. docker-compose.yml comentado

Crea un directorio de trabajo y el archivo principal. Es el corazón del despliegue — cada sección está comentada para que puedas adaptarlo sin romper todo.

Estructura de carpetas
mkdir -p /opt/llm-stack/{traefik,ollama,open-webui,qdrant,n8n}
cd /opt/llm-stack
touch docker-compose.yml .env
.env (por completar)
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
→
¿Por qué dos redes?
La red internal no tiene acceso al proxy inverso. Ollama y Qdrant están solos en ella: un atacante que pasara por Traefik no tendría forma de acceder directamente a sus API. Open WebUI está conectado a ambas redes: recibe el tráfico público a través de proxy y se comunica con Ollama a través de internal.

#3. Configurar Traefik y el DNS

Antes del primer docker compose up, crea tres registros DNS de tipo A que apunten a la IP pública de tu servidor:

chat.votre-domaine.fr
La interfaz de usuario Open WebUI.
n8n.votre-domaine.fr
El editor de flujos n8n.
traefik.votre-domaine.fr
El panel de control de Traefik (protegido por autenticación básica).

Genera el hash para la autenticación básica del panel de Traefik. Atención: en el .env, duplica los $ (escape de docker-compose).

Hash de autenticación básica
sudo apt install apache2-utils
htpasswd -nb admin VotreMotDePasse | sed -e 's/\$/\$\$/g'
!
Let's Encrypt y límites de uso
Let's Encrypt limita a 50 certificados por dominio y por semana. En fase de prueba, utiliza --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory para apuntar al servidor de pruebas. Lo cambiarás cuando todo funcione.

#4. Primera puesta en marcha y comprobaciones

  1. 01
    Iniciar la pila
    Desde /opt/llm-stack, ejecuta docker compose up -d. Traefik negociará los certificados Let's Encrypt en unos segundos; vigila los logs.
  2. 02
    Verificar el estado de los servicios
    docker compose ps doit afficher tous les services en running. Aucun ne doit redémarrer en boucle.
  3. 03
    Descargar un primer modelo
    docker compose exec ollama ollama pull qwen3.5:9b (6,6 Go, 256k de contexte, le polyvalent 8 Go de référence en 2026). Pour un modèle plus costaud sur un GPU 24 Go : qwen3.8:27b (≈18 Go de VRAM, 262k de contexte, licence Apache 2.0).
  4. 04
    Probar la interfaz
    Abre https://chat.votre-domaine.fr. La primera cuenta creada = administrador. ENABLE_SIGNUP=false bloquea cualquier registro público posterior.
  5. 05
    Verificar que el GPU esté en uso
    docker compose exec ollama ollama ps : la colonne PROCESSOR doit afficher 100% GPU.
Logs de Traefik en directo
docker compose logs -f traefik | grep -i acme

#5. Fortalecer la seguridad

Un conjunto de servicios expuesto públicamente y mal configurado recibe escaneos en menos de una hora. Estas son las seis reglas no negociables para un despliegue de LLM en producción con Docker Compose que no acabará en el radar de alguien en shodan.io.

Contraseñas fuertes y únicas
Genera tus secretos con openssl rand -base64 32. Nunca uses la contraseña de ejemplo del .env.
Desactivar el registro público
ENABLE_SIGNUP=false en Open WebUI. Sin eso, cualquiera puede crear una cuenta y consumir recursos de tu GPU.
Encabezados de seguridad mediante middleware Traefik
Agrega un middleware secureHeaders con HSTS, frame-deny, content-type-nosniff. Tres líneas, mucho impacto.
Limitar los recursos por contenedor
En deploy.resources.limits, establece memory y cpus. Esto evita que un servicio que haya fallado deje todo el servidor fuera de servicio.
Contenedores en solo lectura cuando sea posible
Agrega read_only: true en Traefik y Qdrant. Tmpfs para los directorios de escritura temporales.
Actualizaciones mensuales
watchtower automatiza los pulls, pero es preferible que uses un cron + una etiqueta git explícita para mantener el control de las versiones en producción.
Middleware para encabezados de seguridad
# À 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 delante de Traefik
Para bloquear los ataques de fuerza bruta contra la autenticación básica (panel de Traefik, n8n), instala fail2ban con un filtro que analice los logs de Traefik. 5 intentos → 1 hora de bloqueo de IP. Configuración en 10 líneas.

#6. Monitoreo con Prometheus + Grafana

Tres métricas que es imprescindible supervisar en producción: la VRAM utilizada por Ollama, la latencia de las solicitudes de Open WebUI y el estado de los certificados Let's Encrypt (Traefik expone todo en /metrics).

Bloque para añadir 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']

En Grafana, importa el dashboard con ID 14574 (NVIDIA GPU Exporter) y el 17346 (Traefik v3). Tienes una visión completa en 5 minutos: VRAM, temperaturas de la GPU, tasas de solicitudes HTTP por subdominio y latencias P95.

#7. Copias de seguridad y actualizaciones

Cinco volúmenes contienen todos tus datos críticos: ollama_data (modelos), open_webui_data (cuentas + conversaciones + RAG), qdrant_data (vectores), n8n_data (flujos de trabajo), traefik_certs (certificados).

/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 diario a las 3 de la madrugada
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
Por qué no se hace una copia de seguridad de ollama_data
Los modelos pueden llegar fácilmente a ocupar decenas de GB y se pueden volver a descargar con ollama pull. No es necesario incluirlos en copias de seguridad diarias — guarda en su lugar la lista: docker compose exec ollama ollama list > models.txt.

Para las actualizaciones, ve sobre seguro: fija las versiones (image: ollama/ollama:0.5.4 en lugar de :latest), haz una instantánea del VPS antes de cada actualización y prueba primero en un entorno de staging.

Actualización limpia
cd /opt/llm-stack
docker compose pull
docker compose up -d
docker image prune -f

#Solución de problemas

Traefik no genera certificado
Comprueba que los puertos 80 y 443 estén abiertos Y que el DNS apunte correctamente al servidor. docker compose logs traefik | grep -i error revela el 90 % de los casos (DNS no propagado, límite de solicitudes, fallo del desafío).
Open WebUI muestra 'No models found'
Aún no has descargado ningún modelo en Ollama. docker compose exec ollama ollama pull qwen3.5:9b. Revisa también OLLAMA_BASE_URL=http://ollama:11434 (nombre del servicio, no localhost).
GPU no detectado en el contenedor
nvidia-container-toolkit no instalado o Docker no reiniciado tras la configuración. Prueba con docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi.
n8n redirige en bucle hacia HTTP
WEBHOOK_URL y N8N_PROTOCOL=https deben estar definidos. De lo contrario, n8n cree que está en HTTP y el navegador se confunde.
Grafana no accesible detrás de Traefik
Agrega GF_SERVER_ROOT_URL=https://grafana.${DOMAIN} y GF_SERVER_SERVE_FROM_SUB_PATH=false a su sección environment.
Latencia que se dispara con varios usuarios
OLLAMA_NUM_PARALLEL demasiado alto para la VRAM. Reduce su valor a 1 o 2 según tu tarjeta. Supervisa ollama ps mientras el sistema está bajo carga.

#Para ir más allá

Tu stack está funcionando, con monitorización y copias de seguridad. Tres vías naturales para profesionalizarla aún más:

Conectar el RAG a tus documentos
La guía RAG con ChromaDB y Mistral se adapta sin dificultad a Qdrant — misma lógica de embeddings y chunking, base vectorial diferente.
Automatizar workflows empresariales
La guía Automatizar con n8n y Ollama describe diez recetas concretas (traducción de correos, clasificación de leads, resumen de RSS) que aprovechan directamente tu stack.
Abordar el cumplimiento normativo
La guía del LLM local y el RGPD detalla las obligaciones legales (registro, duración de conservación, derechos de los usuarios) que deben documentarse para un despliegue en empresa.
¿Esta guía te ha ayudado?

¿Un comentario, un error, una precisión? Avísanos, eso mejora la guía para todos.