774 - Rollbacks Automáticos en Podman. Como WatchTower pero mejor.
Aprende a configurar actualizaciones automáticas y rollbacks nativos en Podman integrados con SystemD. Supera las limitaciones de Watchtower y Docker.
No es necesario que te mencione la importancia de tener todos tus contenedores a la última. En distintos episodios del podcast lo he tratado. Recientemente, la decisión de los desarrolladores de WatchTower de dejar de mantener el proyecto, sacudió los cimientos de la comunidad self-hoster. En ese momento, nos lanzamos todos a buscar una solución alternativa, para conseguir tener actualizados las imágenes de nuestros contenedores Docker.
Pero, si lo piensas, esto de las actualizaciones tiene la suficiente entidad como para que Docker lo llevara de caja, y sin embargo no es así. Algo que si que está brillantemente resuelto en Podman, y es precisamente de lo que te vengo a hablar en este episodio. De actualizaciones automáticas y rollbacks en Podman.

Rollbacks Automáticos en Podman. Como WatchTower pero mejor.
La integración con Systemd
La magia real de Podman reside en su integración con Systemd. Cuando está funcionando de esta forma, es cuando Podman brilla. El mecanismo de actualizaciones automáticas y rollbacks de Podman, no es la excepción, y depende casi por completo de esta integración. Podman, no depende de herramientas de terceros para su actualización automática, si no que lo trae de caja.
El motor
Podman no tiene un demonio en segundo plano vigilando (como Watchtower en Docker). En su lugar, utiliza el comando podman auto-update, que busca contenedores con una etiqueta específica.
Para que un contenedor pueda ser auto actualizado, tiene que tener la etiqueta io.containers.autoupdate. Esta etiqueta la podemos extraer inspeccionando el contenedor y preguntándole por los Labels. Por ejemplo,
$ podman inspect wp-redis --format {{.Config.Labels}}
map[PODMAN_SYSTEMD_UNIT:wp-redis.service io.containers.autoupdate:registry]
Esta etiqueta puede tener dos valores,
registry. Podman comprueba si hay una versión más reciente en el registro (comparando el digest de la imagen).local. Podman comprueba si la imagen local ha cambiado (ideal si construyes tus propias imágenes localmente).
¿Como etiquetamos el contenedor?
Pues de nuevo, tenemos distintas opciones de etiquetas el contenedor, dependiendo de como lo iniciemos. Por ejemplo, en el caso de iniciarlo directamente desde la línea de comandos, lo podemos hacer de la siguiente forma,
# Ejemplo al crear un contenedor
podman run -d --name mi-servicio \
--label "io.containers.autoupdate=registry" \
docker.io/library/nginx:latest
En el caso de un quadlet, simplemente tenemos que añadir dentro de la sección Container la opción AutoUpdate=registry.
Automatización con Systemd
Hasta el momento lo único que hemos hecho ha sido etiquetar nuestro contenedor. Para que esto sea realmente automático, necesitamos habilitar el timer que viene con Podman. Para esto simplemente tienes que ejecutar el siguiente comando,
systemctl --user enable --now podman-auto-update.timer
Este timer ejecuta el servicio podman-auto-update.service (por defecto cada 24h), el cual simplemente dispara el comando podman auto-update.
¿Como elegir cuando actualizamos?
Inicialmente, en Docker y con WatchTower, yo tenía configurado la actualización diaria. Esto al final se convirtió en un auténtico caos. En concreto los stacks de WordPress, me dieron muchos dolores de cabeza, y me llevó a cambiar las actualizaciones automáticas para que fueran semanales en lugar de diarias, y además a una hora concreta.
Esto me ha llevado directamente a modificar cuando quiero que se realicen esas actualizaciones automáticas. Para ello lo que tienes que hacer es ejecutar este comando,
systemctl --user edit podman-auto-update.timer
El uso de edit es fundamental porque crea un drop-in (un archivo de anulación), lo que evita que tus cambios se borren cuando se actualice el paquete de Podman. Al ejecutar este comando se abre un editor de texto, donde tienes que añadir una sección [Timer]. Por ejemplo, si quieres que se ejecute todos los lunes as las 03:30, tu timer tendrá el siguiente aspecto,
[Timer]
# Primero limpiamos la configuración anterior
OnCalendar=
# Definimos el nuevo horario
OnCalendar=Mon *-*-* 03:30:00
# Añadimos un margen aleatorio para evitar picos de red (opcional)
RandomizedDelaySec=30m
La sintaxis de OnCalendar es la siguiente,
| Campo | Formato | Ejemplos |
|---|---|---|
| Día de la semana | Nombre (Mon, Tue…) o abreviatura | Mon,Fri (Lunes y Viernes) |
| Fecha | AAAA-MM-DD | 2025-12-31, –-01 (día 1 de cada mes) |
| Hora | HH:MM:SS 03:00:00 | :30:00 (cada hora en el minuto 30) |
Algunos ejemplos prácticos,
- Cada mañana a las 4:15: OnCalendar=–-* 04:15:00
- Solo los fines de semana a medianoche: OnCalendar=Sat,Sun –-* 00:00:00
- Cada hora en punto: OnCalendar=hourly (o –-* *:00:00)
- Cada 15 minutos: OnCalendar=*:0,15,30,45
- El primer lunes de cada mes: OnCalendar=Mon –-01..07 02:00:00 (El lunes que caiga entre el día 1 y 7)
Puedes usar operadores para afinar la frecuencia sin crear múltiples líneas:
- Rangos (..): Mon..Fri (de lunes a viernes).
- Repeticiones (/): *:0/15 (cada 15 minutos empezando desde el minuto 0).
Para verificar que tu expresión es correcta, y es lo mejor, porque a mi por lo menos siempre me da mas de un disgusto, puedes utilizar systemd-analyze. Por ejemplo,
$ systemd-analyze calendar "Mon..Fri *-*-* 03:00:00"
Normalized form: Mon..Fri *-*-* 03:00:00
Next elapse: Mon 2026-02-02 03:00:00 CET
(in UTC): Mon 2026-02-02 02:00:00 UTC
From now: 1 day 19h left
Si usas servidores en diferentes zonas horarias, recuerda que systemd suele usar la hora local del sistema. Si prefieres que las actualizaciones ocurran en una hora específica de UTC independientemente de donde esté el VPS, puedes especificarlo en la cadena: OnCalendar=*-*-* 02:00:00 UTC.
Una vez concluidos los cambios, guarda y cierra el editor. Para aplicar los cambios, recarga el daemon de systemd con,
systemctl --user daemon-reload
Y lo que te queda es verificar que el timer está correctamente configurado,
systemctl --user status podman-auto-update.timer
Rollbacks automáticos
El rollback en Podman es reactivo. Si tras descargar una imagen nueva y reiniciar el servicio de systemd, el contenedor falla al arrancar, Podman intenta volver a la versión anterior.
Requisitos para un rollback efectivo,
- Gestionado por systemd. El contenedor debe estar corriendo como un servicio.
- –rollback (activado por defecto). El comando podman auto-update tiene el flag –rollback en true por defecto.
- Detección de fallos (sdnotify). Aquí es donde se pone interesante. Para que Podman sepa realmente que el servicio falló, es recomendable usar –sdnotify=container. Si el contenedor no llega al estado «READY» en el tiempo previsto, systemd marca el servicio como fallido y Podman inicia el rollback a la imagen anterior.
Tu quadlet tiene que tener un aspecto como el que te muestro a continuación,
[Container]
Image=docker.io/library/nginx:latest
AutoUpdate=registry
[Service]
# Opcional: para mejorar la detección de fallos y rollbacks
Environment=PODMAN_SYSTEMD_UNIT=%n
Esta es la forma mas básica, se trata de un fallo de arranque. Si Podman intentan arrancar el contenedor y falla inmediatamente, el proceso devuelve un código distinto de cero, y systemd lo detecta. Podman auto-update, al ver que el servicio falló, inicia el rollback a la versión anterior de la imagen.
Una mejor opción es sdnotify. En este caso tienes que añadir la opción Notify=true en tu Quadlet, de forma que el contenedor tiene que enviar una señal a systemd indicando que está listo. Si no lo hace en el tiempo previsto, systemd marca el servicio como fallido, y Podman inicia el rollback.
[Container]
Image=docker.io/library/nginx:latest
AutoUpdate=registry
# Le dice a Podman que espere a que el contenedor envíe la señal de listo
Notify=true
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
# Tiempo máximo de espera antes de considerar que ha fallado
TimeoutStartSec=60
Pero todavía tenemos una opción mejor, que es el caso de los Health Checks que vimos en el episodio 772.
[Unit]
Description=Servicio Nginx con reintentos y rollback automático
# Permite que systemd intente reiniciar el servicio varias veces si falla
StartLimitIntervalSec=300
StartLimitBurst=5
[Container]
Image=docker.io/library/nginx:latest
AutoUpdate=registry
ContainerName=nginx-web
PublishPort=8080:80
Notify=true
# --- ESTRATEGIA DE REINTENTOS DEL HEALTHCHECK ---
# Comprueba la salud cada 10 segundos
HealthInterval=10s
# Si falla, reintenta hasta 5 veces antes de marcarlo como 'unhealthy'
# (Total: ~50-60 segundos de margen antes de disparar el rollback)
HealthRetries=5
# Comando de verificación
HealthCmd=curl -f http://localhost/ || exit 1
# Tiempo de gracia para que la app arranque (evita falsos negativos al inicio)
HealthStartPeriod=15s
HealthTimeout=5s
[Service]
# Identificador para el auto-update
Environment=PODMAN_SYSTEMD_UNIT=%n
# --- REINTENTOS DE SYSTEMD ---
# Si el contenedor muere, systemd lo reinicia automáticamente
Restart=on-failure
# Tiempo de espera entre reintentos de systemd
RestartSec=5s
# Tiempo máximo que systemd esperará a que el HealthCheck sea 'healthy'
# Debe ser mayor que (HealthInterval * HealthRetries) + HealthStartPeriod
TimeoutStartSec=120
[Install]
WantedBy=default.target
Por aclarar algunos aspectos,
StartLimitIntervalSec=300: Define la ventana de tiempo (5 minutos) en la que systemd contará los fallos de arranque.StartLimitBurst=5: Es el número máximo de intentos permitidos dentro de esa ventana. Si el contenedor falla 5 veces en 5 minutos, deja de intentar arrancarlo y márcalo como fallido (failed).Restart=on-failure: Le dice a systemd que si el contenedor se detiene con un código de error (crash) o si el Healthcheck lo marca como muerto, debe intentar levantarlo de nuevo automáticamente. No lo reiniciará si tú lo detienes manualmente (systemctl stop).RestartSec=5s: Es el tiempo de «respiro» entre un fallo y el siguiente intento de arranque. Evita el churning (intentos inmediatos que suelen fallar de nuevo si el sistema está saturado).Notify=true: Es probablemente el más importante para los rollbacks. Indica que systemd debe esperar una señal de «estoy listo» (READY=1) desde dentro del contenedor.- En Podman, si tienes un Healthcheck definido, Podman no enviará esa señal a systemd hasta que el Healthcheck pase por primera vez. Así, el servicio se queda en estado «activando» hasta que la app es realmente funcional.
Environment=PODMAN_SYSTEMD_UNIT=%n:%nes un especificador de systemd que se traduce automáticamente al nombre del archivo de unidad (ej.nginx-web.service).- Esta variable permite que el proceso hijo de Podman sepa exactamente qué unidad de systemd lo está controlando. Sin esto,
podman auto-updateno sabría qué servicio debe reiniciar tras descargar la imagen.
TimeoutStartSec=120: Es el tiempo máximo que systemd esperará a que el servicio pase de «activando» a «activo». Si tienesNotify=true, el contador empieza a correr. Si tras 120 segundos el Healthcheck no ha dado el visto bueno, systemd asume que la nueva versión de la imagen está rota, mata el proceso y activa el estado de error, lo que dispara el Rollback.
Por resumir los pasos,
- Podman descarga imagen nueva.
- Systemd arranca y pone el cronómetro a 120s.
- El contenedor falla o el Healthcheck no responde.
- Se agotan los 5 reintentos o los 120s.
- Systemd pone el servicio en
failed. podman-auto-updateve elfailed, deshace la actualización y tú recibes una notificación (o simplemente ves que la versión vieja sigue corriendo).
Una de notificaciones
¿Como enterarte cuando se produce un rollback? Pues de nuevo, gracias a la integración con Systemd. Para esto, hacemos un sencillo script que en caso de fallo nos envíe un mensaje a Telegram. El script puede ser algo como esto,
#!/bin/bash
TOKEN="TU_TELEGRAM_TOKEN"
CHAT_ID="TU_CHAT_ID"
URL="https://api.telegram.org/bot$TOKEN/sendMessage"
HOSTNAME=$(hostname)
LOGS=$(journalctl --user -u podman-auto-update.service -n 50)
if echo "$LOGS" | grep -q "rolling back"; then
UNIT=$(echo "$LOGS" | grep "rolling back" | sed -n 's/.*unit "\(.*\)".*/\1/p' | head -n 1)
STATUS="⚠️ *ROLLBACK EN $HOSTNAME*"
MESSAGE="La unidad \`$UNIT\` falló tras actualizarse. Podman ha restaurado la versión anterior para mantener el servicio online."
elif echo "$LOGS" | grep -q "updated"; then
UNITS=$(echo "$LOGS" | grep "updated" | sed -n 's/.*unit "\(.*\)".*/\1/p' | paste -sd ", " -)
STATUS="✅ *ACTUALIZACIÓN EXITOSA*"
MESSAGE="Se han actualizado correctamente: \`$UNITS\`."
else
exit 0
fi
TEXT="$STATUS%0A%0A$MESSAGE"
curl -s -X POST "$URL" \
-d chat_id="$CHAT_ID" \
-d text="$TEXT" \
-d parse_mode="Markdown" > /dev/null
Le damos permiso deejecución, y ahora nos queda añadir esto al servicio de auto actualización. Para ello, ejecutamos,
systemctl --user edit podman-auto-update.service
Y añadimos la siguiente sección,
[Service]
ExecStopPost=/home/lorenzo/scripts/notify-rollback.sh
Por resumir,
- Invisibilidad. El timer (podman-auto-update.timer) trabaja en silencio cada madrugada o a la hora que hayas definido.
- Resiliencia. Si la imagen viene corrupta, los reintentos y el TimeoutStartSec de tu Quadlet protegen el servicio.
- Seguridad. El Rollback automático levanta la versión estable antes de que nadie se dé cuenta.
- Información. Al terminar, el ExecStopPost dispara tu script y te envía un mensaje a Telegram.
Más información,