772 - Evita Contenedores ZOMBIE. Guía Maestra de Health Checks en Podman
Aprende a usar Health Checks en Podman para evitar contenedores zombie, configurar dependencias inteligentes y automatizar reinicios en Linux. ¡Domínalo ya!
En el episodio 688 titulado Tu contenedor Docker podría estar muerto y tu sin enterarte te estuve hablando de los healthchecks en Docker, y de cómo utilizarlos para asegurarte de que tus contenedores están vivos y funcionando correctamente. En este episodio vamos a ver cómo implementar healthchecks nativos en Podman, en todas sus variantes para poder exprimirlos al máximo y asegurarnos que todos nuestros servicios están vivitos y coleando.

Evita Contenedores ZOMBIE. Guía Maestra de Health Checks en Podman
¿Qué es un Healthcheck?
Básicamente, un healthcheck es una prueba que el motor de contenedores ejecuta periódicamente para determinar si el proceso dentro del contenedor no solo está vivo (corriendo), sino que es realmente funcional.
¿Por qué utilizar los healthchecks?
A menudo, un contenedor puede estar en estado running, pero el servicio interno está bloqueado, sufriendo un deadlock o simplemente no responde a peticiones.
- Detección de Zombies. Un proceso puede estar en ejecución pero ser incapaz de conectar con la base de datos.
- Autorreparación. Herramientas de orquestación (o incluso scripts sencillos) pueden usar este estado para reiniciar el contenedor automáticamente si falla.
- Dependencias inteligentes: Permite que otros contenedores esperen a que un servicio esté sano (healthy) antes de arrancar ellos mismos.
- Visibilidad: En lugar de entrar al contenedor a hacer un curl, con un simple
podman psves el estado de salud.
¿Como utilizarlos en Podman?
Básicamente puedes hacerlo de tres formas diferentes, dependiendo de las circunstancias.
La primea es definiéndolo directamente en el Dockerfile, igual que en Docker. Esta es la forma recomendada si tienes control sobre la imagen del contenedor. Pero, claro, para esto tienes que tener acceso al Dockerfile original, y esto no siempre es así.
FROM fedora:latest
RUN dnf install -y httpd && dnf clean all
HEALTHCHECK --interval=1m --timeout=3s \
CMD curl -f http://localhost/ || exit 1
ENTRYPOINT ["/usr/sbin/httpd", "-D", "FOREGROUND"]
La siguiente de las formas es desde la línea de comandos con podman run. Por ejemplo si quieres un healthcheck que haga un curl a la raíz del servidor web cada minuto, y si no responde en 3 segundos, lo considere no saludable,
podman run -d --name mi_apache \
--health-cmd="curl -f http://localhost/ || exit 1" \
--health-interval=1m \
--health-timeout=3s \
httpd:latest
Que vemos aquí,
--health-cmd. El comando a ejecutar. Si devuelve 0, está sano, si devuelve 1, está enfermo.--health-interval. Cada cuánto tiempo se realiza la prueba.--health-timeout. Cuánto tiempo esperar antes de considerar que la prueba ha fallado.--health-retries. Número de fallos consecutivos antes de marcarlo como unhealthy.
Y la tercera es definiéndola en tu quadlet o archivo de systemd si estás utilizando Podman con systemd. Por ejemplo, en un archivo .container podrías tener algo así:
[Unit]
Description=Servicio Nginx con Healthcheck
[Container]
Image=docker.io/library/nginx:latest
PublishPort=8080:80
# --- Configuración del Healthcheck ---
HealthCmd=curl -f http://localhost/ || exit 1
HealthInterval=30s
HealthRetries=3
HealthTimeout=10s
HealthStartPeriod=1m
[Install]
WantedBy=multi-user.target default.target
Aquí,
HealthCmd. El comando que determina la salud. Es vital que el binario (como curl) exista en la imagen.HealthInterval. El tiempo entre pruebas. Soporta unidades de tiempo de systemd (s, m, h).HealthStartPeriod. Muy útil para aplicaciones pesadas (como una base de datos o una app en Java/Rust). Ignora los fallos durante el tiempo de arranque inicial para no marcar el contenedor como «unhealthy» antes de que termine de cargar.
Al usar Quadlets, el ciclo de vida del contenedor lo gestiona systemd. Esto abre una posibilidad muy potente, que es la integración con el estado del servicio.
- Visibilidad con systemctl. Cuando defines un healthcheck, el estado de salud se reporta al motor de Podman. Si haces un systemctl status mi-servicio, verás si el servicio está activo, pero con podman ps verás si realmente está funcionando.
- Auto-reinicio (Self-healing). Aunque el Quadlet por sí solo no reinicia el contenedor si el healthcheck falla (solo cambia el estado a unhealthy), puedes combinarlo con herramientas como Keepalived o scripts de monitoreo que lean el estado de Podman para forzar un systemctl restart.
Un ejemplo de la potencia de los Healthchecks
Veamos la potencia de los Healthchecks con un ejemplo. Para lograr que systemd no solo sepa que el contenedor ha arrancado, sino que espere a que sea realmente funcional antes de dar el servicio por listo, utilizaremos un Quadlet combinado con una técnica de bloqueo en el arranque.
Para esto, lo mejor es utilizar una base de datos y un servicio que dependa de ella. Por ejemplo, un contenedor de WordPress que depende de una base de datos MariaDB.
[Unit]
Description=Base de Datos con Healthcheck bloqueante
[Container]
Image=docker.io/library/mariadb:latest
Environment=MARIADB_ROOT_PASSWORD=atareao
HealthCmd=mariadb-admin ping -p${MARIADB_ROOT_PASSWORD} || exit 1
HealthInterval=5s
HealthRetries=5
HealthStartPeriod=10s
# Esta es la magia:
# Forzamos a que el servicio no se considere 'up' hasta que el healthcheck pase
ExecStartPost=/usr/bin/bash -c 'while [ "$(podman inspect --format "{{.State.Health.Status}}" %N)" != "healthy" ]; do sleep 2; done'
[Install]
WantedBy=multi-user.target
Para integrar los healthchecks en tu despliegue de WordPress con Quadlets, vamos a modificar los archivos para que el contenedor de WordPress no intente conectarse antes de que MariaDB y Redis acepten conexiones.
[Unit]
Description=Base de Datos MariaDB para WordPress
[Container]
Image=docker.io/library/mariadb:10.11
ContainerName=wp-mariadb
Environment=MYSQL_ROOT_PASSWORD=atareao_root
Environment=MYSQL_DATABASE=wordpress
Environment=MYSQL_USER=lorenzo
Environment=MYSQL_PASSWORD=atareao_pass
Volume=wp-db-data.volume:/var/lib/mysql:Z
Network=wp-network.network
# Healthcheck: verifica si MariaDB responde
HealthCmd=mariadb-admin ping -h localhost -u lorenzo -p${MYSQL_PASSWORD} || exit 1
HealthInterval=5s
HealthRetries=5
HealthStartPeriod=15s
[Service]
Restart=always
# Bloquea el estado 'active' hasta que esté healthy
ExecStartPost=/usr/bin/bash -c 'while [ "$(podman inspect --format "{{.State.Health.Status}}" wp-mariadb)" != "healthy" ]; do sleep 2; done'
[Install]
WantedBy=default.target
Hacemos lo mismo para Redis utilizando el comando redis-cli.
[Unit]
Description=Caché Redis para WordPress
[Container]
Image=docker.io/library/redis:7-alpine
ContainerName=wp-redis
Network=wp-network.network
AutoUpdate=registry
# Healthcheck: verifica que Redis responda PONG
HealthCmd=redis-cli ping | grep PONG || exit 1
HealthInterval=5s
[Service]
Restart=always
ExecStartPost=/usr/bin/bash -c 'while [ "$(podman inspect --format "{{.State.Health.Status}}" wp-redis)" != "healthy" ]; do sleep 2; done'
[Install]
WantedBy=default.target
Gracias a las modificaciones anteriores, las directivas After y Requires ahora serán efectivas de verdad: WordPress esperará a que el script de los otros servicios termine con éxito.
[Unit]
Description=Aplicación WordPress
After=wp-network.network wp-mariadb.service wp-redis.service
Requires=wp-mariadb.service wp-redis.service
[Container]
Image=docker.io/library/wordpress:latest
ContainerName=wp-app
Environment=WORDPRESS_DB_HOST=wp-mariadb
Environment=WORDPRESS_DB_USER=lorenzo
Environment=WORDPRESS_DB_PASSWORD=atareao_pass
Environment=WORDPRESS_DB_NAME=wordpress
Environment="WORDPRESS_CONFIG_EXTRA=define('WP_REDIS_HOST', 'wp-redis');"
Network=wp-network.network
PublishPort=8080:80
Volume=wp-app-data.volume:/var/www/html:Z
# Definimos el Healthcheck interno
HealthCmd=curl -f http://localhost/wp-login.php || exit 1
HealthInterval=10s
HealthRetries=3
HealthStartPeriod=30s
[Service]
Restart=always
# La pieza que faltaba: esperar a que WordPress esté sano antes de dar el OK
ExecStartPost=/usr/bin/bash -c 'while [ "$(podman inspect --format "{{.State.Health.Status}}" wp-app)" != "healthy" ]; do sleep 2; done'
[Install]
WantedBy=default.target
¿Cómo funciona el arranque ahora?
- Systemd lanza MariaDB. El estado del servicio se queda en
activating. - Podman ejecuta el contenedor; el proceso interno de MariaDB inicia la base de datos.
- El
ExecStartPostde MariaDB se ejecuta en bucle, preguntando a Podman si el contenedor estáhealthy. - Una vez que el healthcheck de MariaDB es positivo, el
ExecStartPosttermina y el servicio pasa aactive. - Systemd, al ver que MariaDB está
active, procede a lanzar Redis (o WordPress, según el orden de dependencias).
Notificaciones cuando WordPress está listo
Crea este script en una ruta accesible, por ejemplo ~/.local/bin/notify-wp.sh:
#!/bin/bash
# Script para notificar el estado del stack WordPress
# Puedes usar curl para enviar un mensaje a Telegram o ntfy.sh
MESSAGE="🚀 ¡Hola Lorenzo! El stack de WordPress ya está 'healthy' y listo para producir en el puerto 8080."
# Ejemplo con ntfy.sh (muy sencillo para tus tutoriales)
curl -d "$MESSAGE" ntfy.sh/atareao_wp_deploy
# O si prefieres un log local o notificación de escritorio:
# notify-send "Podman Quadlets" "$MESSAGE"
No necesitamos un contenedor para el servicio de notificaciones, basta con una unidad de systemd estándar que dependa de tu Quadlet de WordPress. Crea el archivo ~/.config/systemd/user/wp-notifier.service:
[Unit]
Description=Notificador de despliegue WordPress
# Solo se ejecuta DESPUÉS de que WordPress pase su healthcheck (gracias al ExecStartPost anterior)
BindsTo=wordpress.service
After=wordpress.service
[Service]
Type=simple
# Ejecuta el script una sola vez cuando el servicio dependiente esté listo
ExecStartPre=/usr/bin/sleep 2
ExecStart=%h/.local/bin/notify-wp.sh
RemainAfterExit=no
[Install]
WantedBy=wordpress.service
Ahora nos queda, habilitarle e iniciarlo, de la siguiente manera,
systemctl --user daemon-reload
systemctl --user enable wp-notifier.service
systemctl --user start wp-notifier.service
¿Que pasa cuando no arranca bien WordPress?
Si, está claro, que ese menaje no se enviará. Pero si quieres ser más exhaustivo, podemos utilizar un servicio que se encarga de los fallos. Para esto hay que modificar wordpress.container y añadir en [Unit] lo siguiente,
OnFailure=wp-failure-handler@%n.service
Y por otro lado si queremos un política de reinicio automática en caso de fallo, añadimos en [Service],
Restart=on-failure
RestartSec=10s
StartLimitIntervalSec=300
StartLimitBurst=3
# Tiempo máximo para que el healthcheck pase a healthy
TimeoutStartSec=60
Por otro lado creamos el servicio wp-failure-handler@.service en ~/.config/systemd/user/wp-failure-handler@.service:
[Unit]
Description=Manejador de Errores para %i
[Service]
Type=oneshot
ExecStart=%h/.local/bin/notify-failure-wp.sh %i
Y nuestro script de notificación de fallos notify-failure-wp.sh en ~/.local/bin/notify-failure-wp.sh:
#!/bin/bash
# Script para notificar el estado del stack WordPress
# Puedes usar curl para enviar un mensaje a Telegram o ntfy.sh
SERVICE_NAME=$1
MESSAGE="⚠️ ¡Atención Lorenzo! El servicio $SERVICE_NAME ha fallado al arrancar. El healthcheck no se completó a tiempo. Revisa 'journalctl -u $SERVICE_NAME' para más detalles."
notify-send "Podman Quadlets" "$MESSAGE"
Resumen del flujo de salud
- Capa de Datos:
wp-mariadblevanta y verifica su socket. - Capa de Caché:
wp-redislevanta y responde alping. - Capa de Aplicación:
wordpressespera a los anteriores y verifica quewp-login.phpdevuelva un200 OK. - Capa de Usuario: Recibes el aviso de que todo está en orden.