Guía: implementar health checks y readiness probes para aplicaciones PHP en Docker Compose
Por Kevin Marcondes · Publicado el · 10 min de lectura
Esta guía muestra cómo implementar health checks y readiness probes para aplicaciones PHP en Docker Compose. Verás la diferencia entre liveness y readiness, cómo exponer endpoints mínimos, configurar docker-compose.yml, integrar Nginx y cubrir workers de colas. Incluye ejemplos prácticos y consejos de calibración para producción.
Conceptos: liveness, readiness y cómo aplicarlo en Docker Compose
Antes de configurar, aclara los términos. Liveness verifica si el proceso sigue vivo. Readiness indica si la aplicación puede recibir tráfico con seguridad. En Kubernetes hay checks separados; en Docker Compose existe un único healthcheck por servicio, pero podemos emular readiness con endpoints y reglas de enrutamiento.
Con checks bien definidos evitas despliegues rotos, errores intermitentes al arrancar y loops de reinicio. También reduces incidentes tras migraciones o cuando dependencias externas fluctúan.
Para visualizar estados de salud mediante interfaz, usar una GUI como Portainer facilita la inspección de servicios.
- Liveness: reinicia/elimina cuando el proceso se ha colgado.
- Readiness: retiene el tráfico hasta que las dependencias estén listas.
- Startup: da tiempo extra antes de evaluar la salud.
| Concepto | Qué verifica | Acción al fallar | En Docker Compose |
|---|---|---|---|
| Liveness | El proceso responde | Reiniciar el contenedor | healthcheck + política de restart |
| Readiness | Puede recibir tráfico | Sacar del balanceador / retener tráfico | Endpoint /ready + reverse proxy; depends_on: service_healthy |
| Startup | Se inicializó correctamente | Esperar antes de chequear | start_period en el healthcheck; scripts wait-for |
Endpoints mínimos en PHP: /health (liveness) y /ready (readiness)
Endpoints HTTP simples funcionan bien. /health devuelve 200 cuando el proceso está vivo. /ready devuelve 200 solo si las dependencias esenciales (por ejemplo, base de datos y cache) están accesibles y en el estado esperado.
Mantén los checks rápidos. Evita consultas pesadas. Usa timeouts cortos. No expongas detalles sensibles en la respuesta. Si es posible, limita estos endpoints a la red interna del Compose.
- /health: sin dependencias externas, respuesta rápida.
- /ready: comprueba conexiones ligeras a BD, cache y cola.
- Respuestas planas (texto) y códigos HTTP claros: 200 o 503.
<?php
// public/healthz.php
// Um único script que responde /health e /ready
header('Content-Type: text/plain');
$path = parse_url($_SERVER['REQUEST_URI'] ?? '/health', PHP_URL_PATH);
if ($path === '/health') {
echo 'OK';
exit(0);
}
if ($path === '/ready') {
$errors = [];
// Banco (ex.: Postgres)
try {
$dsn = sprintf(
'pgsql:host=%s;port=%s;dbname=%s',
getenv('DB_HOST') ?: 'db',
getenv('DB_PORT') ?: '5432',
getenv('DB_NAME') ?: 'app'
);
$pdo = new PDO($dsn, getenv('DB_USER') ?: 'app', getenv('DB_PASS') ?: 'secret', [PDO::ATTR_TIMEOUT => 2]);
$pdo->query('SELECT 1');
} catch (Throwable $e) {
$errors[] = 'db';
}
// Cache (ex.: Redis)
try {
if (class_exists('Redis')) {
$r = new Redis();
$r->connect(getenv('REDIS_HOST') ?: 'redis', (int)(getenv('REDIS_PORT') ?: 6379), 1.5);
$r->ping();
}
} catch (Throwable $e) {
$errors[] = 'redis';
}
if ($errors) {
http_response_code(503);
echo 'NOT_READY: ' . implode(',', $errors);
exit(1);
}
echo 'READY';
exit(0);
}
http_response_code(404);
echo 'not found';
docker-compose.yml con healthcheck, start_period y depends_on
En Docker Compose define healthcheck por servicio. Usa start_period para dar tiempo de calentamiento antes de evaluar. Emula readiness conectando Nginx solo cuando la app esté sana con depends_on: condition: service_healthy.
El ejemplo abajo usa: app (PHP-FPM), nginx, db y redis. La app expone un healthcheck vía CLI para readiness. Nginx hace liveness vía HTTP. Para imágenes pequeñas, consulta Imágenes Docker ligeras con multistage.
- Usa retries para evitar falsos negativos.
- Ajusta interval, timeout y start_period según tu entorno.
- Define políticas de restart apropiadas (p. ej.: unless-stopped).
version: "3.9"
services:
app:
image: php:8.2-fpm-alpine
working_dir: /var/www/html
volumes:
- ./:/var/www/html
environment:
DB_HOST: db
DB_PORT: "5432"
DB_NAME: app
DB_USER: app
DB_PASS: secret
REDIS_HOST: redis
REDIS_PORT: "6379"
healthcheck:
test: ["CMD-SHELL", "php /var/www/html/health/ready-cli.php"]
interval: 10s
timeout: 3s
retries: 5
start_period: 25s
restart: unless-stopped
nginx:
image: nginx:alpine
ports: ["8080:80"]
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
- ./:/var/www/html:ro
depends_on:
app:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/health || exit 1"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stopped
db:
image: postgres:15
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: secret
restart: unless-stopped
redis:
image: redis:7
restart: unless-stopped
Workers de colas y cron: healthcheck y heartbeat fiables
Los workers no tienen endpoint HTTP. Haz que el healthcheck ejecute un script CLI que valide dependencias y un heartbeat de progreso. Cada N segundos el worker actualiza un archivo o clave en Redis. El healthcheck falla si el heartbeat queda congelado.
Esto diferencia una pausa natural (sin jobs) de un bloqueo. En frameworks como Laravel puedes incluir métricas en el script (p. ej., tamaño de la cola).
- Actualiza un heartbeat con timestamp en cada iteración.
- En el healthcheck, marca como fallo si el heartbeat está desactualizado.
- Valida conexiones a Redis/DB con timeouts cortos.
<?php
// health/worker_health.php
// 1) Worker debe actualizar este archivo/chave con timestamps.
// 2) Este script roda no healthcheck do container do worker.
$now = time();
$maxAge = (int)(getenv('HEARTBEAT_MAX_AGE') ?: 60); // segundos
$hbFile = getenv('HEARTBEAT_FILE') ?: '/tmp/worker_heartbeat';
$errors = [];
try {
// Ex.: validar Redis rapidamente
if (class_exists('Redis')) {
$r = new Redis();
$r->connect(getenv('REDIS_HOST') ?: 'redis', (int)(getenv('REDIS_PORT') ?: 6379), 1.5);
$r->ping();
}
} catch (Throwable $e) {
$errors[] = 'redis';
}
$age = is_file($hbFile) ? ($now - filemtime($hbFile)) : PHP_INT_MAX;
if ($age > $maxAge) {
$errors[] = 'stale_heartbeat';
}
if ($errors) {
fwrite(STDERR, 'UNHEALTHY: '.implode(',', $errors)."\n");
exit(1);
}
echo "OK\n";
Calibrando interval, timeout, retries y start_period
Valores inadecuados generan falsos negativos o alertas tardías. Ajusta según el rendimiento de la app y el entorno.
Buenas prácticas para empezar y luego refinar con datos reales:
- interval: 10s a 30s para servicios en producción.
- timeout: 2s a 5s. Si es mayor que la latencia típica, investigue.
- retries: 3 a 5 para amortiguar oscilaciones puntuales.
- start_period: cubre el tiempo de arranque + calentamiento de cache.
- Diferencia /health (rápido) y /ready (con dependencias).
Puerta de tráfico con Nginx y FPM ping (readiness sin orquestador)
Sin Kubernetes, aún puedes tener readiness: sirve /health estático y /ready pasando a PHP. Mientras /ready no devuelva 200, haz que Nginx responda 503, evitando tráfico prematuro. Opcionalmente, habilita el ping de PHP-FPM y monitorízalo.
Consulta la documentación de PHP-FPM en php.net: https://www.php.net/manual/en/install.fpm.configuration.php.
- /health: respuesta estática 200 en Nginx.
- /ready: ruta PHP que prueba BD/Redis y devuelve 200/503.
- Ping de FPM opcional para liveness interno.
server {
listen 80;
server_name _;
root /var/www/html/public;
# Liveness simple y rápido
location = /health { return 200 "OK"; add_header Content-Type text/plain; }
# Readiness: sólo 200 cuando la app esté realmente lista
location = /ready {
fastcgi_pass app:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME /var/www/html/public/healthz.php;
fastcgi_param REQUEST_URI /ready;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass app:9000;
}
}
# Opcional (en php-fpm www.conf del container app):
# ping.path = /fpm-ping
# ping.response = pong
# pm.status_path = /fpm-status
Observabilidad: registra y monitorea tus checks
Logs y métricas ayudan a explicar por qué un servicio quedó unhealthy. Centraliza logs de los contenedores y crea un panel con series de los healthchecks y respuestas de /ready.
Para montar rápidamente un stack de métricas en Compose, consulta la guía de Prometheus y Grafana. Para logs estructurados en apps PHP, ve Fluentd y Elasticsearch.
Si usas un proxy dinámico, la comparativa Traefik vs Nginx puede ayudarte a decidir dónde aplicar gates de readiness.
Cómo validar que funcionó y troubleshooting común
Después de levantar el entorno, valida manualmente y fuerza fallos para observar el comportamiento. Prueba también la ruta del worker.
Los errores comunes suelen ser fáciles de corregir cuando sabes dónde mirar.
- Fallo por timeout muy alto/bajo: ajusta timeout y retries.
- /ready lento: simplifica las comprobaciones y añade timeouts de conexión.
- Endpoint expuesto públicamente: restringe por red o firewall.
- Nombres de servicio erróneos en Compose: alinea hosts con los nombres de los servicios.
- Nginx apuntando al script incorrecto: revisa SCRIPT_FILENAME.
# Suba el stack
docker compose up -d
# Verifique estados de salud (Compose v2)
docker compose ps
# Teste endpoints a través de Nginx
curl -i http://localhost:8080/health
curl -i http://localhost:8080/ready
# Force una falla de readiness (detenga Redis)
docker compose stop redis
curl -i http://localhost:8080/ready # debe devolver 503
Preguntas frecuentes
¿Cuál es la diferencia práctica entre /health y /ready en Docker Compose?
/health verifica que el proceso está vivo y responde rápido, sin tocar dependencias externas. /ready comprueba si la aplicación ya puede recibir tráfico, como conexión a base de datos y cache. En Compose usas healthcheck para liveness y gates en Nginx junto con depends_on para readiness.
¿Necesito Kubernetes para tener readiness de verdad?
No. En Compose puedes emular readiness con un endpoint /ready, Nginx devolviendo 503 mientras no esté listo y depends_on: condition: service_healthy para ordenar el arranque de servicios consumidores. No es tan sofisticado como un orquestador, pero cubre la mayoría de casos.
¿Debo poner autenticación en los endpoints de health?
En general, no. Los health checks deben ser rápidos y simples. Prefiere restringir el acceso por red (solo dentro del cluster/Compose) o por firewall. Evita exponerlos públicamente. Nunca devuelvas detalles sensibles, solo códigos y mensajes mínimos.
¿Cómo probar base de datos y Redis sin hacer /ready lento?
Usa operaciones ligeras con timeouts cortos, como SELECT 1 y PING, y evita consultas pesadas. Realiza conexiones con timeouts de 1–3 segundos y falla rápido. Si el entorno sufre picos de latencia, aumenta los retries del healthcheck para reducir falsos negativos.
¿Cómo monitorizar los health checks a lo largo del tiempo?
Centraliza logs de los endpoints y genera métricas en tus dashboards. Una opción práctica es levantar Prometheus y Grafana con Compose para seguir variaciones y fallos. Consulta la guía de métricas con Compose en /es/prometheus-grafana-docker-compose-vps.
¿Puedo usar el ping de PHP-FPM en lugar de /health?
Sí. El ping de FPM es útil para liveness del proceso PHP-FPM. Habilita ping.path y expón una ruta en Nginx que lo reenvíe al FPM. Para readiness completo conserva un /ready que valide las dependencias de la aplicación.
¿Quieres revisar tus health checks antes del próximo deploy?
Comparte tu docker-compose.yml y el diagrama de tu app PHP. Podemos evaluar endpoints, calibración y Nginx para reducir fallos en producción.
Hablar sobre mi entornoVer también
- Guía práctica: configurar failover activo‑pasivo entre dos VPS con Keepalived, HAProxy y lsyncd para uploads sincronizados
- Guía: checklist técnico para preparar un VPS Debian con Docker Compose para aplicaciones PHP en producción
- Guía práctica para dimensionamiento de VPS y tuning de PHP‑FPM para tiendas Magento/OpenMage con picos estacionales: del dimensionamiento a la prueba de carga
- Guía: cómo planificar y ejecutar pruebas de carga en aplicaciones PHP monolito usando k6 y Docker Compose — paso a paso