Guía: implementar health checks y readiness probes para aplicaciones PHP en Docker Compose

Por · Publicado el · 10 min de lectura

Bloques de vidrio cuadrados con reflejos azulados formando un patrón geométrico
Textura moderna y neutra, adecuada como fondo para contenido técnico sobre infraestructura. Foto: Chris F / Pexels

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 entorno

Ver también