SSD Nodes Learn 8GB de RAM — $66/año
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-02

Límites de memoria en Docker Compose para evitar OOM

Configura límites de memoria y CPU en Docker Compose para aislar contenedores: usa deploy.resources o mem_limit, interpreta el código 137 y ajusta swap.

Qué hace un límite de memoria de Docker Compose

Un límite de memoria de Docker Compose es un tope estricto que el kernel de Linux aplica al cgroup (grupo de control, la función del kernel que mide los recursos de un conjunto de procesos) de un contenedor. Configure deploy.resources.limits.memory en un servicio y ese contenedor no podrá usar más memoria que la cantidad especificada. Cuando intenta superar ese límite, el kernel mata un proceso dentro del contenedor y, normalmente, el contenedor termina con el código 137.

Esto es especialmente importante en un VPS, donde la RAM es fija y no hay memoria adicional del host disponible. Un contenedor con una fuga de memoria o una consulta incorrecta consumirá todas las páginas libres de un servidor de 8GB. Entonces, el kernel mata el proceso que considera más perjudicial. Con frecuencia, se trata de una base de datos o de su sesión SSH, no del contenedor que causó el problema. Los límites convierten una interrupción de todo el servidor en un único servicio que se reinicia.

services:
  app:
    image: ghcr.io/example/app:1.4
    deploy:
      resources:
        limits:
          cpus: "1.5"
          memory: 1g
        reservations:
          memory: 256m

Aplique el límite y confirme que está activo:

docker compose up -d
docker stats --no-stream

La columna MEM USAGE / LIMIT debería mostrar algo similar a 142MiB / 1GiB. Si la columna del límite muestra toda la RAM del host, la configuración no se aplicó. El resto de esta guía no será útil hasta que se aplique correctamente. Si el archivo de Compose es nuevo para usted, los conceptos básicos de Docker Compose para un VPS explican la estructura del archivo en la que se basa esta guía.

deploy.resources.limits o mem_limit: cual se aplica

Existen dos formas de escribir la misma idea, por eso resulta confuso.

mem_limit, mem_reservation, memswap_limit, cpus y cpu_shares son claves de servicio de nivel superior heredadas de los formatos de archivo Compose antiguos. deploy.resources proviene del esquema de Swarm y ahora forma parte de Compose Specification, que es el formato que docker compose lee actualmente.

Ambas funcionan en un solo host. Compose V2, el complemento docker compose, aplica deploy.resources.limits y deploy.resources.reservations cuando ejecuta docker compose up, sin ningún clúster de Swarm. Las partes exclusivas de Swarm del bloque deploy son las otras claves: mode, placement, update_config y endpoint_mode tienen significado para docker stack deploy y docker compose up las ignora. Por tanto, el consejo habitual de que "deploy necesita Swarm" es incorrecto para la subsección de recursos. Seguirlo deja los servicios sin ningún límite.

Elija una sola forma de escritura por proyecto. Escribir mem_limit: 512m y deploy.resources.limits.memory: 1g en el mismo servicio produce un archivo cuyo comportamiento no se puede determinar de un vistazo. En lugar de adivinar qué valor se aplicó, consulte al daemon:

docker inspect --format '{{.HostConfig.Memory}} {{.HostConfig.MemoryReservation}} {{.HostConfig.NanoCpus}}' app-1

Los valores de memoria se expresan en bytes, por lo que 1g muestra 1073741824. La CPU se expresa en nano-CPU, por lo que 1.5 muestra 1500000000. Un 0 en cualquier campo indica que no se estableció ningún límite. El límite mínimo de memoria que Docker acepta es 6m. Por debajo de ese valor, el contenedor no se inicia.

Qué sucede cuando un contenedor alcanza el límite

El contenedor no se ralentiza. Termina.

Cuando un proceso solicita una página y el cgroup ya está en memory.max, el kernel recupera primero todo lo posible dentro de ese cgroup: la caché de páginas limpias y, después, las páginas que puede intercambiar. Si la recuperación no libera suficiente memoria, el OOM killer (out of memory) del cgroup selecciona un proceso dentro del contenedor y le envía SIGKILL. Si termina el PID 1 del contenedor, el contenedor termina. El código de salida 137 es simplemente 128 más la señal 9, por lo que 137 es la huella de cualquier SIGKILL, no una prueba de OOM por sí solo.

docker compose ps -a
docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' app-1

true 137 es una terminación por OOM. false 137 significa que otro componente envió SIGKILL. La causa habitual es que docker compose stop alcance su periodo de gracia de diez segundos porque la aplicación ignoró SIGTERM. Esta distinción ahorra horas, porque los dos problemas no tienen nada en común.

Hay otros dos lugares que registran el evento. Supervise el daemon en tiempo real:

docker events --filter event=oom

Después, lea el registro del kernel, que conserva el registro incluso después de un reinicio:

sudo dmesg -T | grep -i -E 'memory cgroup out of memory|killed process'

Una terminación de cgroup imprime una línea que comienza por Memory cgroup out of memory: Killed process 24713 (node). Una línea sin el prefijo Memory cgroup indica un OOM del host. Esto significa que la máquina se quedó sin RAM. Ese es el fallo que los límites deben evitar. Si aparece, significa que la suma de los límites es demasiado alta o que algunos servicios no tienen ningún límite.

Con restart: unless-stopped, un bucle de OOM puede pasar desapercibido, porque el servicio aparece activo en docker compose ps un segundo después de terminar. Compruebe la columna de tiempo de actividad y el contador de reinicios. Combine el límite con un healthcheck que informe de que la aplicación no está en buen estado para que un contenedor que termina continuamente sea visible sin tener que supervisarlo.

La reserva es una indicación; el límite es la regla

reservations.memory (el anterior mem_reservation) es un umbral flexible. Docker lo describe como un límite flexible que se activa cuando el daemon detecta contención o poca memoria en el host. Nunca impide que un contenedor lo supere y nunca garantiza que la memoria esté libre cuando el contenedor la solicite. Solo orienta al kernel para que recupere memoria primero de los contenedores que superan su reserva.

Por tanto, una reserva no protege nada por sí sola. Úsela para marcar un servicio que desea que reciba un trato preferente bajo presión y confíe en el límite para la seguridad. Mantenga la reserva por debajo del límite; de lo contrario, el contenedor no se iniciará: Docker rechaza la configuración con Minimum memory limit can not be less than memory reservation limit.

Contabilidad de swap, sin engaños

La mayoría de las imágenes de VPS se distribuyen sin ningún archivo de swap. Ejecute swapon --show y free -h. Si el total de swap es cero, ninguna de las configuraciones relacionadas con swap que aparecen a continuación tiene efecto, y el límite de memoria es un límite exclusivo de RAM.

memswap_limit no es la cantidad de swap. Es el total de memoria más swap. Con mem_limit: 1g y memswap_limit: 2g, el contenedor obtiene 1GB de RAM y 1GB de swap. Si establece ambos valores con el mismo valor, el contenedor no obtiene swap. Si establece mem_limit y deja memswap_limit sin definir, el contenedor puede usar swap hasta alcanzar de nuevo el tamaño de su límite de memoria.

Ubuntu 24.04 y Debian 13 usan cgroup v2 de forma predeterminada. En esta versión, swap es un contador independiente (memory.swap.max), y esto funciona sin configuración adicional. El mensaje antiguo Your kernel does not support swap limit capabilities procede de hosts con cgroup v1 que se iniciaron sin swapaccount=1. En esos hosts, el límite de memoria sigue aplicándose, pero se ignora la parte correspondiente a swap.

Sea preciso sobre lo que aporta swap. Hace que una terminación por OOM sea más lenta, no menos probable, porque un proceso que tiene una fuga de memoria llena swap con la misma facilidad que llena RAM. Mientras tanto, un contenedor que usa swap de forma intensiva en el almacenamiento compartido del VPS ralentiza todos los demás servicios del servidor. Para cualquier servicio sensible a la latencia, un límite correcto sin swap falla más rápido y de forma más predecible.

Por qué el uso de memoria parece peor de lo que es

La cifra de MEM USAGE en docker stats incluye la caché de páginas, por lo que un contenedor que lee archivos grandes se acerca a su límite y permanece allí. Esto es normal y no es una fuga, porque la caché limpia se recupera antes de que se llame al OOM killer. Un servicio como un servidor multimedia Jellyfin autohospedado parecerá estar permanentemente cerca de su límite por este motivo.

Separe la caché del conjunto de trabajo real desde dentro del contenedor:

docker compose exec app grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat
docker compose exec app cat /sys/fs/cgroup/memory.events

anon es memoria anónima: el conjunto de trabajo que no se puede descartar. file es la caché de páginas, que sí se puede descartar. Establezca el límite según anon más un margen, no según el total. El archivo memory.events resuelve la cuestión: un contador oom_kill superior a cero significa que el kernel ha terminado algún proceso en este contenedor desde que se inició, y que un contador max que aumenta significa que el contenedor está alcanzando su límite en este momento. Ambos comandos necesitan un shell y coreutils dentro de la imagen, por lo que fallan en una imagen distroless o scratch.

Límites de dimensionamiento en un VPS de 8GB

Empiece por el host, no por las aplicaciones. En un VPS de 8GB, reserve aproximadamente 1GB para el kernel, el daemon de Docker, sshd, journald y su propia shell de inicio de sesión. Quedan aproximadamente 7GB para asignar, y la suma de los límites de todos los contenedores debe mantenerse por debajo de esa cantidad. La sobreasignación funciona hasta el día en que dos servicios alcanzan su máximo al mismo tiempo.

Una distribución práctica en un sistema de 8GB:

  • Proxy inverso: límite de 128m. Es un proceso pequeño, y un límite tan ajustado detecta de inmediato una recarga descontrolada de la configuración.
  • PostgreSQL: límite de 2g, con shared_buffers establecido en aproximadamente 512MB en la configuración de la base de datos.
  • Contenedor de la aplicación: límite de 1g.
  • Worker en segundo plano: límite de 512m.
  • Servicio de archivos o multimedia: límite de 2g, la mayor parte para la caché de páginas.

No copie esos valores en su propia pila. Ejecute los servicios con carga real durante un día, supervise docker stats, tome el valor máximo de anon por contenedor y añada aproximadamente la mitad como margen. Un límite demasiado ajustado es peor que no tener límite, porque termina con un servicio sano durante un aumento normal del tráfico.

Hay un problema frecuente que merece una nota propia. La mayoría de los runtimes no detectan el límite a menos que se lo indique. PostgreSQL dimensionará shared_buffers y work_mem por encima del límite de su contenedor y el kernel terminará el proceso. Una JVM (máquina virtual de Java) necesita -XX:MaxRAMPercentage=75 para dimensionar el heap a partir del límite del cgroup y no de la RAM del host. Node.js necesita --max-old-space-size en megabytes, establecido por debajo del límite del contenedor, o su recolector de basura dejará crecer el heap hasta que intervenga el kernel. El cgroup no negocia. Termina el proceso.

Los límites de CPU se comportan de forma completamente distinta

cpus: "1.5" significa el 150% de un núcleo, aplicado como una cuota de CFS (completely fair scheduler). El contenedor obtiene 150ms de tiempo de CPU en cada periodo de 100ms, compartido entre todos sus hilos. Cuando agota ese tiempo, el kernel lo hace esperar hasta el siguiente periodo.

Este es el contraste importante. Un contenedor que supera su límite de memoria termina detenido. Un contenedor que supera su límite de CPU se estrangula y continúa ejecutándose más lentamente. Por eso es seguro establecer un límite de CPU agresivo, mientras que un límite de memoria necesita margen.

cpu_shares es una herramienta distinta: un peso relativo que solo importa cuando las CPU están realmente saturadas. Dos contenedores con shares de 1024 y 512 se reparten un núcleo ocupado aproximadamente en una proporción de dos a uno, y en un sistema inactivo ninguno está restringido. Use shares para establecer la prioridad de los servicios y use cpus cuando necesite un límite real, por ejemplo, para evitar que un trabajo nocturno de transcodificación deje sin recursos a su servidor web.

FAQ

¿Funciona deploy.resources.limits sin Docker Swarm?

Sí. Compose V2 aplica deploy.resources.limits y deploy.resources.reservations cuando ejecuta docker compose up en un solo host. Confírmelo con docker inspect --format '{{.HostConfig.Memory}}' <container>, que muestra el límite en bytes y muestra 0 cuando no se aplicó ningún límite. Las claves dentro de deploy que requieren realmente Swarm son mode, placement, update_config y endpoint_mode.

¿Qué significa el código de salida 137 en Docker Compose?

Significa que el proceso principal recibió SIGKILL, porque 137 es 128 más la señal 9. El terminador OOM del kernel es la causa habitual, pero un tiempo de espera de apagado produce el mismo código cuando una aplicación ignora SIGTERM. Ejecute docker inspect --format '{{.State.OOMKilled}} {{.State.ExitCode}}' <container> para distinguirlos. true 137 indica una terminación por falta de memoria, y false 137 no.

¿Debo establecer mem_limit o deploy.resources.limits.memory?

Cualquiera de los dos funciona con docker compose. deploy.resources.limits.memory es la forma actual de Compose Specification y es la opción recomendada para un archivo nuevo. Mantenga mem_limit si el resto del archivo ya usa las claves de nivel superior antiguas. Establecer ambos en un mismo servicio solo dificulta la lectura del archivo, así que elija uno y verifique el resultado con docker inspect.

¿Por qué mi contenedor usa todo su límite de memoria sin que termine?

La cifra de uso de docker stats incluye la caché de páginas, que el kernel libera bajo presión en lugar de provocar una terminación OOM. Ejecute docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat y consulte el valor anon, que representa el conjunto de trabajo que no se puede recuperar. Un valor alto de file junto a un valor bajo de anon indica que el contenedor realiza operaciones de entrada y salida de disco, no que esté a punto de terminar.

¿Cuánta RAM debo dejar sin asignar en un VPS de 8GB?

Deje aproximadamente 1GB para el kernel, el daemon de Docker, sshd, journald y su propia shell. Después, mantenga la suma de los límites de todos los contenedores por debajo de los 7GB restantes. Supervise durante un día, con carga real, el valor máximo de anon de cada contenedor antes de fijar las cifras. Considere el total como un presupuesto, no como un objetivo que deba agotarse.