← SCRAM AI Lab
Regla operativa sobre 94 contenedores en e2-standard-4: mem_limit prohibido, solo reservations. Hard limits causan OOMKilled en bursts; reservations garantizan piso.
May 21, 2026
6 lecturas
En el servidor de producción de SCRAM corren 94 contenedores Docker en una sola máquina GCP e2-standard-4 (4 vCPU, 16GB RAM, Debian 12). La regla operativa que aprendimos a las malas y que ahora es ley en el equipo: nunca uses hard limits (mem_limit, cpus, limits). Solo soft limits — reservations. Los hard limits no son una red de seguridad, son una pistola apuntada a tu uptime.
Un container con mem_limit: 512m recibe SIGKILL del kernel en el momento en que toca 513MB. No hay gracia. No hay swap. No hay log útil — solo OOMKilled: true en docker inspect. Y los bursts ocurren todo el tiempo en cargas reales:
El container muere, Docker lo reinicia, y por 8-15 segundos tu servicio está caído. Multiplica por 94 containers y tienes un patrón de flapping permanente.
Reservations son piso, no techo. Le dicen al scheduler de Docker: "garantiza al menos esta memoria/CPU; si hay disponible, deja que use más". El kernel solo mata por OOM cuando la máquina entera se queda sin memoria — no por un burst individual.
# docker-compose.yml — patrón correcto
services:
scram-api:
image: scram-api:latest
deploy:
resources:
reservations:
memory: 512M
cpus: '0.5'
# NO limits aquí. NUNCA.
restart: unless-stopped
scram-web:
image: scram-web:latest
deploy:
resources:
reservations:
memory: 256M
cpus: '0.25'
postgres:
image: postgres:16
deploy:
resources:
reservations:
memory: 2G
cpus: '1.0'
El argumento clásico: "pero entonces un container loco se come toda la RAM y mata a los demás". Es válido, pero el remedio (hard limits) es peor que la enfermedad. La forma correcta de manejar containers locos:
En SCRAM la suma de reservations de los 94 containers es ~22GB sobre 16GB físicos. Oversubscription ratio 1.4x. Esto funciona porque no todos los containers pegan su pico a la vez — Redis pica de noche, los workers de importación pican de mañana, las APIs públicas en horario laboral. Si tu carga es sincrónica (todos pican al mismo evento), no oversuscribas más de 1.1x.
Bajo carga extrema (todos pidiendo a la vez), Linux empieza a swappear y la máquina se vuelve lenta para todos. Esto pasa 2-3 veces al año en SCRAM, dura ~90 segundos, y se resuelve solo cuando el burst pasa. Vs hard limits: pasaría 30-40 veces al mes, con downtime real de containers individuales. El trade-off de soft limits gana por dos órdenes de magnitud.
# ver memoria real consumida vs reservada
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}"
# detectar containers cerca del pico de la máquina
docker stats --no-stream | awk '{print $1, $4}' | sort -k2 -h | tail -20
Alertas en Grafana sobre container_memory_working_set_bytes de cAdvisor, threshold 85% del host. Si dispara, miramos quién creció y por qué — no movemos mem_limits.
La pregunta filosófica del 2026: ¿migrar a Kubernetes te resuelve esto? No. Te da mejores primitivas (QoS classes, PodDisruptionBudgets) pero la regla sigue siendo la misma: requests sí, limits no para memoria, hasta que pruebes que la necesitas.
Artículos relacionados
Traefik v2.10 con auto-renewal certs para 94 containers
Wildcard *.scram2k.com cubre la mayoría, certs individuales para el resto. acme.json shared, DNS-01 para wildcards, HTTP-01 para subdomains. Anti-patrón: cert por container.
OpenTelemetry tracing para pipelines LLM
Instrumentar pipelines LLM multi-tier con spans nested: router → tier-selection → provider-call → parsing → side-effects. Atributos gen_ai semantic conventions y flame chart en Tempo.
Loki + Grafana para logs de chatbot: query patterns
Labels útiles (org_id, session_id, tier, provider) sin caer en high-cardinality. LogQL patterns para errores por tier, p99 latency y costos por hora. Retention 30d hot.