Por defecto, un contenedor Docker puede usar toda la CPU y toda la memoria del servidor. Si una aplicación tiene una fuga de memoria o entra en un bucle, puede dejar sin recursos al resto de contenedores e incluso al sistema. En este tutorial aprenderás a limitar la memoria, el swap, la CPU y el número de procesos de un contenedor en Ubuntu 24.04, tanto con docker run como con Docker Compose, y a comprobar en cgroups v2 que el kernel aplica esos límites.
Requisitos previos
Para seguir esta guía necesitas:
- Un servidor con Ubuntu 24.04 LTS, por ejemplo un VPS de CubePath, con al menos 2 vCPU para ver bien el efecto de los límites de CPU.
- Un usuario no root con privilegios
sudoque pertenezca al grupodocker. - Docker Engine y el plugin Docker Compose instalados desde el repositorio oficial de Docker.
Cómo aplica Docker los límites
Docker no controla los recursos por sí mismo: crea un grupo de control (cgroup) del kernel para cada contenedor y escribe en él los límites. Ubuntu 24.04 usa cgroups v2, y dentro de un contenedor los ficheros relevantes están en /sys/fs/cgroup/:
| Opción de Docker | Fichero de cgroups v2 | Efecto |
|---|---|---|
--memory | memory.max | Límite duro de RAM; si se supera, el kernel mata procesos del contenedor |
--memory-reservation | memory.low | Mínimo que el kernel intenta respetar cuando falta memoria |
--memory-swap | memory.swap.max | Swap adicional permitido |
--cpus | cpu.max | Tope de CPU (por ejemplo, 1,5 núcleos) |
--cpu-shares | cpu.weight | Peso relativo cuando varios contenedores compiten por la CPU |
--cpuset-cpus | cpuset.cpus | Núcleos concretos en los que puede ejecutarse |
--pids-limit | pids.max | Número máximo de procesos e hilos |
Comprueba que tu sistema usa cgroups v2:
docker info --format '{{.CgroupVersion}} {{.CgroupDriver}}'
2 systemd
Paso 1: Limitar la memoria
La opción --memory (o -m) fija el máximo de RAM. Acepta los sufijos k, m y g. Arranca un Nginx con 256 MB:
docker run -d --name web-limitado --memory 256m nginx:alpine
Comprueba el límite de tres formas distintas. Primero, en la configuración del contenedor (el valor está en bytes):
docker inspect web-limitado --format '{{.HostConfig.Memory}}'
268435456
Después, en el cgroup que ve el propio contenedor:
docker exec web-limitado cat /sys/fs/cgroup/memory.max
268435456
Por último, en docker stats, que muestra el consumo actual frente al límite:
docker stats --no-stream web-limitado
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
4b1e9c2a7d3f web-limitado 0.00% 3.1MiB / 256MiB 1.21% 1.2kB / 0B 0B / 8kB 3
Qué ocurre al superar el límite
Cuando el contenedor intenta usar más memoria de la permitida, el OOM killer del kernel mata el proceso. Puedes provocarlo con tail /dev/zero, que acumula datos en memoria sin parar. Limita también el swap al mismo valor para que no pueda desbordarse al disco:
docker run --name oom-test --memory 64m --memory-swap 64m alpine tail /dev/zero
echo "Código de salida: $?"
Código de salida: 137
El código 137 significa que el proceso recibió SIGKILL. Confirma que la causa fue la memoria:
docker inspect oom-test --format 'OOMKilled={{.State.OOMKilled}}'
OOMKilled=true
Borra el contenedor de prueba:
docker rm oom-test
Si un servicio real muestra OOMKilled=true, no basta con subir el límite a ciegas: revisa con docker stats cuánto consume en carga normal y fija el límite con margen por encima.
Paso 2: Controlar el swap y la reserva de memoria
La opción --memory-swap indica el total de memoria más swap, no el swap por separado. Las combinaciones posibles son:
--memory | --memory-swap | Resultado |
|---|---|---|
512m | sin indicar | 512 MB de RAM y hasta 512 MB de swap |
512m | 512m | 512 MB de RAM y sin swap |
512m | 1g | 512 MB de RAM y hasta 512 MB de swap |
512m | -1 | 512 MB de RAM y swap ilimitado |
Para servicios sensibles a la latencia, como bases de datos, lo habitual es desactivar el swap del contenedor igualando ambos valores.
La reserva (--memory-reservation) es un límite blando: el contenedor puede superarla, pero cuando el servidor se queda sin memoria el kernel intenta no recortarle por debajo de ese valor. Debe ser menor que --memory:
docker run -d --name db-test \
--memory 1g --memory-swap 1g --memory-reservation 512m \
-e POSTGRES_PASSWORD=tu_contraseña_segura \
postgres:17
Comprueba los tres valores en el cgroup:
docker exec db-test sh -c 'cat /sys/fs/cgroup/memory.max /sys/fs/cgroup/memory.low /sys/fs/cgroup/memory.swap.max'
1073741824
536870912
0
Borra el contenedor de prueba:
docker rm -f db-test
Paso 3: Limitar la CPU
Tope de CPU con --cpus
--cpus es la forma más sencilla de limitar la CPU: --cpus 0.5 permite usar como máximo medio núcleo, y --cpus 1.5, núcleo y medio, repartido entre los núcleos disponibles. Arranca un contenedor que consume CPU sin parar, limitado a medio núcleo:
docker run -d --name cpu-test --cpus 0.5 alpine sh -c 'while :; do :; done'
Mide su consumo:
docker stats --no-stream cpu-test
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
9a3d7e1f5b2c cpu-test 50.12% 412KiB / 3.8GiB 0.01% 0B / 0B 0B / 0B 1
En docker stats, el 100 % equivale a un núcleo completo, así que el contenedor se queda en torno al 50 %. En el cgroup, el límite aparece como cuota y periodo en microsegundos (50 ms de cada 100 ms):
docker exec cpu-test cat /sys/fs/cgroup/cpu.max
50000 100000
Cambiar límites sin recrear el contenedor
docker update modifica los límites de un contenedor en marcha. Sube el límite a un núcleo y comprueba el cambio:
docker update --cpus 1 cpu-test
docker exec cpu-test cat /sys/fs/cgroup/cpu.max
cpu-test
100000 100000
Los cambios hechos con docker update se pierden si el contenedor se recrea (por ejemplo, con docker compose up -d), así que trasládalos después a tu configuración.
Fijar núcleos con --cpuset-cpus
--cpuset-cpus restringe el contenedor a núcleos concretos, numerados desde 0 (acepta listas como 0,2 y rangos como 0-1). Es útil para aislar una carga pesada en un núcleo y dejar el resto libre. nproc dentro del contenedor muestra cuántos núcleos puede usar:
docker run --rm --cpuset-cpus 0 alpine nproc
1
Prioridad relativa con --cpu-shares
--cpu-shares no es un límite: solo reparte la CPU cuando hay competencia. El valor por defecto es 1024; un contenedor con 2048 recibirá el doble de tiempo de CPU que uno con 1024 cuando ambos quieran la CPU completa. Si el servidor está desocupado, cualquier contenedor puede usar toda la CPU.
Detén el contenedor de prueba:
docker rm -f cpu-test
Paso 4: Limitar el número de procesos
Un fallo que crea procesos sin control (o una bomba fork) puede agotar la tabla de procesos del servidor aunque la CPU y la memoria estén limitadas. --pids-limit lo evita:
docker run --rm --pids-limit 50 alpine cat /sys/fs/cgroup/pids.max
50
Un valor entre 100 y 500 suele bastar para servicios web; ajústalo según los hilos que cree tu aplicación.
Paso 5: Definir límites en Docker Compose
En Docker Compose, los límites se declaran en deploy.resources, que Compose aplica también fuera de Swarm. Crea un proyecto de ejemplo:
mkdir -p ~/limites && cd ~/limites
nano compose.yaml
services:
web:
image: nginx:alpine
ports:
- "8080:80"
pids_limit: 200
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
memory: 128M
worker:
image: alpine
command: sleep infinity
mem_limit: 256m
memswap_limit: 256m
cpus: 0.5
El servicio web usa la sintaxis deploy.resources y el servicio worker, las claves equivalentes a nivel de servicio (mem_limit, memswap_limit, cpus). Ambas funcionan con docker compose; elige una y úsala de forma coherente en todo el proyecto. memswap_limit no tiene equivalente en deploy.resources, por lo que es la forma de controlar el swap en Compose.
Arranca los servicios y comprueba los límites:
docker compose up -d
docker stats --no-stream
CONTAINER ID NAME CPU % MEM USAGE / LIMIT MEM % NET I/O BLOCK I/O PIDS
1f2e3d4c5b6a limites-web-1 0.00% 3.4MiB / 512MiB 0.66% 1.1kB / 0B 0B / 8kB 3
6a5b4c3d2e1f limites-worker-1 0.00% 328KiB / 256MiB 0.12% 1.1kB / 0B 0B / 0B 1
Verifica también los límites de CPU y de procesos del servicio web:
docker compose exec web cat /sys/fs/cgroup/cpu.max /sys/fs/cgroup/pids.max
100000 100000
200
Cuando termines, elimina el proyecto de prueba:
docker compose down
Cómo elegir los valores
- Mide antes de limitar: observa el consumo con
docker statsdurante un periodo de carga real y fija el límite de memoria entre un 25 % y un 50 % por encima del pico. - Deja memoria para el sistema: la suma de los límites de memoria de todos los contenedores no debería superar la RAM del servidor menos unos cientos de MB para el sistema operativo y Docker.
- Ajusta la aplicación al límite: algunos entornos no leen el cgroup y se configuran según la RAM total del servidor. Por ejemplo, fija
shared_buffersen PostgreSQL o-Xmxen Java por debajo del límite del contenedor. - Limita todo lo que no controlas: contenedores de terceros, tareas por lotes y procesos de compilación son los primeros candidatos.
Solución de problemas
Minimum memory limit allowed is 6MB: el límite es demasiado bajo. Usa valores realistas; ningún servicio útil funciona con unos pocos MB.Memory limit should be smaller than already set memoryswap limit: al subir--memorycondocker update, sube también--memory-swapen el mismo comando, ya que la memoria nunca puede superar el total de memoria más swap.- El contenedor se reinicia en bucle con código 137: revisa
docker inspect nombre --format '{{.State.OOMKilled}}'. Si estrue, el límite de memoria es insuficiente para su carga; si esfalse, otro proceso o una sonda de salud lo está matando. Range of CPUs is from 0.01 to N.00: has pedido más CPU de la que tiene el servidor. Comprueba los núcleos disponibles connproc.
Conclusión
Has limitado la memoria, el swap, la CPU y los procesos de tus contenedores con docker run, docker update y Docker Compose, y has comprobado en cgroups v2 que el kernel aplica cada límite. Como siguientes pasos, añade límites a todos los servicios de tus ficheros de Compose, recoge las métricas de docker stats en un sistema de monitorización para ajustar los valores con datos reales y configura healthcheck para detectar contenedores que se quedan sin recursos.
