Límites de recursos, healthchecks y políticas de reinicio en Docker Compose

13 min de lectura·Matthieu·vpsproductioncontainersdocker-composedocker|

Tu archivo Compose funciona en desarrollo pero no está listo para producción. Aprende a añadir límites de memoria/CPU, healthchecks, políticas de reinicio y ordenación de arranque para proteger tu VPS de OOM kills y fallos en cascada.

Un archivo Compose que ejecuta docker compose up sin errores no está listo para producción. Sin límites de recursos, un solo contenedor puede consumir toda la memoria del host y activar el OOM killer de Linux, tumbando cada servicio del VPS. Sin healthchecks, Docker no tiene forma de detectar un proceso colgado. Sin políticas de reinicio, un contenedor caído permanece muerto hasta que lo notes.

Estos tres sistemas trabajan juntos: los límites de recursos previenen el consumo descontrolado, los healthchecks detectan fallos, y las políticas de reinicio se encargan de la recuperación. Esta guía cubre los tres como una capa integrada de endurecimiento para Docker Compose v2.

Requisitos previos: Una instalación funcional de Docker Compose en un VPS. Familiaridad con la estructura básica de un archivo Compose. Si necesitas un repaso, consulta .

¿Cómo se configuran los límites de memoria y CPU en Docker Compose?

Usa la clave deploy.resources.limits en la definición de tu servicio. Establece memory con un valor como 512M o 1G para crear un techo fijo. Establece cpus con una cadena decimal como '0.5' para medio núcleo. El proceso del contenedor muere por OOM kill si supera el límite de memoria. Los límites de CPU ralentizan el proceso en lugar de matarlo.

services:
  api:
    image: node:22-slim
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.25'
          memory: 256M

Qué hace esto: El contenedor api puede usar como máximo 1 núcleo de CPU y 512 MB de RAM. Docker garantiza que al menos 0,25 núcleos y 256 MB estén disponibles para él, incluso cuando otros contenedores compiten por recursos.

Verifica que los límites están aplicados:

docker compose up -d
docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"

La columna MEM USAGE / LIMIT muestra tanto el uso actual como el techo configurado. Deberías ver 512MiB como límite, no la RAM total del host. Si ves la memoria total del host, los límites no están activos.

¿Cuál es la diferencia entre limits y reservations?

Los limits son techos estrictos. El contenedor no puede superarlos. Las reservations son garantías flexibles. Docker las usa para decisiones de planificación y gestión de presión de memoria.

Configuración Función Cuándo importa
limits.memory Techo estricto. OOM kill si se supera. Siempre. Previene contenedores descontrolados.
limits.cpus Limitación. El proceso se ralentiza. Cargas pesadas de CPU (builds, inferencia).
reservations.memory Mínimo garantizado. Host bajo presión de memoria.
reservations.cpus Cuota de CPU garantizada. Múltiples contenedores pesados en CPU.

Si omites las reservations, Docker asigna recursos por orden de llegada. Bajo presión, cualquier contenedor puede quedarse sin recursos. Configura las reservations al mínimo que tu servicio necesita para funcionar.

¿Qué ocurre cuando un contenedor Docker supera su límite de memoria?

El OOM killer del kernel Linux termina el proceso principal del contenedor con SIGKILL. Docker registra el código de salida 137 (128 + 9, donde 9 es SIGKILL). Si la política de reinicio es on-failure o always, Docker reinicia el contenedor automáticamente.

Comprueba si un contenedor fue víctima de un OOM kill:

docker inspect api-1 --format '{{.State.OOMKilled}}'

Salida: true confirma un OOM kill.

Para más detalle:

docker inspect api-1 --format '{{json .State}}' | python3 -m json.tool

Busca "OOMKilled": true, "ExitCode": 137, y "RestartCount" para ver cuántas veces se reinició.

Sin límites, el contenedor asigna memoria hasta agotar el host. El kernel entonces mata procesos a nivel de sistema para liberar RAM. Esto puede tumbar tu base de datos, reverse proxy o daemon SSH. Los límites confinan el radio de impacto al contenedor problemático.

¿Cómo planificar el presupuesto de recursos de contenedores en un VPS?

En un VPS con recursos fijos, debes repartir la memoria entre todos los contenedores. Deja margen para el sistema operativo del host y Docker.

Ejemplo de presupuesto para un VPS de 8 GB:

Componente Memoria
SO del host + motor Docker 1 GB
PostgreSQL 2 GB
Redis 512 MB
API Node.js 1 GB
Nginx 128 MB
Worker en segundo plano 1 GB
Margen (sin asignar) 2,35 GB

Mantén un 20-30 % sin asignar como margen. Si la suma de los límites de tus contenedores supera la RAM total del host, te arriesgas a que el OOM killer del host intervenga, ignorando las fronteras de Docker.

Verifica la asignación total contra la memoria del host:

free -h
docker stats --no-stream --format "{{.Name}}: {{.MemUsage}}"

¿Cómo se configura un healthcheck en Docker Compose?

Añade un bloque healthcheck a la definición de tu servicio. Docker ejecuta el comando de prueba en el intervalo especificado y marca el contenedor como unhealthy tras el número configurado de fallos consecutivos. Otros servicios pueden depender de este estado de salud para la ordenación del arranque.

services:
  api:
    image: node:22-slim
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s
      start_interval: 2s

Parámetros del healthcheck:

Parámetro Por defecto Recomendado Función
interval 30s 15-60s Tiempo entre comprobaciones tras el arranque
timeout 30s 5-10s Tiempo máximo de una comprobación
retries 3 3-5 Fallos antes de unhealthy
start_period 0s 10-60s Periodo de gracia para servicios de arranque lento
start_interval 5s 2-5s Intervalo durante el arranque (Compose v2.20+)

El parámetro start_interval (añadido en Compose v2.20) permite comprobar con más frecuencia durante el arranque. El contenedor pasa de starting a healthy en cuanto la primera comprobación tiene éxito durante el start_period. Después, las comprobaciones se ejecutan en el interval normal.

¿Cuál es la diferencia entre CMD y CMD-SHELL en los healthchecks?

CMD ejecuta el comando directamente sin shell. CMD-SHELL lo pasa por /bin/sh -c. Usa CMD siempre que sea posible. Evita la sobrecarga del shell y elimina problemas de PID 1 donde el shell, no tu comando de comprobación, recibe las señales.

# Formato CMD - sin shell, ejecuta el binario directamente
healthcheck:
  test: ["CMD", "pg_isready", "-U", "postgres"]

# Formato CMD-SHELL - pasa por /bin/sh -c
healthcheck:
  test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]

# Atajo en cadena - equivalente a CMD-SHELL
healthcheck:
  test: curl -f http://localhost:3000/health || exit 1

Usa CMD-SHELL cuando necesites funcionalidades del shell como ||, pipes o expansión de variables. Usa CMD para ejecución simple de binarios.

¿Qué comando de healthcheck usar para PostgreSQL, Redis y Nginx?

Cada servicio necesita un healthcheck que pruebe la capacidad del servicio para procesar peticiones, no solo si el proceso está corriendo.

Servicio Comando healthcheck Qué prueba
PostgreSQL ["CMD", "pg_isready", "-U", "postgres"] Acepta conexiones
Redis ["CMD", "redis-cli", "ping"] Responde a comandos
Nginx `["CMD-SHELL", "curl -f http://localhost/
App Node.js `["CMD-SHELL", "curl -f http://localhost:3000/health
MySQL/MariaDB ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] Motor listo, no solo el socket abierto

Importante: Para healthchecks basados en curl, la imagen debe incluir curl. Muchas imágenes slim no lo traen. Instálalo en tu Dockerfile, usa wget en su lugar, o escribe un endpoint de salud mínimo que tu framework sirva de forma nativa.

Verifica el estado del healthcheck:

docker compose ps

Busca (healthy) o (unhealthy) en la columna STATUS. Para el historial detallado:

docker inspect api-1 --format '{{json .State.Health}}' | python3 -m json.tool

Esto muestra los últimos resultados de comprobación, incluyendo la salida stdout/stderr de las comprobaciones fallidas. Presta atención: si FailingStreak sigue incrementándose, tu comando de comprobación está mal o el servicio tiene un problema real.

¿Cómo interactúan las políticas de reinicio con los healthchecks?

Las políticas de reinicio controlan lo que hace Docker cuando un contenedor se detiene. Los healthchecks controlan cómo Docker detecta problemas en contenedores en ejecución. Juntos, crean un bucle de recuperación automática: el healthcheck detecta el fallo, el contenedor se detiene, y la política de reinicio lo levanta de nuevo.

services:
  api:
    restart: on-failure:5
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 10s

Comparación de políticas de reinicio

Política En caso de crash Al reiniciar En docker stop Ideal para
no Queda muerto Queda muerto Queda muerto Tareas puntuales, depuración
always Reinicia Reinicia Reinicia Infraestructura base (bases de datos, proxys)
unless-stopped Reinicia Reinicia Queda muerto La mayoría de servicios en producción
on-failure:N Reinicia (hasta N veces) Queda muerto Queda muerto Servicios que no deben reiniciar indefinidamente

on-failure:5 significa que Docker reintenta hasta 5 veces. Después, el contenedor queda muerto. Esto previene bucles de reinicio que desperdician CPU en un servicio fundamentalmente roto.

La interacción OOM + reinicio: Cuando un contenedor alcanza su límite de memoria y sufre un OOM kill (código de salida 137), Docker lo trata como un fallo. Con on-failure:5, reinicia hasta 5 veces. Si el servicio tiene una fuga de memoria, será matado y reiniciado repetidamente hasta alcanzar el límite de reintentos. Comprueba el contador de reinicios:

docker inspect api-1 --format '{{.RestartCount}}'

Para la mayoría de servicios en producción, usa unless-stopped. Reinicia en caso de crash y reinicio del host, pero respeta los comandos manuales docker compose stop. Usa on-failure:N para servicios donde un bucle de crash debe disparar una alerta, no reintentar en silencio indefinidamente.

¿Cómo hacer que un servicio espere a que otro esté sano?

Usa depends_on con condition: service_healthy. Esto hace que Docker Compose espere a que el healthcheck de la dependencia pase antes de iniciar el servicio dependiente.

services:
  db:
    image: postgres:17
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s
    restart: unless-stopped

  api:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 3
    restart: unless-stopped

Sin condition: service_healthy, depends_on solo espera a que el contenedor arranque, no a que el servicio dentro de él esté listo. PostgreSQL tarda varios segundos en inicializarse. Tu aplicación fallaría al intentar conectarse a una base de datos que aún no acepta conexiones.

La opción restart: true dentro de depends_on (Compose v2.21+) indica a Docker que reinicie el servicio dependiente si la dependencia se reinicia:

depends_on:
  db:
    condition: service_healthy
    restart: true

Esto es útil cuando tu aplicación cachea la conexión a la base de datos y no puede recuperarse de un reinicio de la base de datos sin un reinicio completo.

¿Qué ulimits configurar para contenedores en producción?

Configura nofile (descriptores de archivo abiertos) y nproc (número máximo de procesos) para servicios que manejan muchas conexiones simultáneas. Cada conexión TCP, archivo abierto y pipe consume un descriptor de archivo. El límite por defecto (1024 en muchas imágenes) es demasiado bajo para bases de datos y servicios de alto tráfico.

services:
  db:
    image: postgres:17
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
      nproc:
        soft: 4096
        hard: 4096

Verificación dentro del contenedor:

docker compose exec db cat /proc/1/limits

Busca Max open files y Max processes. Los valores deben coincidir con los que configuraste.

Prevención de fork bombs con límite de PIDs

Configura deploy.resources.limits.pids para limitar el número de procesos que un contenedor puede crear. Esto previene que fork bombs y la creación descontrolada de procesos consuman todos los PIDs del host.

services:
  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          pids: 200

Si no usas deploy.resources para límites de CPU/memoria, la clave de nivel superior pids_limit también funciona. Pero cuando deploy.resources.limits está presente, debes poner el límite de PIDs ahí también. Mezclar ambos causa un error de validación en Compose v5+.

200 PIDs es generoso para una aplicación web típica. Una app Node.js usa entre 10 y 30. PostgreSQL usa aproximadamente un proceso por conexión más workers en segundo plano. Dimensiona a 2-3x tu pico esperado.

¿Cómo limitar el tamaño de los logs de contenedores?

Sin límites de logs, los logs de los contenedores crecen sin control. Un servicio verboso puede llenar tu disco en horas. Configura max-size y max-file en el driver de logging json-file para rotar logs automáticamente.

services:
  api:
    image: myapp:latest
    logging:
      driver: "json-file"
      options:
        max-size: "10m"
        max-file: "3"

Esto mantiene como máximo 3 archivos de 10 MB cada uno, limitando el almacenamiento de logs a 30 MB por servicio. Ajusta según tus necesidades de depuración. Usa un ancla YAML para aplicar la misma configuración de logs a todos los servicios:

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

services:
  api:
    image: myapp:latest
    logging: *default-logging
  worker:
    image: myapp:latest
    logging: *default-logging

Configurar stop_grace_period para un apagado limpio

Cuando Docker detiene un contenedor, envía SIGTERM y espera a que el proceso termine de forma ordenada. Si el proceso no termina dentro del periodo de gracia, Docker envía SIGKILL. El valor por defecto es 10 segundos.

services:
  db:
    image: postgres:17
    stop_grace_period: 30s

  api:
    image: myapp:latest
    stop_grace_period: 5s

Las bases de datos necesitan periodos de gracia más largos para volcar escrituras y cerrar conexiones limpiamente. Los servidores web y procesos de API normalmente terminan en pocos segundos. Configura el periodo de gracia según el tiempo real de apagado de tu servicio, con un pequeño margen.

Archivo Compose completo listo para producción

Este ejemplo combina todas las configuraciones para un stack de aplicación web típico: reverse proxy Nginx, API Node.js, base de datos PostgreSQL y caché Redis.

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "10m"
    max-file: "3"

services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "80:80"
      - "443:443"
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 128M
        reservations:
          memory: 64M
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost/ || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
    restart: unless-stopped
    stop_grace_period: 5s
    logging: *default-logging
    depends_on:
      api:
        condition: service_healthy

  api:
    image: myapp:latest
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
          pids: 200
        reservations:
          cpus: '0.25'
          memory: 256M
    healthcheck:
      test: ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval: 30s
      timeout: 5s
      retries: 3
      start_period: 15s
      start_interval: 2s
    restart: unless-stopped
    stop_grace_period: 5s
    logging: *default-logging
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_healthy

  db:
    image: postgres:17
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 2G
        reservations:
          cpus: '0.5'
          memory: 1G
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "postgres"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 15s
    restart: unless-stopped
    stop_grace_period: 30s
    ulimits:
      nofile:
        soft: 65536
        hard: 65536
    logging: *default-logging
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
        reservations:
          memory: 128M
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
    restart: unless-stopped
    stop_grace_period: 5s
    logging: *default-logging

volumes:
  pgdata:

secrets:
  db_password:
    file: ./secrets/db_password.txt

Nota: La contraseña de la base de datos usa Docker secrets con POSTGRES_PASSWORD_FILE en lugar de una variable de entorno POSTGRES_PASSWORD en texto plano. Crea el archivo de secrets con permisos restringidos:

mkdir -p secrets
openssl rand -base64 32 > secrets/db_password.txt
chmod 600 secrets/db_password.txt

Lista de verificación

Después de aplicar todas las configuraciones, recorre estas comprobaciones para confirmar que todo está activo.

1. Comprobar que los límites de recursos están aplicados:

docker stats --no-stream --format "table {{.Name}}\t{{.MemUsage}}\t{{.CPUPerc}}"

La columna MEM USAGE / LIMIT muestra tanto el uso actual como el techo configurado. Cada contenedor debería mostrar su límite de memoria configurado, no la RAM total del host.

2. Comprobar el estado de los healthchecks:

docker compose ps

Todos los servicios deberían mostrar (healthy) en la columna STATUS. Si alguno muestra (health: starting), espera a que termine el start_period.

3. Comprobar la política de reinicio:

docker inspect --format '{{.HostConfig.RestartPolicy.Name}}:{{.HostConfig.RestartPolicy.MaximumRetryCount}}' $(docker compose ps -q)

4. Comprobar ulimits dentro de un contenedor:

docker compose exec db cat /proc/1/limits | grep -E "open files|processes"

5. Comprobar la configuración de logs:

docker inspect --format '{{.HostConfig.LogConfig.Type}} max-size={{index .HostConfig.LogConfig.Config "max-size"}} max-file={{index .HostConfig.LogConfig.Config "max-file"}}' $(docker compose ps -q api)

6. Probar la cadena de recuperación completa:

Detén un contenedor y observa su recuperación:

docker compose stop api
docker compose ps  # api debería mostrar Exited
docker compose start api
docker compose ps  # api debería mostrar (healthy) tras el start_period
docker inspect $(docker compose ps -q api) --format 'RestartCount: {{.RestartCount}}'

Para probar el reinicio automático en un crash real, baja el límite de memoria por debajo del mínimo de la aplicación. La política de reinicio se activa cuando el proceso termina por sí solo. Ten en cuenta que docker kill no activa las políticas de reinicio en versiones recientes de Docker.

Referencia rápida de dimensionamiento de recursos

Puntos de partida para servicios comunes en un VPS. Ajusta según tu carga real.

Servicio Límite de memoria Límite de CPU Healthcheck
PostgreSQL 1-4 GB 1.0-2.0 pg_isready -U postgres
Redis 256M-1G 0.25-0.5 redis-cli ping
API Node.js 256M-1G 0.5-1.0 curl -f http://localhost:PORT/health
Nginx 64M-256M 0.25-0.5 curl -f http://localhost/
Ollama (LLM) 4-8 GB 2.0-4.0 curl -f http://localhost:11434/
Worker en segundo plano 256M-1G 0.5-1.0 Comprobación específica de la aplicación

¿Algo salió mal?

El contenedor se reinicia en bucle:

docker compose logs api --tail 50
docker inspect api-1 --format '{{.State.ExitCode}} OOM:{{.State.OOMKilled}} Restarts:{{.RestartCount}}'

Si OOMKilled: true, aumenta el límite de memoria. Si el código de salida no es 137, revisa los logs de la aplicación para encontrar el error real.

El healthcheck siempre falla:

docker inspect api-1 --format '{{json .State.Health.Log}}' | python3 -m json.tool

Esto muestra la salida de cada comprobación. Causas comunes: el endpoint de salud no existe, curl no está instalado en la imagen, o el servicio escucha en un puerto diferente al que apunta la comprobación.

depends_on no espera:

Asegúrate de que la dependencia tiene un healthcheck definido. Sin él, condition: service_healthy no tiene nada que esperar y Compose dará error al arrancar.

Los límites no aparecen en docker stats:

Verifica que estás usando Docker Compose v2 (el plugin docker compose, no el viejo binario docker-compose). Comprueba tu versión:

docker compose version

La clave deploy.resources requiere Compose v2. Si estás en una versión anterior, consulta para instrucciones de instalación.

Leer logs cuando algo falla:

journalctl -u docker -f
docker compose logs -f --tail 100

El log del daemon Docker muestra eventos OOM y cambios en el ciclo de vida de los contenedores. Los logs de Compose muestran la salida de la aplicación.


Copyright 2026 Virtua.Cloud. Todos los derechos reservados. Este contenido es una obra original del equipo de Virtua.Cloud. La reproducción, republicación o redistribución sin permiso escrito está prohibida.

¿Listo para probarlo?

Despliega tu propio servidor en segundos. Linux, Windows o FreeBSD.

Ver planes VPS