SSD Nodes Learn Hosting plans →
Guías Matt ConnorPor Matt Connor · Actualizado 2026-08-23

Límites de memoria en Docker Compose sin OOM

Configura límites de memoria y CPU en Docker Compose para evitar que un contenedor derribe tu VPS. Incluye deploy.resources, mem_limit, exit 137, swap y sizing.

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. Defina deploy.resources.limits.memory en un servicio y ese contenedor nunca podrá usar más memoria que la cantidad indicada. 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 memoria RAM es fija y no hay memoria adicional del host disponible. Un contenedor con una fuga de memoria o una consulta defectuosa puede consumir todas las páginas libres de un servidor con 8GB. Entonces el kernel mata el proceso que considera más problemático. A menudo es una base de datos o la sesión SSH, no el 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 cambio y confirme que el límite está activo:

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

La columna MEM USAGE / LIMIT debe mostrar un valor similar a 142MiB / 1GiB. Si la columna del límite muestra toda la memoria 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 de archivo en la que se basa esta configuración.

deploy.resources.limits o mem_limit: cuál 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 antiguos de Compose. deploy.resources procede del esquema de Swarm y ahora forma parte de la 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 se ejecuta docker compose up, sin ningún clúster de Swarm. Las partes exclusivas de Swarm del bloque deploy son las demás 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 resultado 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 nanoCPU, por lo que 1.5 muestra 1500000000. Un 0 en cualquier campo indica que no se estableció ningún límite. El límite de memoria mínimo que Docker acepta es 6m. Por debajo de ese valor, el contenedor no se inicia.

Qué ocurre cuando un contenedor alcanza el límite

El contenedor no se ralentiza. Muere.

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 enviar al espacio de intercambio. 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. Al matar 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 indica una terminación por OOM. false 137 significa que otro componente envió SIGKILL. La causa habitual es que docker compose stop agotó 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 donde se registra el evento. Supervise el daemon en tiempo real:

docker events --filter event=oom

Después, lea el registro del kernel, que conserva el registro aunque se reinicie el sistema:

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

Una terminación por cgroup muestra 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, es decir, que la propia máquina se quedó sin RAM. Esto es precisamente lo 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 morir. 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á saludable para detectar un contenedor que muere continuamente sin tener que supervisarlo.

La reserva es una referencia; el límite es la regla

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

Por sí sola, una reserva no protege nada. Úsela para indicar que un servicio debe recibir un trato preferente bajo presión y confíe en el límite para aplicar la protección. 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 ambigüedades

La mayoría de las imágenes de VPS se publican sin ningún archivo de swap. Ejecute swapon --show y free -h. Si el total de swap es cero, ninguna de las opciones relacionadas con swap que se indican a continuación tiene efecto, y el límite de memoria es exclusivamente un límite 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 la misma cantidad, el contenedor no tiene 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 arrancados sin swapaccount=1. En esos hosts, el límite de memoria sigue aplicándose, pero se ignora la parte correspondiente a swap.

Sea claro sobre lo que aporta swap. Hace que una terminación OOM sea más lenta, no menos probable, porque un proceso con una fuga de memoria llena swap con la misma facilidad que llena la RAM. Además, un contenedor que somete el almacenamiento compartido del VPS a una carga excesiva por usar swap ralentiza todos los demás servicios del equipo. 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 eso, un contenedor que lee archivos grandes se acerca a su límite y permanece allí. Es normal y no indica una fuga de memoria, porque la caché limpia se recupera antes de que se invoque el OOM killer. Un servicio como un servidor multimedia Jellyfin autohospedado parecerá estar permanentemente cerca de su límite exactamente por este motivo.

Separe la cifra entre la caché y el 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 liberar. file es la caché de páginas, que sí se puede liberar. Configure el límite basándose en anon más un margen, no en el total. El archivo memory.events lo confirma: un contador oom_kill superior a cero indica que el kernel ha terminado un proceso en este contenedor desde que se inició, y un contador max que aumenta indica 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 propio 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 pico al mismo tiempo.

Una distribución viable en un servidor 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 multimedia o de archivos: límite de 2g, destinado en su mayor parte a 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 de cada contenedor y añada aproximadamente la mitad como margen. Un límite demasiado ajustado es peor que no establecer ningún límite, porque detiene un servicio sano durante un aumento normal del tráfico.

Hay un problema frecuente que merece una nota aparte. 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 terminará detenido. Una JVM (Java virtual machine) necesita -XX:MaxRAMPercentage=75 para dimensionar su 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. Con Ollama ocurre lo mismo con otro parámetro, porque aumentar num_ctx hace crecer la caché KV en cientos de megabytes y el contenedor muere a mitad de una petición larga. El cgroup no negocia. Detiene el proceso.

Los límites de CPU funcionan 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.

Esta es la diferencia importante. El kernel mata un contenedor que supera su límite de memoria. Un contenedor que supera su límite de CPU queda limitado y continúa funcionando, pero más despacio. Por eso, es seguro establecer un límite de CPU ajustado, mientras que un límite de memoria necesita margen.

cpu_shares es una herramienta distinta: un peso relativo que sólo se aplica 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. En un equipo inactivo, ninguno queda limitado. Use shares para establecer la prioridad relativa de los servicios y use cpus cuando necesite un límite real, por ejemplo, para impedir que un trabajo nocturno de transcodificación deje sin recursos al 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 ha aplicado 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, ya que 137 es 128 más la señal 9. El OOM killer 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 diferenciarlos. 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 las dos opciones funciona con docker compose. deploy.resources.limits.memory es la forma actual de Compose Specification y la opción predeterminada más adecuada para un archivo nuevo. Mantenga mem_limit si el resto del archivo ya utiliza las claves de nivel superior antiguas. Establecer ambas en un mismo servicio sólo dificulta la lectura del archivo. Elija una y verifique el resultado con docker inspect.

¿Por qué mi contenedor alcanza su límite máximo de memoria sin que se termine?

La cifra de uso de docker stats incluye la caché de páginas, que el kernel libera bajo presión en lugar de activar una terminación por falta de memoria. Ejecute docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat y lea el valor anon, que representa el conjunto de trabajo que no se puede reclamar. Un valor file alto junto a un valor anon bajo indica que el contenedor realiza 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. Observe durante un día el valor máximo de anon de cada contenedor bajo carga real antes de fijar las cifras. Considere el total como un presupuesto, no como un objetivo que deba agotarse.