Límites de recursos, healthchecks y políticas de reinicio en Docker Compose
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