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

Límites de memoria en Docker Compose para evitar 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 ajuste.

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 nunca podrá usar más memoria que la cantidad indicada. Cuando intenta superarla, el kernel mata un proceso dentro del contenedor y este suele salir 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 defectuosa consumirá todas las páginas libres de un servidor de 8GB. Entonces el kernel mata el proceso que considera más problemático, que a menudo es una base de datos o su sesión SSH, en lugar 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

Aplíquelo y confirme que el límite está activo:

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

La columna MEM USAGE / LIMIT debería mostrar algo parecido a 142MiB / 1GiB. Si la columna del límite muestra toda la RAM del host, la configuración no se aplicó y el resto de esta guía no será útil hasta que lo haga. 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. Docker Compose, 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. El complemento se encuentra ahora en la línea v5 (v5.6.0 era la última versión estable cuando se comprobó el 2026-10-10). Pasó directamente de v2 a v5 y omitió las versiones 3 y 4 para evitar confusiones con las versiones antiguas del formato de archivos de Compose. Ninguna versión v5 ha cambiado la forma de gestionar los límites de recursos. Las partes orientadas a Swarm del bloque deploy son las demás claves, como placement, update_config y endpoint_mode, que describen la programación y las actualizaciones progresivas en un clúster de nodos para docker stack deploy. 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 no sólo dificulta la lectura: la Compose Specification indica que, cuando se establece mem_limit, debe ser coherente con limits.memory, pero no especifica qué valor prevalece si difieren. Independientemente de la forma utilizada, confirme qué aplicó realmente el 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 significa que no se estableció ningún límite. El límite de memoria más pequeño 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. Termina.

Cuando un proceso solicita una página y el cgroup ya ha alcanzado su 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 a la swap. Si la recuperación no libera suficiente memoria, el OOM (out of memory) killer del cgroup selecciona un proceso dentro del contenedor y le envía SIGKILL. Al terminar 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 suele indicar que otro componente envió SIGKILL (un programa también puede salir con 137 de forma intencionada, pero es poco frecuente), y 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 de cgroup muestra una línea que comienza por Memory cgroup out of memory: Killed process 24713 (node). Esa línea significa que un contenedor alcanzó su propio límite, y puede aparecer aunque el host todavía tenga mucha RAM libre. Una línea sin el prefijo Memory cgroup indica un OOM del host, es decir, que la propia máquina se quedó sin RAM. Eso es precisamente lo que los límites deben evitar, por lo que verlo suele significar que la suma de los límites es demasiado alta, que algunos servicios no tienen ningún límite o que algo externo a Docker está usando más memoria de la prevista.

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 detectar un contenedor que termina continuamente sin tener que supervisarlo.

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

reservations.memory (el mem_reservation antiguo) 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 ni garantiza que la memoria esté libre cuando el contenedor la solicite. Sólo orienta al kernel para que recupere memoria primero de los contenedores que superen su reserva.

Por tanto, una reserva no protege nada por sí sola. Úsela para indicar que un servicio debe recibir un trato preferente bajo presión y confíe en el límite para la seguridad. Mantenga la reserva por debajo del límite o 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 opciones relacionadas con swap que se muestran a continuación tiene efecto, y el límite de memoria es un límite de RAM estricto.

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 tendrá swap. Si establece mem_limit y deja memswap_limit sin definir, el contenedor podrá 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 iniciados sin swapaccount=1. En esos hosts, el límite de memoria sigue aplicándose, pero se ignora la parte correspondiente a swap.

Sea realista sobre lo que aporta swap. Hace que un proceso OOM kill tarde más, pero no reduce la probabilidad de que ocurra, porque un proceso con una fuga de memoria llena swap con la misma facilidad que llena la RAM. Además, un contenedor que somete a swap al 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 antes y de forma más predecible.

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

En Linux, la cifra de MEM USAGE en docker stats resta sólo la caché de páginas inactiva (inactive_file en cgroup v2), por lo que la caché usada recientemente sigue contando. Un contenedor que lee archivos grandes se acerca a su límite y permanece ahí. Esto es normal y no indica una fuga, 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 precisamente 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 descartar. file es la caché de páginas, que sí se puede descartar. Establezca el límite a partir de anon más un margen, no a partir del total. El archivo memory.events resuelve la cuestión: un contador oom_kill superior a cero significa que el kernel ha terminado un proceso en este contenedor desde que se inició, y 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 unos 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 equipo de 8GB:

  • Proxy inverso: límite de 128m. Es un proceso pequeño, y un límite tan ajustado detecta de inmediato una recarga de configuración descontrolada.
  • 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 infraestructura. 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 tener límite, porque detiene un servicio saludable durante un pico normal de tráfico.

Hay un caso problemático que merece una nota aparte. Algunos runtimes ignoran el límite a menos que se les indique, y los que lo leen pueden dimensionarse de una forma distinta de la esperada. PostgreSQL puede dimensionar shared_buffers y work_mem por encima del límite de su contenedor y terminar detenido. Una JVM (máquina virtual de Java) moderna detecta el límite del cgroup de forma predeterminada, pero las compilaciones de HotSpot, como OpenJDK, asignan al heap sólo el 25 por ciento de ese límite. Por eso, -XX:MaxRAMPercentage=75 es la forma habitual de permitir que el heap use una mayor parte del límite y dejar espacio para la memoria externa al heap. Node.js calcula el tamaño de su heap predeterminado a partir del límite del cgroup desde la versión 12, aproximadamente en la mitad del límite del contenedor y con un máximo cercano a 2GB. Establezca --max-old-space-size en megabytes sólo cuando quiera un límite diferente y manténgalo por debajo del límite del contenedor, para que el recolector de basura actúe antes de que intervenga el kernel. En Ollama ocurre lo mismo con otro parámetro, porque aumentar num_ctx incrementa la caché KV en cientos de megabytes y el contenedor muere a mitad de una petición larga. El cgroup no negocia. Mata.

Los límites de CPU funcionan de una manera completamente distinta

cpus: "1.5" significa el 150% de un núcleo, aplicado como una cuota 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 periodo siguiente.

Esta es la diferencia importante. Si un contenedor supera su límite de memoria, se termina. Si supera su límite de CPU, se limita su ejecución y continúa funcionando, pero más despacio. Por tanto, es seguro establecer un límite de CPU relativamente ajustado, mientras que un límite de memoria necesita margen.

cpu_shares es una herramienta diferente: un peso relativo que sólo 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. En un sistema inactivo, ninguno de los dos está limitado. Use shares para establecer la prioridad relativa de los servicios y use cpus cuando necesite un límite máximo real, por ejemplo, para evitar que un trabajo nocturno de transcodificación deje sin CPU al servidor web.

FAQ

¿Funciona deploy.resources.limits sin Docker Swarm?

Sí. Docker Compose, el complemento docker compose (actualmente en su línea v5), 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 de deploy escritas pensando en Swarm son las relacionadas con clústeres, como placement, update_config y endpoint_mode.

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

Casi siempre significa que el proceso principal recibió SIGKILL, porque 137 es 128 más la señal 9 (un programa también puede salir deliberadamente con 137, pero es poco frecuente). La causa habitual es el terminador OOM del kernel, 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 distinguir ambos casos. true 137 indica una terminación por 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 el formato actual de Compose Specification y la opción recomendada para un archivo nuevo. Mantenga mem_limit si el resto del archivo ya usa las claves de nivel superior antiguas. Compose Specification exige que ambas sean coherentes cuando se establecen las dos, así que elija una y verifique el resultado con docker inspect.

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

La cifra de uso de docker stats todavía incluye la caché de páginas usada recientemente. El kernel la libera bajo presión en lugar de provocar una terminación por OOM. Ejecute docker compose exec <service> grep -E '^(anon|file) ' /sys/fs/cgroup/memory.stat y consulte el valor de 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 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, 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 una cantidad que deba ocupar por completo.

¿Te resultó útil este artículo?