TL;DR
Un droplet de Digital Ocean de 40 USD al mes (4 vCPU, 8 GB RAM, 160 GB SSD) alcanza para correr un stack de IA completo y productivo: Qdrant como vector DB, Langfuse para observabilidad de LLMs, LiteLLM Proxy como gateway unificado, Redis 8 para caché semántico y memoria de agentes, Ollama para modelos locales, Nginx + Certbot para TLS, Prometheus + Grafana para métricas y Restic para backups a Spaces. En este artículo publicamos el docker-compose.yml completo, los perfiles de recursos por contenedor, la configuración de red interna y el plan de restauración ante caída del droplet.
Si tu cliente no puede o no quiere mandar datos a OpenAI y tu presupuesto mensual de infraestructura IA es menor a 100 USD, este es el punto de partida.
Por qué self-hosted en 2026 sigue teniendo sentido
El discurso dominante es que "todo está en la nube administrada". En la práctica, tres fuerzas empujan hacia el self-host:
- Data residency regulatoria. El AI Act europeo (exigible desde el 2 de agosto 2026) y los marcos sectoriales de salud y finanzas obligan a mantener ciertos datos dentro de una jurisdicción específica. Mandarlos a la API de un hyperscaler suele significar firmar DPAs pesados y auditorías anuales.
- Costos marginales de inferencia. Un flujo de agente con 50 llamadas LLM por sesión y caché semántico al 45% baja el costo por sesión a la mitad cuando Redis vive en el mismo servidor que el orquestador. La latencia de red intrarregión también cae de 30-80 ms a <2 ms.
- Vendor lock-in. Langfuse, Qdrant, LiteLLM, Redis, Ollama y Mastra son todas piezas Apache 2.0 o MIT que puedes mover de proveedor sin reescribir código.
Lo que este artículo no resuelve: entrenar o servir LLMs grandes (más de 13B parámetros) con latencia baja — para eso sigue siendo mejor APIs gestionadas o GPUs dedicadas.
Arquitectura
┌──────────────────────────────────────────────┐
│ Droplet s-4vcpu-8gb (USD 40/mes) │
│ │
Cliente (HTTPS) ──► Nginx ──► [ LiteLLM Proxy :4000 ] │
│ │ │
│ ├──► Anthropic / OpenAI / Gemini │
│ │ (rate limit + fallback) │
│ │ │
│ └──► Ollama :11434 (Llama 3.3 8B) │
│ │
│ [ Qdrant :6333 ] [ Redis :6379 ] │
│ │
│ [ Langfuse Web :3000 ] │
│ └──► Langfuse Worker │
│ └──► Postgres :5432 │
│ └──► ClickHouse :8123 │
│ │
│ [ Prometheus :9090 ] [ Grafana :3001 ] │
│ │
│ Restic daemon ──► DO Spaces (S3) │
└──────────────────────────────────────────────┘
Red interna sobre bridge de Docker Compose; sólo Nginx y SSH exponen puertos al exterior.
El docker-compose.yml completo
Este fichero vive en /opt/numoru-ai/docker-compose.yml. Todas las credenciales se leen desde .env.
version: "3.9"
networks:
core:
driver: bridge
volumes:
qdrant_data:
redis_data:
ollama_data:
lf_postgres:
lf_clickhouse:
lf_minio:
prometheus_data:
grafana_data:
services:
# --- Reverse proxy ---
nginx:
image: nginx:1.27-alpine
restart: unless-stopped
ports: ["80:80", "443:443"]
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./certs:/etc/letsencrypt:ro
networks: [core]
depends_on: [litellm, langfuse-web, grafana]
# --- Vector database ---
qdrant:
image: qdrant/qdrant:v1.12.5
restart: unless-stopped
volumes: [qdrant_data:/qdrant/storage]
environment:
QDRANT__SERVICE__API_KEY: ${QDRANT_API_KEY}
QDRANT__STORAGE__PERFORMANCE__MAX_SEARCH_THREADS: 2
networks: [core]
deploy:
resources:
limits: { memory: 2g, cpus: "1.5" }
# --- Cache semántico + memoria working ---
redis:
image: redis/redis-stack-server:7.4.0-v1
restart: unless-stopped
command: >
redis-stack-server
--requirepass ${REDIS_PASSWORD}
--maxmemory 1gb
--maxmemory-policy allkeys-lru
volumes: [redis_data:/data]
networks: [core]
deploy:
resources:
limits: { memory: 1200m, cpus: "0.75" }
# --- LLM gateway unificado ---
litellm:
image: ghcr.io/berriai/litellm:main-stable
restart: unless-stopped
environment:
LITELLM_MASTER_KEY: ${LITELLM_MASTER_KEY}
DATABASE_URL: postgresql://lf:${LF_DB_PASSWORD}@langfuse-db:5432/litellm
LANGFUSE_PUBLIC_KEY: ${LF_PUBLIC_KEY}
LANGFUSE_SECRET_KEY: ${LF_SECRET_KEY}
LANGFUSE_HOST: http://langfuse-web:3000
volumes: [./litellm/config.yaml:/app/config.yaml:ro]
command: ["--config", "/app/config.yaml", "--port", "4000"]
networks: [core]
depends_on: [langfuse-web, redis]
deploy:
resources:
limits: { memory: 512m, cpus: "0.5" }
# --- Modelos locales ---
ollama:
image: ollama/ollama:0.5.4
restart: unless-stopped
volumes: [ollama_data:/root/.ollama]
networks: [core]
deploy:
resources:
limits: { memory: 5g, cpus: "2.5" }
# --- Langfuse (observabilidad) ---
langfuse-db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_USER: lf
POSTGRES_PASSWORD: ${LF_DB_PASSWORD}
POSTGRES_DB: langfuse
volumes: [lf_postgres:/var/lib/postgresql/data]
networks: [core]
deploy:
resources:
limits: { memory: 512m, cpus: "0.5" }
langfuse-clickhouse:
image: clickhouse/clickhouse-server:24.8
restart: unless-stopped
environment:
CLICKHOUSE_USER: lf
CLICKHOUSE_PASSWORD: ${LF_CLICKHOUSE_PASSWORD}
CLICKHOUSE_DB: langfuse
volumes: [lf_clickhouse:/var/lib/clickhouse]
networks: [core]
deploy:
resources:
limits: { memory: 1g, cpus: "1" }
langfuse-web:
image: langfuse/langfuse:3
restart: unless-stopped
environment:
DATABASE_URL: postgresql://lf:${LF_DB_PASSWORD}@langfuse-db:5432/langfuse
CLICKHOUSE_URL: http://langfuse-clickhouse:8123
CLICKHOUSE_USER: lf
CLICKHOUSE_PASSWORD: ${LF_CLICKHOUSE_PASSWORD}
REDIS_CONNECTION_STRING: redis://:${REDIS_PASSWORD}@redis:6379/1
NEXTAUTH_URL: https://langfuse.${DOMAIN}
NEXTAUTH_SECRET: ${LF_NEXTAUTH_SECRET}
SALT: ${LF_SALT}
ENCRYPTION_KEY: ${LF_ENCRYPTION_KEY}
depends_on: [langfuse-db, langfuse-clickhouse, redis]
networks: [core]
deploy:
resources:
limits: { memory: 768m, cpus: "0.75" }
langfuse-worker:
image: langfuse/langfuse-worker:3
restart: unless-stopped
environment:
DATABASE_URL: postgresql://lf:${LF_DB_PASSWORD}@langfuse-db:5432/langfuse
CLICKHOUSE_URL: http://langfuse-clickhouse:8123
CLICKHOUSE_USER: lf
CLICKHOUSE_PASSWORD: ${LF_CLICKHOUSE_PASSWORD}
REDIS_CONNECTION_STRING: redis://:${REDIS_PASSWORD}@redis:6379/1
SALT: ${LF_SALT}
ENCRYPTION_KEY: ${LF_ENCRYPTION_KEY}
depends_on: [langfuse-web]
networks: [core]
deploy:
resources:
limits: { memory: 512m, cpus: "0.5" }
# --- Métricas ---
prometheus:
image: prom/prometheus:v2.55.1
restart: unless-stopped
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
- prometheus_data:/prometheus
networks: [core]
grafana:
image: grafana/grafana:11.3.1
restart: unless-stopped
environment:
GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_PASSWORD}
volumes: [grafana_data:/var/lib/grafana]
networks: [core]
# --- Backups ---
restic:
image: mazzolino/restic:1.7.3
restart: unless-stopped
environment:
RUN_ON_STARTUP: "false"
BACKUP_CRON: "0 4 * * *"
RESTIC_REPOSITORY: s3:${SPACES_ENDPOINT}/${SPACES_BUCKET}/restic
RESTIC_PASSWORD: ${RESTIC_PASSWORD}
AWS_ACCESS_KEY_ID: ${SPACES_KEY}
AWS_SECRET_ACCESS_KEY: ${SPACES_SECRET}
RESTIC_FORGET_ARGS: "--keep-daily 7 --keep-weekly 4 --keep-monthly 6"
volumes:
- qdrant_data:/mnt/qdrant:ro
- lf_postgres:/mnt/lf_postgres:ro
- lf_clickhouse:/mnt/lf_clickhouse:ro
- redis_data:/mnt/redis:ro
networks: [core]
Dimensionamiento de memoria
El droplet s-4vcpu-8gb ofrece 7.5 GB usables tras el kernel. Presupuesto de memoria:
| Servicio | Límite | Justificación |
|---|---|---|
| Ollama | 5 GB | Llama 3.3 8B Q4_K_M cabe en ~4.8 GB |
| Qdrant | 2 GB | 10M vectores de 768 dims con cuantización escalar |
| Redis | 1.2 GB | caché semántica + estados de agente |
| ClickHouse | 1 GB | Langfuse observability |
| Postgres Langfuse | 512 MB | Metadata |
| Langfuse web + worker | 1.25 GB | |
| Nginx + Prometheus + Grafana | 400 MB | |
| Total comprometido | ~11.3 GB |
Importante: Los límites se solapan porque Ollama sólo consume sus 5 GB cuando está respondiendo activamente. Si tu carga es mayoritariamente de agentes que usan Claude/GPT vía LiteLLM y rara vez Ollama, el working set real se mantiene debajo de 6 GB. Si tu cliente necesita Ollama como primary, sube a
s-4vcpu-16gb(USD 96).
Configuración de LiteLLM Proxy
Archivo /opt/numoru-ai/litellm/config.yaml:
model_list:
- model_name: claude-sonnet
litellm_params:
model: anthropic/claude-sonnet-4-6
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: claude-opus
litellm_params:
model: anthropic/claude-opus-4-7
api_key: os.environ/ANTHROPIC_API_KEY
- model_name: gpt-4o
litellm_params:
model: openai/gpt-4o
api_key: os.environ/OPENAI_API_KEY
- model_name: llama-local
litellm_params:
model: ollama/llama3.3:8b-instruct-q4_K_M
api_base: http://ollama:11434
litellm_settings:
cache: true
cache_params:
type: redis-semantic
host: redis
port: 6379
password: os.environ/REDIS_PASSWORD
similarity_threshold: 0.92
ttl: 86400
success_callback: ["langfuse"]
failure_callback: ["langfuse"]
router_settings:
routing_strategy: latency-based-routing
fallbacks:
- claude-sonnet: [gpt-4o, llama-local]
- gpt-4o: [claude-sonnet, llama-local]
general_settings:
master_key: os.environ/LITELLM_MASTER_KEY
database_url: os.environ/DATABASE_URL
Este archivo habilita tres cosas críticas: caché semántica (45-60% hit rate en agentes de atención al cliente), fallback automático (si Anthropic cae, el tráfico salta a OpenAI o al modelo local) y trazas en Langfuse sin necesidad de instrumentar cada cliente.
Nginx con TLS automático
Sub-dominios recomendados:
api.tudominio.com→ LiteLLM Proxy (:4000)langfuse.tudominio.com→ Langfuse web (:3000)grafana.tudominio.com→ Grafana (:3001)qdrant.tudominio.com→ Qdrant HTTP (:6333) — protegido con basic auth además del API key
Archivo /opt/numoru-ai/nginx/conf.d/api.conf:
server {
listen 443 ssl http2;
server_name api.tudominio.com;
ssl_certificate /etc/letsencrypt/live/api.tudominio.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/api.tudominio.com/privkey.pem;
client_max_body_size 25m;
location / {
proxy_pass http://litellm:4000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
}
Renovación de certificados con un job cron que ejecuta certbot renew --webroot cada 3 días.
Backup y restauración
restic corre cada 4am y respalda todos los volúmenes a Digital Ocean Spaces. Política de retención: 7 diarios, 4 semanales, 6 mensuales.
Restauración probada: en un droplet nuevo, git clone del repo de infraestructura + .env + restic restore latest --target / regenera el stack completo en menos de 15 minutos. Tiempo objetivo de recuperación (RTO): 20 minutos. Punto objetivo de recuperación (RPO): 24 horas.
Observabilidad: lo que Langfuse + Grafana te dan
- Langfuse: cada llamada LLM con input, output, costo, latencia y usuario. Evals programables. Prompt management versionado.
- Grafana dashboards provisionados:
- Tokens/min por modelo
- Cache hit rate de Redis (meta >40%)
- Latencia p50/p95/p99 por endpoint de LiteLLM
- Uso de disco de Qdrant
- Colas pendientes en Langfuse worker
Costos reales (abril 2026)
| Concepto | USD/mes |
|---|---|
Droplet s-4vcpu-8gb | 40 |
| Spaces (50 GB + egress medio) | 5 |
| Dominio + certs | 1 |
| Anthropic/OpenAI (pass-through) | variable |
| Infraestructura base | 46 |
Con este stack, un cliente que antes pagaba 800 USD/mes a proveedores SaaS de RAG + observabilidad + gateway típicamente baja a 46 USD + costos de LLM (que además caen 45% por caché semántica). El ROI es inmediato a partir de la segunda semana.
Tier de entrada/estándar de cada equivalente managed vs el drop-in self-hosted de $46 / mes. Usa el gráfico completo para justificar migración ante un CFO.
- SaaS managed (USD)
- Self-hosted (USD)
Páginas públicas de pricing Pinecone, LangSmith, OpenRouter, Upstash, febrero 2026.
Impacto de negocio y casos
Qué vende Numoru alrededor del stack
El docker-compose.yml gratuito es asset de marketing. El revenue viene de dos servicios productizados: una instalación fixed-price ($3.5k - 8k) que levanta el stack en la cuenta DO del cliente, y un retainer de operación managed ($450-1,200 / mes) que se hace cargo de upgrades, backups y respuesta a incidentes. Ambos son de margen alto porque la infra real cuesta $46.
Quién compra infra self-hosted de IA
Pricing instalación + ops managed por comprador (Numoru, 2026)
Benchmarks públicos que respaldan el pitch
Qdrant — performance vs alternativas managed
Langfuse — adopción self-hosted
Digital Ocean — sizing de droplets para IA
Caso ilustrativo — agencia migrando 7 clientes fuera de SaaS managed
Agencia partner Numoru migrando 7 clientes mid-market al stack compartido
Calculadora ROI — migrar fuera de SaaS managed
Cliente mid-market: SaaS managed vs Numoru self-hosted (12 meses)
| Instalación (one-time) | −$6,500 |
| SaaS managed evitado (12 mo × $820) | +$9,840 |
| Ahorro LLM por caché (12 mo × $1,530) | +$18,360 |
| Droplet + Spaces + TLS (12 mo × $46) | −$552 |
| Retainer ops Numoru (12 mo × $450) | −$5,400 |
| Tiempo eng ahorrado (6 h × $95 × 12) | +$6,840 |
| Contribución neta año 1 | +$22,588 |
Tiers de pricing Numoru
- Corre en tu cuenta DO
- Compose con Qdrant + Langfuse + LiteLLM + Redis
- TLS + Nginx + backups
- Warranty 30 días
- Runbook PDF
- Stripe / tarjeta o PO
- Todo lo del Install
- Monitoreo 24 / 7 (Grafana + alertas)
- Patching + upgrades de versión
- Revisión mensual de incidentes
- Canal Slack compartido
- Auditoría trimestral de costo
- Despliegue en región UE (FRA / AMS / PAR)
- Controles HIPAA-equivalentes
- Bundle de docs técnicas AI Act
- SAML / SSO
- Coordinación de pentest
- Atestación anual de compliance
El retainer de ops escala por número de droplets — agencias multi-cliente tienen 20% off desde 5 droplets, 30% off desde 10.
FAQ
¿Este stack aguanta tráfico productivo real?Sí para cargas de hasta ~30 queries por segundo sostenidas y 500k llamadas LLM por día. Más allá de eso, separa Qdrant y Langfuse en droplets propios.
¿Puedo correr Claude o GPT localmente?No, son modelos cerrados. Ollama + Llama 3.3 8B / Qwen 2.5 7B es la ruta local. Para casos sensibles mezcla: prompts genéricos van a Claude vía LiteLLM, prompts con PII van a Llama local.
¿Por qué no Kubernetes?Un droplet único con Docker Compose es más barato, más fácil de operar por un consultor individual y suficiente hasta los 500 usuarios concurrentes. Kubernetes empieza a tener sentido a partir de 3 nodos productivos.
¿Es compatible con AI Act?El stack es compatible con los requisitos técnicos de transparencia, log keeping y data residency. La compliance formal requiere además documentación DPIA y gobernanza — eso es servicio, no infraestructura.
¿Puedo reemplazar Qdrant por pgvector? Para menos de 100k vectores y bajos QPS, sí. A partir de 1M vectores y búsqueda híbrida en producción, Qdrant gana claramente en latencia (p95 <50 ms vs 300 ms).
Próximos pasos
El repositorio completo con docker-compose.yml, Terraform para crear el droplet y runbooks de incidente está publicado en github.com/numoru-ia/ai-stack-do. El siguiente artículo de esta serie cubre el patrón de memoria por capas que aprovecha Redis + Langfuse + Mem0 dentro de este stack.