Por defecto, un contenedor de Docker puede consumir toda la CPU y toda la memoria del host, y sus logs crecen sin límite hasta llenar el disco. En un homelab con varios servicios compartiendo la misma máquina, eso significa que un solo contenedor descontrolado tumba el resto. En esta guía verás cómo poner techos de CPU y RAM con deploy.resources y con los atajos mem_limit/cpus en Docker Compose, cómo evitar que el matador por falta de memoria (OOM killer) mate el proceso equivocado, y cómo elegir y rotar el controlador de logs para que no se te llene el almacenamiento. La misma guía está disponible en inglés.

Puntos clave

  • Sin límites, un contenedor puede usar toda la CPU y la RAM del host; Docker no reserva nada por defecto.
  • En Compose los límites se ponen con deploy.resources.limits (cpus, memory, pids) y las reservas con deploy.resources.reservations. docker compose up respeta esta sección aunque no uses Swarm.
  • Los atajos mem_limit, cpus y mem_reservation hacen lo mismo con una sintaxis más corta; si usas ambos, deben coincidir.
  • El límite de memoria mínimo es de 6m (6 MB); el valor por defecto de cpu-shares es 1024, un peso relativo que solo actúa cuando hay contención.
  • El controlador de logs por defecto (json-file) no rota: los logs crecen sin freno. El controlador local rota por defecto a 20m por fichero, 5 ficheros y con compresión.
  • docker stats muestra en vivo el uso de CPU, memoria y su límite; docker logs y docker compose logs leen la salida de cada contenedor.

¿Por qué limitar la CPU y la memoria de un contenedor?

Un contenedor no es una máquina virtual con recursos asignados: comparte el kernel y, salvo que le digas lo contrario, compite por la CPU y la RAM del host en igualdad de condiciones con todo lo demás. La documentación de Docker es explícita: «Por defecto, un contenedor no tiene restricciones de recursos y puede usar tanta memoria como el planificador del kernel del host le permita».

En la práctica esto se traduce en dos accidentes muy comunes. El primero es una fuga de memoria en una aplicación (un importador que carga un fichero enorme, una base de datos mal configurada) que se come toda la RAM y provoca que el kernel empiece a matar procesos. El segundo es un pico de CPU (una tarea de reindexado, una compilación) que deja al resto de servicios sin tiempo de cómputo y hace que tu panel o tu proxy dejen de responder. Poner un techo a cada servicio convierte esos incidentes en un problema local: el contenedor culpable se ralentiza o se reinicia, pero el servidor sigue en pie.

Si todavía no tienes el motor instalado, empieza por cómo instalar Docker en Ubuntu 24.04; esta guía asume que ya puedes ejecutar docker compose.

¿Cómo se ponen límites con deploy.resources y mem_limit en Compose?

Hay dos formas de declarar límites en un docker-compose.yml, y ambas funcionan con docker compose up sin necesidad de Docker Swarm. La primera y recomendada es la sección deploy.resources, heredada de la especificación de Compose:

  • limits fija el techo duro: cpus (número fraccionario de núcleos), memory (RAM máxima) y pids (número máximo de procesos).
  • reservations fija un mínimo blando que Docker intenta garantizar cuando hay contención.

La segunda forma son los atajos a nivel de servicio: cpus, mem_limit, mem_reservation, memswap_limit y pids_limit. Son equivalentes y más cortos, pero si defines el mismo valor por las dos vías tienen que coincidir. Este es un docker-compose.yml completo, listo para copiar, con una aplicación web y su base de datos, cada una con su techo de recursos y con la rotación de logs ya configurada:

services:
  web:
    image: nginx:1.27.3
    restart: unless-stopped
    ports:
      - "8080:80"
    deploy:
      resources:
        limits:
          cpus: "1.0"
          memory: 256M
          pids: 200
        reservations:
          cpus: "0.25"
          memory: 64M
    logging:
      driver: "local"
      options:
        max-size: "10m"
        max-file: "3"
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost/"]
      interval: 30s
      timeout: 5s
      retries: 3

  db:
    image: postgres:17.2
    restart: unless-stopped
    environment:
      POSTGRES_PASSWORD: cambia_esto
    mem_limit: 512m
    mem_reservation: 128m
    cpus: 0.75
    volumes:
      - db_data:/var/lib/postgresql/data
    logging:
      driver: "local"

volumes:
  db_data:

El servicio web usa la sintaxis moderna deploy.resources; el servicio db usa los atajos mem_limit, mem_reservation y cpus para que veas las dos convenciones en el mismo fichero. Levántalo y comprueba que los límites se han aplicado:

docker compose up -d
docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.NanoCpus}}' \
  $(docker compose ps -q web)

El primer número es el límite de memoria en bytes (268435456 = 256 MB) y el segundo, los nanonúcleos de CPU (1000000000 = 1 CPU). Recuerda que el mínimo aceptado por memory es 6m; por debajo de eso Docker rechaza el arranque. Si tienes servicios que solo quieres levantar en algunos entornos, combínalos con los perfiles (profiles) de Docker Compose para no arrastrar recursos que no necesitas.

¿Cómo evitar que un contenedor tumbe el servidor (OOM)?

Cuando el host se queda sin memoria, el kernel de Linux invoca al OOM killer (matador por falta de memoria) para liberar RAM matando procesos. Sin límites, ese proceso puede ser cualquiera, incluido uno crítico del sistema. Con un memory por contenedor, el kernel mata primero al contenedor que ha superado su techo, que es justo lo que quieres: el fallo queda contenido.

Docker ajusta además la prioridad del propio demonio para que sea menos probable que lo maten a él antes que a un contenedor. Su documentación advierte de forma tajante contra trucos peligrosos: «No deberías intentar sortear estas salvaguardas ajustando manualmente --oom-score-adj a un número negativo extremo en el demonio o en un contenedor, ni activando --oom-kill-disable en un contenedor». La opción --oom-kill-disable solo es defendible si además fijas -m/memory, porque de lo contrario un contenedor sin techo y sin posibilidad de ser matado puede congelar la máquina entera.

La receta práctica es sencilla: pon un memory realista a cada servicio (con algo de margen sobre su consumo normal), deja restart: unless-stopped para que el contenedor se recupere tras un reinicio por OOM, y añade un healthcheck para que Compose sepa cuándo el servicio vuelve a estar sano. Puedes ver qué contenedores están reiniciándose y por qué en la salida de docker ps y en los eventos del propio Docker.

¿Qué controladores de logs hay y cómo rotarlos (json-file y local)?

Aquí está el segundo fallo que llena discos en silencio. El controlador de logs por defecto es json-file, que guarda cada línea de salida como JSON en el disco del host y no realiza ninguna rotación: los ficheros de log crecen indefinidamente hasta que el disco se llena. En un servicio ruidoso, eso puede ser cuestión de días.

Tienes dos maneras de arreglarlo. La primera es limitar el propio json-file con las opciones max-size y max-file. La segunda, y la que recomienda Docker, es cambiar al controlador local, que rota por defecto (a 20m por fichero y 5 ficheros), comprime los rotados y usa un formato binario más eficiente que el JSON. Puedes fijarlo por servicio, como en el docker-compose.yml de arriba, o cambiar el valor por defecto de todo el demonio en /etc/docker/daemon.json:

{
  "log-driver": "local",
  "log-opts": {
    "max-size": "10m",
    "max-file": "5"
  }
}

Tras editar ese fichero hay que recargar el demonio para que tome efecto:

sudo systemctl restart docker
docker info --format '{{.LoggingDriver}}'

La última orden confirma el controlador activo. Ten en cuenta que el cambio de controlador solo afecta a los contenedores que crees después; los ya existentes conservan el suyo hasta que los recrees con docker compose up -d --force-recreate.

¿Cómo consultar los logs y las métricas de un contenedor?

Con los límites y la rotación en su sitio, la observación del día a día se apoya en dos órdenes. Para los logs, docker logs (o docker compose logs) lee la salida estándar de un contenedor; -f la sigue en vivo, --tail 100 muestra solo las últimas líneas y --since 10m filtra por tiempo. Para las métricas, docker stats muestra en tiempo real el uso de CPU, la memoria consumida frente a su límite, la E/S de red y de disco y el número de procesos:

docker compose logs -f --tail 100 web
docker stats --no-stream

La columna MEM USAGE / LIMIT de docker stats es la prueba visual de que tus límites funcionan: si un contenedor se acerca a su techo, lo verás ahí antes de que el OOM killer actúe. Para no depender de la terminal, un visor web de logs como Dozzle te da la misma información en el navegador, y para gráficas históricas de CPU y memoria por contenedor puedes exportar métricas con Node Exporter y cAdvisor. El controlador local y esos exportadores se complementan: uno guarda el texto rotado, los otros las series temporales.

Preguntas frecuentes

¿Docker Compose respeta deploy.resources sin Docker Swarm?

Sí. Aunque la sección deploy nació para Swarm, docker compose up aplica específicamente deploy.resources.limits y deploy.resources.reservations en un host normal; solo ignora otras claves de deploy como replicas o placement. Por eso es la forma recomendada de poner límites en un homelab con un único nodo.

¿Qué diferencia hay entre un límite y una reserva de memoria?

El límite (memory en limits, o mem_limit) es un techo duro: si el contenedor lo supera, el kernel lo mata por OOM. La reserva (reservations.memory o mem_reservation) es un mínimo blando que Docker intenta garantizar cuando hay contención, pero que no impide usar más si hay RAM libre. Usa el límite para protegerte y la reserva para priorizar.

¿Por qué se me llena el disco con los logs de Docker?

Porque el controlador por defecto json-file no rota los logs: crecen sin límite. La solución es fijar max-size y max-file, o cambiar al controlador local, que rota y comprime por defecto (20 MB por fichero, 5 ficheros). El cambio solo afecta a los contenedores creados después de aplicarlo.

Conclusión

Poner límites de recursos y controlar los logs es lo que convierte un montón de contenedores en una infraestructura estable: cada servicio tiene su techo de CPU y RAM, el OOM killer sabe a quién sacrificar y los logs rotan en lugar de llenar el disco. Empieza por añadir un memory y un controlador local a tus servicios más ruidosos, verifícalo con docker stats y ve ajustando. El siguiente paso natural es vigilar todo eso de un vistazo: monta Dozzle para los logs y Node Exporter con cAdvisor para las métricas.

Fuentes: [1] Docker Docs: Runtime options with Memory, CPUs, and GPUs[1], [2] Docker Docs: Configure logging drivers[2], [3] docker/compose, releases[3], [4] Kernel.org: Control Group v2 (memory y cpu)[4].

Fuentes

  1. Docker Docs: Runtime options with Memory, CPUs, and GPUs
  2. Docker Docs: Configure logging drivers
  3. docker/compose, releases
  4. Kernel.org: Control Group v2 (memory y cpu)

Ruta: Self-hosting con Docker: de cero a producción