10

Healthchecks y ciclo de vida de contenedores

Vistas: 1
Tutorial: Tutorial de Podman
Healthchecks y ciclo de vida de contenedores

Has visto como crear imágenes, ejecutar contenedores, gestionar redes y volúmenes, orquestar con Quadlets y hasta desplegar aplicaciones completas. Tus contenedores funcionan. Pero… ¿cómo saber si realmente están funcionando bien? Porque una cosa es que un contenedor esté corriendo, podman ps te lo muestra, y otra muy distinta es que la aplicación que lleva dentro esté viva, respondiendo peticiones y comportándose correctamente. Un contenedor puede estar «Up» con su proceso dentro, pero si tu base de datos se ha quedado sin conexiones, si tu servidor web está en un bucle infinito o si tu worker de colas lleva horas sin procesar mensajes… para el mundo exterior ese contenedor está muerto en vida. Es un contenedor zombie.

Los healthchecks (comprobaciones de salud) son el mecanismo que tiene Podman para diferenciar entre un contenedor que simplemente existe y uno que realmente funciona. En este capítulo veremos cómo definirlos, cómo configurarlos, cómo usarlos para auto-recuperación, cómo integrarlos con systemd y Quadlets, y cómo construir sistemas autocurables que no necesiten supervisión humana constante.

¿Qué es un healthcheck y por qué lo necesitas?

Un healthcheck es un comando que Podman ejecuta periódicamente dentro de tu contenedor para verificar que la aplicación se comporta correctamente. Piensa en ello como un reconocimiento médico: cada cierto tiempo, el médico (Podman) entra a tu casa (el contenedor) y le pide al paciente (tu aplicación) que haga una prueba sencilla. Si la supera, está sano. Si falla, está enfermo. Si falla varias veces seguidas, lo declaramos grave.

El resultado del healthcheck es un estado que Podman asigna al contenedor y que puedes consultar en cualquier momento:

  • starting — El contenedor acaba de arrancar y está en el periodo de gracia inicial.
  • healthy — El último healthcheck se completó con éxito.
  • unhealthy — El healthcheck ha fallado más veces de las permitidas.

Sin healthchecks, estás operando a ciegas. Tu monitorización solo ve el estado del proceso, no el de la aplicación. Y en producción, la diferencia entre un proceso vivo y una aplicación funcionando es la diferencia entre un incidente silencioso y una caída detectada a tiempo.

Contenedores zombie: el enemigo silencioso

Un contenedor zombie es aquel que sigue corriendo (su proceso principal no ha muerto) pero la aplicación que alberga no responde, está degradada o ha entrado en un estado inconsistente. Son particularmente peligrosos porque:

  • No los detectas con podman ps — aparecen como «Up».
  • Consumen recursos — CPU, memoria, conexiones de red.
  • Degradan la experiencia — tus usuarios reciben errores o timeouts.
  • Enmascaran problemas — cuando finalmente miras, parece que todo está bien porque el contenedor «sigue corriendo».

Los healthchecks son el antídoto contra los contenedores zombie. Y combinados con reinicios automáticos, convierten tus despliegues en sistemas resilientes que se curan solos.

HEALTHCHECK en Containerfile / Dockerfile

La forma más fundamental de definir un healthcheck es directamente en la imagen, mediante la instrucción HEALTHCHECK del Containerfile (o Dockerfile, que Podman entiende igual). De esta manera, cada vez que alguien ejecute un contenedor desde tu imagen, el healthcheck estará activo por defecto. No hay que acordarse de pasarlo como argumento en podman run.

La sintaxis básica es:

HEALTHCHECK [OPCIONES] CMD comando

O, para deshabilitar explícitamente cualquier healthcheck que viniera de una imagen base:

HEALTHCHECK NONE

Opciones de HEALTHCHECK

Las opciones controlan el cuándo, el cómo y el cuánto del chequeo:

OpciónPor defectoDescripción
--interval30sTiempo entre ejecuciones del healthcheck
--timeout30sTiempo máximo para que el comando complete
--start-period0sPeriodo de gracia inicial (los fallos no cuentan)
--start-interval5sIntervalo durante el start-period (más frecuente)
--retries3Fallos consecutivos antes de marcar como unhealthy

Veamos un ejemplo real para un servidor web Nginx:

FROM docker.io/library/nginx:alpine

HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1

COPY index.html /usr/share/nginx/html/

Fíjate en cada parámetro:

  • --interval=30s — Cada 30 segundos se ejecuta el healthcheck.
  • --timeout=10s — Si el comando tarda más de 10 segundos, se considera fallido.
  • --start-period=15s — Durante los primeros 15 segundos tras arrancar, los fallos no cuentan. Esto da tiempo a Nginx a cargar configuración, certificados, etc.
  • --retries=3 — Hacen falta 3 fallos consecutivos para marcar el contenedor como unhealthy.

El comando wget --no-verbose --tries=1 --spider http://localhost:80/ lo que hace es descargar la página principal sin guardarla (--spider), con un solo intento (--tries=1) y sin ruido (--no-verbose). Si la petición HTTP falla (código distinto de 200), wget devuelve un código de salida distinto de cero y Podman registra el fallo.

Reglas del comando HEALTHCHECK

El comando que pongas tras CMD debe seguir estas reglas:

  • Salida 0 — El contenedor está sano (healthy).
  • Salida distinta de 0 — El contenedor está enfermo en esa comprobación.
  • Salida 1 — Se usa tradicionalmente para «enfermo pero no crítico».
  • Salida 137 o superior — Señal externa (normalmente SIGKILL = 137 = 128 + 9).

No hay límite en lo complejo que pueda ser el comando. Puedes ejecutar curl, wget, pg_isready, mysqladmin ping, un script personalizado, o incluso múltiples comandos encadenados. La única regla: el código de salida decide el resultado.

HEALTHCHECK NONE

A veces una imagen base ya trae un healthcheck que no te interesa. Por ejemplo, algunas imágenes oficiales incluyen healthchecks que pueden no ser apropiados para tu uso. Para desactivarlo:

FROM docker.io/library/nginx:alpine

HEALTHCHECK NONE

# Ahora tú configuras tu propio healthcheck...

También puedes sobrescribir un healthcheck existente simplemente definiendo uno nuevo. El último HEALTHCHECK en el Containerfile es el que cuenta.

Ejemplos de healthchecks para diferentes aplicaciones

Servidor web Nginx / Apache:

HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=2 \
  CMD curl --fail http://localhost:8080/health || exit 1

Base de datos PostgreSQL:

HEALTHCHECK --interval=10s --timeout=5s --start-period=20s --retries=5 \
  CMD pg_isready -U postgres -d mi_base || exit 1

Base de datos MariaDB / MySQL:

HEALTHCHECK --interval=15s --timeout=5s --start-period=20s --retries=3 \
  CMD mysqladmin ping -u root -p"$MYSQL_ROOT_PASSWORD" || exit 1

Cola de mensajes Redis:

HEALTHCHECK --interval=10s --timeout=3s --start-period=5s --retries=3 \
  CMD redis-cli ping | grep -q PONG || exit 1

Aplicación Node.js / Python con endpoint de salud:

HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
  CMD curl --fail http://localhost:3000/api/health || exit 1

Worker de cola (sin puerto HTTP):

# Para workers que no exponen puertos, usamos un script que verifica el PID
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
  CMD ps aux | grep -q "[w]orker" || exit 1

Consejo: Para aplicaciones que no exponen HTTP (workers, processadores batch), el healthcheck puede verificar la existencia del proceso, la presencia de un archivo de bloqueo, o contactar con un socket Unix. Sé creativo, pero mantenlo rápido: un healthcheck lento es peor que ningún healthcheck.

Tipos de HEALTHCHECK: CMD vs CMD-SHELL

Existen dos formas de ejecutar el comando de healthcheck:

HEALTHCHECK CMD (forma exec, recomendada)

HEALTHCHECK CMD ["curl", "--fail", "http://localhost:80/"]

En esta forma, Podman ejecuta el comando directamente, sin pasar por un shell. Esto significa que no hay expansión de variables, no hay pipes, no hay globbing. Es más seguro y predecible, pero también más limitado.

HEALTHCHECK CMD-SHELL (forma shell)

HEALTHCHECK CMD curl --fail http://localhost:80/ || exit 1

Sin los corchetes, Podman ejecuta el comando a través de /bin/sh -c. Esto te permite usar tuberías (|), redirecciones (>), variables de entorno ($VAR) y toda la potencia del shell.

La regla general: usa la forma CMD con corchetes para comandos simples y directos. Usa CMD-SHELL (que puedes escribir explícitamente) o la forma sin corchetes cuando necesites la flexibilidad del shell.

Healthchecks desde la línea de comandos

No hace falta que el healthcheck esté definido en la imagen. Puedes configurarlo (o sobrescribirlo) al arrancar el contenedor con podman run usando las opciones --health-*:

podman run -d --name mi-web \
  --health-cmd="curl --fail http://localhost:80/ || exit 1" \
  --health-interval=15s \
  --health-timeout=5s \
  --health-start-period=10s \
  --health-retries=3 \
  -p 8080:80 \
  docker.io/library/nginx:alpine

Y también puedes actualizar el healthcheck de un contenedor ya en ejecución con podman update:

podman update --health-cmd="curl --fail http://localhost:8080/api/health" mi-web
podman update --health-interval=10s mi-web
podman update --health-retries=5 mi-web

Esto es increíblemente útil en entornos de desarrollo, cuando estás ajustando los parámetros del healthcheck sin tener que reconstruir la imagen ni reiniciar el contenedor.

Consultar el estado de salud

Una vez que el contenedor tiene un healthcheck configurado, puedes consultar su estado de varias formas:

Con podman ps

podman ps

La salida incluye una columna STATUS que muestra algo como:

CONTAINER ID  IMAGE                           COMMAND               CREATED        STATUS                    PORTS                 NAMES
a1b2c3d4e5f6  docker.io/library/nginx:alpine  nginx -g daemon o...  2 minutes ago  Up 2 minutes (healthy)    0.0.0.0:8080->80/tcp  mi-web

O si está en el periodo de gracia:

Up 10 seconds (starting)

O si está enfermo:

Up 5 minutes (unhealthy)

Con podman inspect

Para ver los detalles completos del healthcheck, incluyendo el historial:

podman inspect mi-web --format '{{json .State.Health}}' | jq

La salida es algo como:

{
  "Status": "healthy",
  "FailingStreak": 0,
  "Log": [
    {
      "Start": "2025-06-11T10:00:00Z",
      "End": "2025-06-11T10:00:01Z",
      "ExitCode": 0,
      "Output": ""
    }
  ]
}

El campo Log contiene el historial de las últimas comprobaciones. Puedes ver cuándo se ejecutaron, cuánto tardaron, su código de salida y su salida estándar. Esto es oro puro para depurar healthchecks que fallan sin motivo aparente.

Con podman healthcheck run

Si sospechas que el healthcheck está fallando pero no sabes por qué, puedes ejecutarlo manualmente con:

podman healthcheck run mi-web

Esto fuerza una ejecución inmediata del healthcheck y muestra el resultado en la terminal:

Healthy

O si falla:

Unhealthy: exit code 1

Puedes combinarlo con un ligero aumento de verbosidad para ver exactamente qué está pasando:

podman healthcheck run mi-web && echo "OK" || echo "FALLÓ"

Esta es tu mejor herramienta de diagnóstico. Si el healthcheck falla cuando lo ejecutas manualmente, el problema está en el comando, no en Podman. Ejecuta el mismo comando dentro del contenedor con podman exec para depurar:

# Probar el comando directamente dentro del contenedor
podman exec mi-web curl --fail http://localhost:80/

Ver logs del healthcheck

Los resultados del healthcheck también se registran como eventos. Puedes verlos con:

podman events --filter container=mi-web --filter event=health_status

O en tiempo real:

podman events --filter container=mi-web --filter event=health_status --stream

Auto-reinicio con --restart

Los healthchecks por sí solos no reinician el contenedor. Simplemente informan de su estado. Para que un contenedor se reinicie automáticamente cuando algo va mal, necesitas combinarlo con políticas de reinicio.

Política --restart

La opción --restart de podman run define cuándo Podman debe reiniciar el contenedor:

  • no — No reiniciar nunca (por defecto).
  • on-failure — Reiniciar si el contenedor termina con código de error distinto de cero.
  • always — Reiniciar siempre, independientemente del código de salida.
  • unless-stopped — Reiniciar siempre, excepto si el contenedor se detuvo explícitamente con podman stop.

Ejemplo:

podman run -d --name mi-web \
  --restart=always \
  --health-cmd="curl --fail http://localhost:80/ || exit 1" \
  -p 8080:80 \
  docker.io/library/nginx:alpine

Si el proceso principal muere, Podman lo reinicia automáticamente. Pero esto solo cubre el caso de que el proceso termine. No cubre los contenedores zombie, esos cuyo proceso principal sigue vivo pero la aplicación está degradada.

--health-on-failure: la pieza que faltaba

A partir de Podman 5.0, existe la opción --health-on-failure que conecta el healthcheck con la acción a tomar. Es el eslabón perdido entre detectar un problema y solucionarlo.

¿Qué versión de Podman tienes? Puedes comprobarlo con podman version. Si estás en Podman 4.x y no aparece --health-on-failure, no te preocupes: puedes conseguir el mismo resultado combinando --restart=always con HealthOnFailure=kill en Quadlets (lo veremos en la sección de Quadlets más adelante).

Las opciones disponibles son:

  • none — No hacer nada (por defecto).
  • restart — Reiniciar el contenedor cuando se vuelva unhealthy.
  • stop — Detener el contenedor (para que systemd lo reinicie si está gestionado por él).
  • kill — Matar el contenedor inmediatamente (mejor integración con systemd).

Ejemplo de uso con restart:

podman run -d --name mi-web \
  --restart=always \
  --health-cmd="curl --fail http://localhost:80/ || exit 1" \
  --health-on-failure=restart \
  -p 8080:80 \
  docker.io/library/nginx:alpine

Ahora, cuando el healthcheck falle las retries configuradas, Podman:

  1. Marca el contenedor como unhealthy.
  2. Ejecuta la acción configurada (restart).
  3. El contenedor se reinicia con --restart=always, volviendo a empezar.

Conviene entender la diferencia entre --restart y --health-on-failure:

Situación--restart--health-on-failure
El proceso crashea (exit code != 0)✅ Reinicia❌ No actúa
El proceso está vivo pero la app no responde❌ No actúa✅ Reinicia
El healthcheck detecta degradación❌ No actúa✅ Reinicia
El usuario detiene el contenedorDepende de la política❌ No actúa

Para una protección completa, necesitas ambos. El --restart cubre los crash súbitos del proceso; el --health-on-failure cubre los estados zombie donde el proceso sigue vivo pero la aplicación no funciona.

Healthchecks con Quadlets

En el capítulo anterior viste el poder de los Quadlets para gestionar contenedores como servicios systemd. Pues bien, los Quadlets también soportan healthchecks de forma nativa.

En un archivo .container, las claves relacionadas con healthcheck son:

[Container]
Image=docker.io/library/nginx:alpine
ContainerName=mi-web
PublishPort=8080:80
HealthCmd=curl --fail http://localhost:80/ || exit 1
HealthInterval=15s
HealthTimeout=5s
HealthStartPeriod=10s
HealthRetries=3
HealthOnFailure=kill

[Service]
Restart=always

[Install]
WantedBy=default.target

Fíjate en HealthOnFailure=kill. Esta es la combinación ganadora con Quadlets. Cuando el contenedor se vuelve unhealthy:

  1. Podman lo mata (kill).
  2. El proceso termina con código de salida 137 (SIGKILL).
  3. systemd detecta que el servicio ha terminado.
  4. Como tiene Restart=always, systemd lo reinicia inmediatamente.

Esto es mucho más robusto que --health-on-failure=restart porque systemd gestiona el ciclo de vida completo: reintentos, tiempos de espera entre reinicios, límite de reinicios, etc.

Quadlet completo para PostgreSQL con healthcheck

[Unit]
Description=Base de datos PostgreSQL con healthcheck
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=mi-postgres
PublishPort=5432:5432
Volume=postgres-data:/var/lib/postgresql/data
Environment=POSTGRES_USER=admin
Environment=POSTGRES_PASSWORD=secreto123
Environment=POSTGRES_DB=miapp
HealthCmd=pg_isready -U admin -d miapp || exit 1
HealthInterval=10s
HealthTimeout=5s
HealthStartPeriod=30s
HealthRetries=5
HealthOnFailure=kill

[Service]
Restart=always
RestartSec=5s

[Install]
WantedBy=default.target

Quadlet para Redis con healthcheck

[Unit]
Description=Cola de mensajes Redis
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target

[Container]
Image=docker.io/library/redis:7-alpine
ContainerName=mi-redis
PublishPort=6379:6379
Volume=redis-data:/data
HealthCmd=redis-cli ping | grep -q PONG || exit 1
HealthInterval=10s
HealthTimeout=3s
HealthStartPeriod=5s
HealthRetries=3
HealthOnFailure=kill

[Service]
Restart=always
RestartSec=3s

[Install]
WantedBy=default.target

sd_notify: watchdog inteligente con systemd

Los healthchecks de Podman son potentes, pero tienen una limitación: los define Podman, no la aplicación. La aplicación no sabe que la están vigilando. No puede decir «estoy viva» ni «estoy ocupada, dame un momento».

Aquí entra en juego sd_notify, un protocolo de systemd que permite a los procesos notificar su estado directamente al sistema de init. Cuando un proceso sabe que está listo, envía un mensaje READY=1 a systemd. Cuando quiere indicar que sigue vivo, envía WATCHDOG=1. Y systemd, que es quien realmente gestiona el servicio, sabe en todo momento si la aplicación está respondiendo.

¿Cómo funciona sd_notify en Podman?

Podman expone el socket de sd_notify dentro del contenedor de dos formas, controladas por la opción --sdnotify:

  • conmon (por defecto) — El gestor de contenedores conmon envía la notificación READY a systemd. Con healthchecks, espera al primer healthcheck exitoso antes de notificar.
  • container — La aplicación dentro del contenedor envía directamente READY a systemd. La aplicación debe ser consciente de sd_notify.
  • healthy — Similar a conmon, pero espera explícitamente a que el contenedor esté healthy.

Modo conmon (por defecto)

En este modo, conmon se encarga de todo. No necesitas modificar tu aplicación. El flujo es:

  1. El contenedor arranca.
  2. Conmon espera el primer healthcheck exitoso (respetando el start-period).
  3. Conmon envía READY=1 a systemd.
  4. systemd considera el servicio como «activo».

Esto es lo que ocurre cuando ejecutas un Quadlet con healthcheck: systemd espera a que el contenedor esté realmente sano antes de marcar el servicio como active. Si usas systemctl status mi-servicio, verás el estado activating hasta que el healthcheck inicial se complete.

Modo container

Para aplicaciones que soportan sd_notify de forma nativa, el modo container permite que la aplicación misma decida cuándo está lista:

podman run -d --name mi-app \
  --sdnotify=container \
  mi-app:latest

La aplicación debe enviar READY=1 al socket $NOTIFY_SOCKET. En lenguajes como Go, Python o Node.js hay librerías específicas para esto.

Modo healthy

Este modo combina lo mejor de ambos mundos: el healthcheck de Podman determina la salud, pero espera explícitamente a que el contenedor esté healthy antes de notificar a systemd:

podman run -d --name mi-web \
  --sdnotify=healthy \
  --health-cmd="curl --fail http://localhost:80/ || exit 1" \
  nginx:alpine

Puedes verificar el estado del servicio con:

systemctl --user status mi-web

Durante el arranque, el estado será activating. Cuando el primer healthcheck sea exitoso, pasará a active (running).

En Quadlets, el modo sd_notify se configura con:

[Container]
Image=docker.io/library/nginx:alpine
ContainerName=mi-web
PublishPort=8080:80
HealthCmd=curl --fail http://localhost:80/ || exit 1
HealthInterval=15s
HealthTimeout=5s
HealthStartPeriod=10s
HealthRetries=3
HealthOnFailure=kill
SdNotifyMode=healthy

[Service]
Restart=always
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Importante: Type=notify y NotifyAccess=all se configuran automáticamente en los Quadlets .container, pero puedes sobrescribirlos si necesitas comportamiento específico.

Watchdog de systemd

El watchdog de systemd va un paso más allá. No solo comprueba la salud periódicamente, sino que establece un contrato temporal: la aplicación debe enviar una señal de «sigo viva» antes de que expire un temporizador. Si no lo hace, systemd asume que la aplicación se ha colgado y la reinicia.

Para activarlo en un Quadlet:

[Service]
Restart=always
WatchdogSec=20s

Con WatchdogSec=20s, la aplicación debe notificar «sigo viva» cada 20 segundos. Si no lo hace, systemd mata el proceso y lo reinicia.

En el modo container, la aplicación envía WATCHDOG=1 periódicamente. En el modo conmon, Podman gestiona el watchdog usando los healthchecks.

La combinación de healthcheck + watchdog + sd_notify es probablemente el sistema de monitoreo más robusto que puedes tener sin salir del ecosistema Podman + systemd. Cada capel cubre un aspecto diferente:

  1. Healthcheck — Verifica que la aplicación responde correctamente.
  2. Watchdog — Verifica que la aplicación no está congelada.
  3. sd_notify — Coordina el arranque y la notificación de estado con systemd.

Eventos de healthcheck

Podman emite eventos cada vez que se ejecuta un healthcheck. Puedes usarlos para monitorización en tiempo real, integración con sistemas externos o alertas.

Ver eventos en tiempo real

podman events --filter event=health_status --stream

Esto muestra cada cambio de estado de salud:

2025-06-11 10:00:00.123456789 +0000 UTC health_status (container=a1b2c3d4e5f6, name=mi-web, status=healthy)
2025-06-11 10:00:15.456789012 +0000 UTC health_status (container=a1b2c3d4e5f6, name=mi-web, status=healthy)
2025-06-11 10:00:45.789012345 +0000 UTC health_status (container=a1b2c3d4e5f6, name=mi-web, status=unhealthy)

Filtrar por contenedor específico

podman events --filter container=mi-web --filter event=health_status

Integrar con scripts de alerta

Puedes combinar los eventos con un script que reaccione automáticamente:

podman events --filter event=health_status --stream | while read -r line; do
  if echo "$line" | grep -q "status=unhealthy"; then
    echo "ALERTA: Contenedor no saludable: $(echo $line | grep -oP 'name=\K[^,]+')"
    # Aquí podrías enviar un email, una notificación a Slack, etc.
  fi
done

O usar jq si prefieres JSON:

podman events --filter event=health_status --format json --stream | while read -r event; do
  status=$(echo "$event" | jq -r '.Status')
  name=$(echo "$event" | jq -r '.Actor.Attributes.name // "desconocido"')
  if [ "$status" = "unhealthy" ]; then
    echo "⚠️ Contenedor $name en estado crítico"
  fi
done

Necesitas jq: Si optas por la versión con JSON, asegúrate de tener jq instalado. En Debian/Ubuntu: sudo apt install jq. En Fedora: sudo dnf install jq. En Arch: sudo pacman -S jq. Sin jq, puedes usar la primera versión con grep, que no necesita dependencias adicionales.

Una forma elegante de recibir notificaciones es usar los eventos de Podman desde un servicio systemd. Puedes crear un servicio que escuche eventos y ejecute acciones cuando un contenedor se vuelva unhealthy:

Puedes usar el script que encontrarás en scripts/podman-healthwatch.sh dentro del repositorio del tutorial. Guárdalo en una ubicación accesible y luego crea el servicio systemd:

~/.config/systemd/user/podman-healthwatch.service

[Unit]
Description=Monitor de healthchecks de Podman
Documentation=https://atareao.es/tutorial/podman/

[Service]
ExecStart=/home/tu-usuario/scripts/podman-healthwatch.sh
Restart=always
RestartSec=5s

[Install]
WantedBy=default.target

Actívalo:

systemctl --user daemon-reload
systemctl --user enable --now podman-healthwatch.service

A partir de ahí, cada vez que un contenedor se vuelva unhealthy, recibirás un mensaje en el log del sistema que podrás consultar con:

journalctl --user -t podman-healthwatch -f

Ejemplos prácticos

Vamos a construir varios escenarios completos que integran todo lo que has aprendido.

Ejemplo 1: Servicio web con auto-recuperación

Crea el archivo web-saludable.container:

[Unit]
Description=Servicio web Nginx con healthcheck y auto-recuperación
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target
Wants=network-online.target

[Container]
Image=docker.io/library/nginx:alpine
ContainerName=web-saludable
PublishPort=8080:80
Volume=./html:/usr/share/nginx/html:Z
HealthCmd=curl --fail http://localhost:80/ || exit 1
HealthInterval=15s
HealthTimeout=5s
HealthStartPeriod=15s
HealthRetries=3
HealthOnFailure=kill
SdNotifyMode=healthy

[Service]
Restart=always
RestartSec=5s

[Install]
WantedBy=default.target

Guárdalo en ~/.config/containers/systemd/web-saludable.container, recarga systemd y arráncalo:

mkdir -p ~/.config/containers/systemd
cp web-saludable.container ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user enable --now web-saludable

Ahora puedes ver cómo systemd espera a que el healthcheck se complete:

systemctl --user status web-saludable

Durante los primeros segundos (el start-period de 15s más el primer healthcheck exitoso), verás:

● web-saludable.service - Servicio web Nginx con healthcheck y auto-recuperación
     Loaded: loaded (/home/usuario/.config/containers/systemd/web-saludable.container; generated)
     Active: activating (auto-restart) (running)

Cuando el healthcheck se complete con éxito:

     Active: active (running)

Ejemplo 2: Base de datos PostgreSQL con watchdog

Crea postgres-watchdog.container:

[Unit]
Description=PostgreSQL con watchdog de systemd
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target
Wants=network-online.target
Requires=postgres-data.volume

[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=postgres-watchdog
PublishPort=5432:5432
Volume=postgres-data:/var/lib/postgresql/data
Environment=POSTGRES_USER=admin
Environment=POSTGRES_PASSWORD=secreto
Environment=POSTGRES_DB=miapp
HealthCmd=pg_isready -U admin -d miapp || exit 1
HealthInterval=10s
HealthTimeout=5s
HealthStartPeriod=30s
HealthRetries=5
HealthOnFailure=kill
SdNotifyMode=healthy

[Service]
Restart=always
RestartSec=10s
WatchdogSec=30s

[Install]
WantedBy=default.target

Y su volumen asociado postgres-data.volume:

[Volume]
Label=app=postgres

Con WatchdogSec=30s, si el healthcheck deja de responder durante más de 30 segundos, systemd mata el contenedor y lo reinicia. Es una capa adicional de seguridad: no solo el healthcheck verifica la salud, sino que si el propio Podman o conmon se cuelgan, systemd actúa como último recurso.

Ejemplo 3: Cola de mensajes Redis con eventos

Crea redis-colista.container:

[Unit]
Description=Redis con healthcheck y notificaciones
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target

[Container]
Image=docker.io/library/redis:7-alpine
ContainerName=redis-colista
PublishPort=6379:6379
Volume=redis-data:/data
HealthCmd=redis-cli ping | grep -q PONG || exit 1
HealthInterval=10s
HealthTimeout=3s
HealthStartPeriod=5s
HealthRetries=3
HealthOnFailure=kill
SdNotifyMode=healthy

[Service]
Restart=always
RestartSec=3s

[Install]
WantedBy=default.target

Y un script de notificación que se ejecuta como servicio aparte. El script lo tienes en scripts/healthcheck-notifier.sh del repositorio del tutorial. Crea el servicio systemd que lo ejecute:

~/.config/systemd/user/healthcheck-notifier.service

[Unit]
Description=Notificador de cambios de salud en contenedores
Documentation=https://atareao.es/tutorial/podman/

[Service]
ExecStart=/home/tu-usuario/scripts/healthcheck-notifier.sh
Restart=always
RestartSec=5s

[Install]
WantedBy=default.target

Actívalo:

systemctl --user daemon-reload
systemctl --user enable --now healthcheck-notifier

Ahora, cada vez que cualquier contenedor se vuelva unhealthy, recibirás una notificación en el escritorio.

Nota sobre jq: El script healthcheck-notifier.sh usa jq para procesar los eventos en JSON. Si no lo tienes instalado, puedes hacerlo con sudo apt install jq (Debian/Ubuntu) o sudo dnf install jq (Fedora). Sin jq, el script no podrá extraer el estado y el nombre del contenedor de los eventos.

Ejemplo 4: Aplicación Python con endpoint de salud

Containerfile para una aplicación Flask con healthcheck:

FROM docker.io/library/python:3.12-alpine

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .

# El healthcheck verifica el endpoint /health
HEALTHCHECK --interval=15s --timeout=5s --start-period=10s --retries=3 \
  CMD curl --fail http://localhost:5000/health || exit 1

EXPOSE 5000
CMD ["python", "app.py"]

Y la aplicación app.py:

from flask import Flask, jsonify
import os
import time

app = Flask(__name__)

# Simulamos un estado interno
health = {"status": "ok", "uptime": 0}
start_time = time.time()

@app.route('/health')
def health_check():
    health["uptime"] = int(time.time() - start_time)
    return jsonify(health), 200

@app.route('/')
def index():
    return "¡Hola desde Podman con healthcheck!"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Construye y ejecuta:

podman build -t flask-saludable .
podman run -d --name flask-app --restart=always -p 5000:5000 flask-saludable

Buenas prácticas

Después de todo lo visto, aquí tienes un decálogo de buenas prácticas para tus healthchecks:

  1. Sé rápido, no exhaustivo — Un healthcheck debe durar milisegundos, no segundos. No verifiques toda la base de datos en cada chequeo; con un ping basta. Si necesitas comprobaciones profundas, hazlas en un proceso separado.
  2. Usa start-period generoso — Dale tiempo a tu aplicación para inicializarse. Una base de datos puede tardar 30 segundos o más en estar lista. Un start-period de 0 hará que falle el healthcheck y se reinicie en bucle.
  3. Ajusta retries según la criticidad — Para servicios críticos, 3 reintentos está bien. Para servicios menos importantes, puedes subir a 5 o más para evitar falsos positivos.
  4. Combina --restart con --health-on-failure — El primero cubre crash súbitos; el segundo cubre contenedores zombie. Los dos juntos son imbatibles.
  5. En Quadlets, usa HealthOnFailure=kill — Es la opción que mejor se integra con systemd. El contenedor se mata y systemd lo reinicia limpiamente.
  6. Usa SdNotifyMode=healthy en Quadlets — Así systemd espera a que el healthcheck inicial sea exitoso antes de marcar el servicio como activo.
  7. Documenta tus healthchecks — Pon un comentario en el Containerfile explicando qué verifica y por qué. Ayudará a quien herede tu código.
  8. Prueba el healthcheck manualmente primero — Usa podman healthcheck run y podman exec para verificar que el comando funciona dentro del contenedor antes de confiar en él en producción.
  9. Monitoriza los eventos — Usa podman events para llevar un registro de los cambios de salud. Puedes integrarlo con sistemas de alerta.
  10. No abuses de los healthchecks — Un healthcheck por contenedor es suficiente. No anides healthchecks dentro de healthchecks. Mantenlo simple.

Solución de problemas comunes

El contenedor entra en bucle de reinicios

Síntoma: El contenedor se reinicia constantemente, nunca llega a healthy.

Causa: El healthcheck falla siempre durante el start-period o el comando es incorrecto.

Solución: Aumenta start-period para dar más tiempo a la inicialización. Verifica el comando con podman exec:

podman exec mi-contenedor curl --fail http://localhost:80/

El contenedor nunca sale de «starting»

Síntoma: El contenedor lleva minutos o horas en estado starting.

Causa: Este estado debería ser temporal. Si persiste, el healthcheck no se está ejecutando o el start-period es excesivo.

Solución: Comprueba que el healthcheck está configurado correctamente:

podman inspect mi-contenedor --format '{{json .Config.Healthcheck}}' | jq

El healthcheck funciona manualmente pero falla automáticamente

Síntoma: podman healthcheck run mi-contenedor devuelve Healthy, pero el contenedor aparece como unhealthy en podman ps.

Causa: Normalmente es un problema de temporización o de recursos. El healthcheck manual se ejecuta inmediatamente, pero el automático podría ejecutarse cuando el contenedor está bajo carga.

Solución: Aumenta el timeout o reduce el intervalo. Revisa los logs del healthcheck:

podman inspect mi-contenedor --format '{{json .State.Health.Log}}' | jq

El contenedor no se reinicia al fallar el healthcheck

Síntoma: El contenedor está unhealthy pero no se reinicia.

Causa: Falta --health-on-failure o --restart en la configuración.

Solución: Verifica la configuración:

podman inspect mi-contenedor --format '{{json .HostConfig.RestartPolicy}}' | jq
podman inspect mi-contenedor --format '{{.Config.HealthcheckOnFailureAction}}'

Conclusión

Los healthchecks son una de esas características que, hasta que las necesitas, no sabes que las necesitas. Pero una vez que las usas, no concibes desplegar un contenedor sin ellas. En este capítulo has visto:

  • Qué son los healthchecks y por qué son esenciales para evitar contenedores zombie.
  • Cómo definirlos en el Containerfile con la instrucción HEALTHCHECK, incluyendo todas sus opciones (interval, timeout, start-period, retries).
  • Cómo configurarlos desde la línea de comandos con --health-cmd, --health-interval y compañía.
  • Cómo consultar el estado de salud con podman ps, podman inspect y podman healthcheck run.
  • Cómo auto-recuperar contenedores combinando --restart con --health-on-failure.
  • Cómo integrar healthchecks con Quadlets usando las claves HealthCmd, HealthOnFailure y SdNotifyMode.
  • Cómo usar sd_notify y watchdog para una integración profunda con systemd.
  • Cómo monitorizar con eventos de healthcheck para alertas y notificaciones.
  • Ejemplos prácticos completos para web, base de datos y cola de mensajes.

Con todo esto, tus contenedores ya no serán simples procesos aislados. Serán ciudadanos de primera clase en tu sistema, capaces de autodiagnosticarse, auto-recuperarse y notificar su estado al ecosistema. Adiós, contenedores zombie. Hola, despliegues resilientes.

En el próximo capítulo veremos cómo gestionar secretos y actualizaciones automáticas en Podman, dos características esenciales para mantener tus contenedores seguros y actualizados sin intervención manual. Nos vemos allí.


Más información,

Deja una respuesta