← SCRAM AI Lab

Tutoriales

Docker soft limits en GCP: por qué nunca hard limits

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

El OOMKilled de las 3 AM es siempre por un mem_limit que parecía conservador

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.

Hard limits: el problema

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:

  • Garbage collector de Node corriendo durante un endpoint de export CSV
  • Postgres construyendo un index en background mientras llega una query pesada
  • Nginx logueando burst de requests durante un ataque DDoS bajito

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.

Soft limits (reservations): la solución

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 contra soft limits y por qué está mal

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:

  • Healthchecks bien definidos con timeout corto y restart on failure
  • Alertas en Grafana sobre memoria de cada container, con threshold antes del límite del host
  • Priority via cgroups: containers críticos (Postgres, Redis, Traefik) tienen reservations altas; los nice-to-have (workers de import CSV) bajas
  • Healing automático: si un container excede X memoria por Y minutos, restart preventivo en ventana de bajo tráfico

El número que importa: oversubscription ratio

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.

Trade-off real: thrashing potencial

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.

Verificación en runtime

# 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.

docker
gcp
infra
← Volver a SCRAM AI Lab