Hasta ahora has visto como ejecutar contenedores, a gestionarlos con Quadlets, a crear tus propias imágenes y a orquestar aplicaciones multicontenedor. Pero hay dos aspectos que todo despliegue en producción —o en tu servidor casero— necesita sí o sí: gestión segura de secretos y actualizaciones automáticas.
Si alguna vez has metido una contraseña a fuego en un docker-compose.yml (todos lo hemos hecho), este capítulo te va a cambiar la vida. Y si tienes servicios corriendo 24/7 y te preocupa estar al día con las últimas versiones sin tener que hacer podman pull y podman stop/start manualmente cada semana, también.
En este capítulo veremos
- Podman Secrets: qué son, cómo crearlos, los distintos tipos (env, file), los drivers disponibles (file, pass, dbus), y cómo usarlos desde
podman runy desde Quadlets. - Podman Auto-Update: cómo configurar actualizaciones automáticas de contenedores, las políticas disponibles, la integración con Quadlets, los rollbacks automáticos, y cómo se compara con Watchtower.
- El patrón Cloe completo: un ejemplo real que combina secretos, auto-update, healthchecks y Quadlets.
Podman Secrets
Empecemos por los secretos, que probablemente sea lo que más falta te haga si vienes del mundo Docker Compose.
¿Qué son los Podman Secrets?
Un secreto en Podman es un dato sensible —una contraseña, una clave API, un token, un certificado— que se almacena fuera del contenedor y se inyecta en él solo cuando se necesita. El objetivo es claro: no quemar credenciales en la imagen, ni en variables de entorno visibles, ni en archivos de configuración versionados.
En Docker, los secrets llegaron con Docker Swarm y nunca funcionaron del todo bien en docker-compose a secas. En Podman, los secrets son una característica de primera clase que funciona tanto en modo root como rootless, con o sin systemd.
La filosofía es sencilla:
- Creas un secreto con
podman secret create. - Se almacena de forma segura (según el driver que uses).
- Cuando ejecutas un contenedor, le dices qué secretos montar.
- El contenedor accede al secreto como un archivo en
/run/secrets/o como variable de entorno.
Comandos básicos de gestión de secretos
Podman ofrece un conjunto completo de subcomandos para gestionar secretos:
| Comando | Descripción |
|---|---|
podman secret create | Crea un nuevo secreto |
podman secret list | Lista los secretos disponibles |
podman secret inspect | Muestra los metadatos de un secreto |
podman secret rm | Elimina uno o varios secretos |
podman secret ls | Alias de list |
podman secret create
El comando fundamental. Su sintaxis básica es:
podman secret create [opciones] NOMBRE [ARCHIVO]
Si no especificas un archivo, Podman lee el secreto desde la entrada estándar (stdin). Esto es útil para evitar que el secreto quede en el historial del shell:
echo "mi-contraseña-super-segura" | podman secret create mi_secreto -
O puedes leerlo de un archivo:
podman secret create mi_secreto ./clave-api.txt
podman secret list
Para ver los secretos que tienes creados:
$ podman secret list
ID NAME DRIVER CREATED UPDATED
1a2b3c4d mi_secreto file 5 minutes ago 5 minutes ago
5e6f7g8h db_password file 2 hours ago 2 hours ago
podman secret inspect
Para ver los metadatos de un secreto concreto:
$ podman secret inspect mi_secreto
[
{
"ID": "1a2b3c4d5e6f",
"CreatedAt": "2025-06-10T12:00:00Z",
"UpdatedAt": "2025-06-10T12:00:00Z",
"Spec": {
"Name": "mi_secreto",
"Driver": {
"Name": "file"
}
}
}
]
Importante: podman secret inspect no muestra el valor del secreto. Solo metadatos. El contenido está cifrado en disco aunque el driver sea file.
podman secret rm
Para eliminar un secreto:
podman secret rm mi_secreto
Puedes eliminar varios a la vez:
podman secret rm secreto1 secreto2 secreto3
Tipos de secrets: env vs file
Cuando montas un secreto en un contenedor, tienes dos formas de inyectarlo:
Secreto tipo env (variable de entorno)
El secreto se expone como una variable de entorno dentro del contenedor. Esto es útil para aplicaciones que leen credenciales de variables de entorno.
podman run -d --name mi-app --secret mi_secreto,type=env nginx:alpine
Dentro del contenedor, la variable de entorno MI_SECRETO contendrá el valor del secreto. El nombre de la variable se deriva del nombre del secreto: se convierte a mayúsculas y los guiones se reemplazan por guiones bajos.
Si quieres un nombre de variable personalizado:
podman run -d --name mi-app --secret mi_secreto,type=env,target=DB_PASSWORD nginx:alpine
Secreto tipo file (archivo — el más común)
El secreto se monta como un archivo dentro del contenedor, normalmente en /run/secrets/. Esta es la opción por defecto y la más recomendada, porque muchas aplicaciones están diseñadas para leer secretos de archivos.
podman run -d --name mi-app --secret mi_secreto nginx:alpine
Dentro del contenedor, el archivo /run/secrets/mi_secreto contendrá el valor. Puedes personalizar la ruta:
podman run -d --name mi-app --secret mi_secreto,target=/etc/secrets/clave.txt nginx:alpine
O cambiar los permisos:
podman run -d --name mi-app --secret mi_secreto,type=file,uid=1000,gid=1000,mode=0400 nginx:alpine
¿Cuál elegir?
| Tipo | Ventajas | Desventajas |
|---|---|---|
env | Fácil, compatible con apps que usan env vars | Menos seguro (env vars se heredan, se ven en /proc) |
file | Más seguro, soportado por la mayoría de apps | Requiere que la app sepa leer de archivos |
La recomendación general: usa file siempre que puedas. Es más seguro porque las variables de entorno pueden filtrarse accidentalmente (al hacer ps aux, al volcarlas en logs, al heredarse a procesos hijo). Los archivos en /run/secrets/ son más difíciles de filtrar.
Drivers de secretos
El driver determina dónde y cómo se almacena el secreto en el host. Podman soporta varios drivers:
Driver file (por defecto)
El driver por defecto. Los secretos se almacenan en el sistema de archivos del host, cifrados con una clave generada aleatoriamente. La ubicación típica es:
- Root:
/var/lib/containers/storage/secrets/ - Rootless:
~/.local/share/containers/storage/secrets/
No necesitas hacer nada especial para usarlo. Es el que se usa cuando no especificas --driver:
echo "clave-secreta" | podman secret create mi_secreto -
Ventajas: simple, no requiere configuración externa.
Desventajas: los secretos están en disco, aunque cifrados. Si alguien accede a tu máquina y a tu almacenamiento de contenedores, podría intentar descifrarlos.
Driver pass
El driver pass utiliza pass, el gestor de contraseñas estándar de Unix (que a su vez usa GPG). Los secretos se almacenan cifrados con tu clave GPG personal.
Para usarlo, primero necesitas tener pass instalado e inicializado:
# Instalar pass
sudo apt install pass # Debian/Ubuntu
sudo dnf install pass # Fedora
# Inicializar con tu clave GPG
pass init "tu-email@ejemplo.com"
Luego creas el secreto indicando el driver:
podman secret create --driver=pass mi_secreto_gpg ./secreto.txt
O puedes pasar directamente un archivo cifrado con GPG:
podman secret create --driver=pass mi_secreto ./secreto.txt.gpg
Ventajas: los secretos se almacenan cifrados con tu clave GPG personal. Se integran con tu flujo de trabajo de pass. Muy seguro.
Desventajas: necesitas tener pass configurado. Si pierdes tu clave GPG, pierdes los secretos. Es menos práctico para entornos automatizados sin intervención humana.
Driver dbus
El driver dbus utiliza el servicio dbus del sistema para almacenar los secretos, normalmente a través de un servicio de secretos como secret-tool (parte de libsecret).
podman secret create --driver=dbus mi_secreto_dbus ./secreto.txt
Ventajas: se integra con el llavero de secretos del sistema (GNOME Keyring, KDE Wallet, etc.). Muy seguro.
Desventajas: requiere un servicio dbus corriendo y un llavero configurado. No funciona bien en entornos sin sesión gráfica (servidores headless) o en contenedores rootless sin dbus.
Configuración global del driver
Puedes cambiar el driver por defecto en containers.conf:
[secrets]
driver = "pass"
O puedes configurarlo a nivel de usuario en ~/.config/containers/containers.conf.
Usar secrets con podman run
Ya has visto ejemplos básicos, pero veamos un caso más completo. Imagina que tienes una aplicación Node.js que necesita una clave de API de Stripe y una contraseña de MongoDB.
Primero, creas los secretos:
echo "sk_test_4eC39HqLyjWDarjtT1zdp7dc" | podman secret create stripe_api_key -
echo "mongodb+srv://admin:pass123@cluster.mongodb.net" | podman secret create mongo_url -
Luego ejecutas el contenedor montando ambos secretos:
podman run -d \
--name mi-app \
--secret stripe_api_key,type=env,target=STRIPE_API_KEY \
--secret mongo_url,type=file,target=/etc/secrets/mongo-url \
-p 3000:3000 \
mi-app:latest
Dentro del contenedor:
- La variable de entorno
STRIPE_API_KEYtendrá el valor. - El archivo
/etc/secrets/mongo-urltendrá la URL de MongoDB.
Usar secrets en Quadlets
Aquí es donde la cosa se pone interesante. En un Quadlet .container, usas la clave Secret:
[Unit]
Description=Aplicación con secretos
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/mi-app:latest
ContainerName=mi-app-secreta
PublishPort=3000:3000
Secret=stripe_api_key,type=env,target=STRIPE_API_KEY
Secret=mongo_url,type=file,target=/etc/secrets/mongo-url
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
Nota importante: Los secretos deben existir antes de que Quadlet intente arrancar el contenedor. Quadlet no crea secretos automáticamente. Puedes crearlos manualmente o mediante un script ExecStartPre:
[Service]
ExecStartPre=/bin/bash -c 'echo "${STRIPE_KEY}" | podman secret create stripe_api_key - 2>/dev/null || true'
O, mejor aún, usando un servicio systemd separado que se encargue de la creación de secretos.
Limpieza de secretos
Los secretos no se eliminan automáticamente al eliminar el contenedor. Tienes que gestionarlos explícitamente:
# Listar todos los secretos
podman secret ls
# Eliminar los que ya no uses
podman secret rm stripe_api_key mongo_url
Puedes automatizar la limpieza con un timer de systemd o incluirla en tus scripts de despliegue.
Buenas prácticas con Podman Secrets
- Nunca quemes secretos en imágenes. Si construyes una imagen que necesita un secreto, usa
--secretenpodman buildconRUN --mount=type=secreten el Containerfile. El secreto estará disponible durante el build pero no quedará en la imagen final. - Usa
type=filesiempre que puedas. Es más seguro que las variables de entorno. - Rota los secretos periódicamente. No uses el mismo secreto durante años. Ten un proceso para regenerarlos.
- No compartas secretos entre contenedores a menos que sea necesario. Cada contenedor debería tener sus propios secretos.
- Protege el almacenamiento. En producción, considera usar el driver
passodbus. El driverfilees cómodo pero los secretos están cifrados en disco con una clave almacenada junto a ellos. - Usa
--secretconpodman buildpara construir imágenes que usen secretos sin incluirlos. Este es un tema que merece un apartado propio dentro de los secretos de Podman, porque es una de las funcionalidades más potentes y menos conocidas.
Secretos en tiempo de build
Cuando construyes una imagen con podman build, a menudo necesitas credenciales para descargar dependencias privadas (paquetes npm privados, repositorios pip, claves SSH para clonar repositorios, etc.). Tradicionalmente, la gente usaba ARG o ENV para pasar estos datos, pero eso los dejaba visibles en la imagen final.
Con Podman, puedes usar el flag --secret en podman build y la instrucción RUN --mount=type=secret en el Containerfile. El flujo es:
Containerfile:
FROM node:20-alpine AS builder
# El secreto estará disponible solo durante este RUN
RUN --mount=type=secret,id=npmrc \
cp /run/secrets/npmrc ~/.npmrc && \
npm ci --only=production
FROM node:20-alpine
COPY --from=builder /app /app
CMD ["node", "index.js"]
Construcción:
echo "//registry.npmjs.org/:_authToken=npm_xxx" | \
podman build --secret id=npmrc,src=/dev/stdin -t mi-app .
El secreto npmrc se monta en /run/secrets/npmrc durante la ejecución del RUN, pero no queda en ninguna capa de la imagen final. Ni siquiera es visible con podman history. Es un verdadero secreto efímero.
Puedes usar múltiples secretos en un mismo build:
podman build \
--secret id=npmrc,src=./.npmrc \
--secret id=ssh_key,src=~/.ssh/id_ed25519 \
--secret id=api_token,src=/dev/stdin <<< "tok_xxx" \
-t mi-app .
Combinación con secretos de Podman existentes:
Ojo, que aquí hay una trampa que conviene aclarar para que no te lleves un susto. El flag --secret de podman build no lee del almacén de secretos de Podman, sino de un archivo o de una variable de entorno del sistema. Es decir, no existe una sintaxis tipo src=secret://... que consulte lo que guardaste con podman secret create. La forma documentada de pasar un secreto en tiempo de build es mediante un archivo (src=RUTA) o mediante una variable de entorno (env=VARIABLE o src=VARIABLE,type=env). Lo que sí puedes hacer es exportar el valor a una variable de entorno y pasársela al build, o leerlo directamente de un archivo:
# Opción con variable de entorno (la forma documentada)
export NPMRC="//registry.npmjs.org/:_authToken=npm_xxx"
podman build --secret id=npmrc,env=NPMRC -t mi-app .
# O con un archivo, que es lo más habitual y lo que suelo usar yo
podman build --secret id=npmrc,src=./.npmrc -t mi-app .
En el Containerfile, el RUN --mount=type=secret,id=npmrc monta el valor en /run/secrets/npmrc durante ese paso, y no queda rastro en la imagen final. La moraleja es sencilla, en tiempo de build el secreto viene de fuera del almacén de Podman, así que guarda el .npmrc en un sitio seguro de tu máquina o en tu gestor de contraseñas, y pásalo al build cuando lo necesites.
Visibilidad y depuración de secretos
Un aspecto importante: desde fuera del contenedor, no puedes inspeccionar el valor de un secreto. podman secret inspect solo muestra metadatos. El contenido está cifrado incluso en el driver file. Para verificarlo:
# Intentar leer el secreto directamente desde el almacenamiento
$ cat ~/.local/share/containers/storage/secrets/secrets.json
# Esto muestra datos cifrados, no el valor original
Si necesitas verificar que un secreto tiene el valor correcto, la forma segura es montarlo en un contenedor temporal:
# Montar el secreto y leerlo desde dentro
podman run --rm --secret mi_secreto alpine cat /run/secrets/mi_secreto
Obviamente, haz esto solo en entornos seguros y nunca compartas la salida.
Eliminación segura y limpieza
Cuando elimines un secreto con podman secret rm, los datos se borran del almacén de secretos. Sin embargo, si usaste el driver file, los archivos pueden quedar en disco hasta que el recolector de basura de Podman los limpie.
Para una eliminación más segura, puedes sobrescribir el archivo antes de crear el secreto y rotarlo periódicamente.
Además, ten en cuenta que los secretos se conservan aunque elimines el contenedor. Esto es intencional: el mismo secreto puede usarse en múltiples contenedores. Pero significa que debes limpiar explícitamente los secretos que ya no uses:
# Limpiar secretos huérfanos (no referenciados por ningún contenedor)
podman secret ls --filter 'driver=file' | grep -v ID | awk '{print $2}' | while read name; do
# Comprobar si algún contenedor lo usa
if ! podman inspect --format '{{.Name}}' $(podman ps -aq) 2>/dev/null | xargs -I {} sh -c \
'podman inspect {} | grep -q "'$name'" || echo '$name''; then
podman secret rm "$name"
fi
done
Quadlet Secret en profundidad
La clave Secret= en los Quadlets tiene una sintaxis más rica de lo que parece. La forma completa es:
Secret=nombre-secreto[,type=TIPO][,target=RUTA][,uid=UID][,gid=GID][,mode=MODO]
Todos los parámetros que admite podman run --secret están disponibles en Quadlet:
| Parámetro | Descripción | Valor por defecto |
|---|---|---|
nombre-secreto | Nombre del secreto en Podman | (obligatorio) |
type=TIPO | env o file | file |
target=RUTA | Variable de entorno (type=env) o ruta (type=file) | Nombre del secreto |
uid=UID | Propietario del archivo (solo type=file) | 0 |
gid=GID | Grupo del archivo (solo type=file) | 0 |
mode=MODO | Permisos del archivo en octal (solo type=file) | 0444 |
Ejemplos:
# Secreto montado como archivo con permisos restringidos
Secret=db_password,type=file,target=/etc/db/passwd,mode=0400
# Secreto como variable de entorno con nombre personalizado
Secret=api_key,type=env,target=STRIPE_SECRET_KEY
# Múltiples secretos
Secret=mi_secreto
Secret=otro_secreto,type=file,target=/etc/secrets/otro
Segunda parte: Podman Auto-Update
Ahora que sabes gestionar secretos de forma segura, pasemos a la segunda gran funcionalidad: las actualizaciones automáticas de contenedores.
¿Qué es podman auto-update?
podman auto-update es un comando integrado en Podman que busca nuevas versiones de las imágenes de tus contenedores y las actualiza automáticamente. Y cuando digo automáticamente, es automático de verdad: descarga la nueva imagen, detiene el contenedor actual, y levanta uno nuevo con la imagen actualizada.
Pero lo mejor no es eso. Lo mejor es que Podman incluye un mecanismo de rollback: si el contenedor actualizado no arranca correctamente, Podman vuelve automáticamente a la versión anterior. Sin intervención manual.
Cómo funciona
El flujo es el siguiente:
- Marcas un contenedor para auto-update con la etiqueta
io.containers.autoupdate=registry(o la claveAutoUpdate=registryen Quadlet). - Cuando ejecutas
podman auto-update(o se ejecuta mediante un timer de systemd), Podman:
- Consulta el registro de imágenes para ver si hay una versión más nueva.
- Si la hay, descarga la nueva imagen.
- Guarda una referencia a la imagen anterior (para el rollback).
- Detiene el contenedor actual usando systemd (
systemctl stop). - Arranca el contenedor con la nueva imagen.
- Espera a que el contenedor esté listo (healthcheck o sdnotify).
- Si el contenedor no arranca, hace rollback automático a la imagen anterior.
Políticas de auto-update
Podman soporta varias políticas que se especifican mediante la etiqueta io.containers.autoupdate (o la clave AutoUpdate en Quadlet):
| Política | Descripción |
|---|---|
registry | Comprueba el registro de imágenes. Si hay una versión nueva, la descarga y actualiza el contenedor. Requiere una referencia de imagen completa (con registro, no solo nombre). |
local | Actualiza el contenedor si la imagen local ha cambiado (por ejemplo, tras un podman pull o un podman build manual). No consulta el registro. |
disabled | Desactiva el auto-update para este contenedor. |
La política más utilizada es registry, que es la que realmente automatiza el proceso.
Requisito importante: imágenes fully-qualified
Para que registry funcione, la imagen debe estar especificada con su nombre completamente cualificado (fully-qualified image reference). Esto significa incluir el registro, el repositorio y el tag:
✅ Correcto:
docker.io/library/nginx:stable-alpine
quay.io/podman/stable:latest
ghcr.io/mi-usuario/mi-app:latest
❌ Incorrecto:
nginx:latest
mi-app:latest
Podman necesita saber exactamente a qué registro preguntar. Sin el registro, no puede consultar si hay una versión nueva.
Auto-update con Quadlets
La forma más elegante de configurar auto-updates es mediante Quadlets. Solo tienes que añadir AutoUpdate=registry en la sección [Container]:
[Unit]
Description=Mi servicio con auto-update
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/nginx:stable-alpine
ContainerName=mi-nginx-auto
PublishPort=8080:80
AutoUpdate=registry
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
Con este Quadlet, cada vez que ejecutes podman auto-update, Podman comprobará si hay una versión más nueva de nginx:stable-alpine en Docker Hub y, si la hay, actualizará el contenedor.
Con labels (alternativa)
Si no usas Quadlets, puedes marcar un contenedor con la etiqueta directamente al crearlo:
podman run -d \
--name mi-nginx \
--label io.containers.autoupdate=registry \
-p 8080:80 \
docker.io/library/nginx:stable-alpine
Ahora bien, aquí hay un matiz que conviene tener claro, y que a mí me costó un rato pillar. Las etiquetas de auto-update solo se pueden fijar en el momento de crear el contenedor. No existe un comando tipo podman container update --label ... que te permita añadir la etiqueta a un contenedor que ya está en marcha, porque podman container update solo gestiona recursos (CPU, memoria, healthchecks, ulimits) y no admite --label. Si te olvidaste de ponerla al crearlo, no queda otra que recrear el contenedor con la etiqueta. La forma más limpia es, precisamente, recrearlo:
# Eliminar el contenedor existente
podman rm -f mi-nginx
# Recrearlo con la etiqueta de auto-update
podman run -d \
--name mi-nginx \
--label io.containers.autoupdate=registry \
-p 8080:80 \
docker.io/library/nginx:stable-alpine
Si el contenedor lo gestiona systemd (por ejemplo, con podman generate systemd --new), la cosa es todavía más sencilla: editas la unidad o el Quadlet, recargas systemd y reinicias el servicio para que se cree de nuevo con la etiqueta. En el fondo es lo mismo de siempre, un contenedor es inmutable en lo que a su configuración de arranque se refiere, así que cualquier cambio pasa por recrearlo.
El timer de systemd: podman-auto-update.timer
Podman incluye un timer de systemd listo para usar. Se llama podman-auto-update.timer y, por defecto, ejecuta podman auto-update una vez al día.
Para activarlo:
Para contenedores del sistema (root):
sudo systemctl enable --now podman-auto-update.timer
Para contenedores rootless (usuario):
systemctl --user enable --now podman-auto-update.timer
Recuerda que si usas rootless, necesitas loginctl enable-linger para que los timers de usuario funcionen aunque no tengas sesión abierta:
loginctl enable-linger $USER
Puedes comprobar que el timer está activo:
$ systemctl --user list-timers --all
NEXT LEFT LAST PASSED UNIT ACTIVATES
Thu 2025-06-12 00:00:00 CEST 12h Wed 2025-06-11 00:00:00 CEST 12h podman-auto-update.timer podman-auto-update.service
¿Cada cuánto se ejecuta?
Por defecto, una vez al día a medianoche (00:00, hora local), con un pequeño retardo aleatorio para no saturar los registros de imágenes. Pero puedes cambiarlo editando el timer:
systemctl --user edit podman-auto-update.timer
Esto abre un archivo donde puedes sobreescribir la configuración:
[Timer]
OnCalendar=daily
RandomizedDelaySec=2h
Con RandomizedDelaySec, evitas que todos los sistemas del mundo golpeen los registros de imágenes a la misma hora. También puedes poner una hora específica:
[Timer]
OnCalendar=*-*-* 04:00:00
RandomizedDelaySec=1h
O cada 6 horas:
[Timer]
OnCalendar=*:0/6
O cada día laborable a las 3 de la madrugada:
[Timer]
OnCalendar=Mon..Fri 03:00:00
Solución de problemas comunes
Si el timer no se ejecuta correctamente, estos son los problemas más frecuentes:
El timer aparece como inactive (dead) en lugar de waiting.
Esto suele ocurrir porque el servicio falló en la última ejecución. Compruébalo con:
systemctl --user status podman-auto-update.service
Si ves un error, revísalo y corrige la causa (imagen incorrecta, secretos que faltan, etc.). Luego reinicia el timer:
systemctl --user reset-failed podman-auto-update.service
systemctl --user restart podman-auto-update.timer
El timer no arranca al iniciar sesión (rootless).
Asegúrate de tener loginctl enable-linger activado. Sin esto, los servicios de usuario solo se ejecutan mientras tienes una sesión abierta:
loginctl enable-linger $USER
El auto-update falla diciendo que la imagen no es fully-qualified.
Revisa el Quadlet o el comando podman run. La imagen debe incluir el registro completo. Por ejemplo, docker.io/library/nginx:alpine en lugar de nginx:alpine.
El contenedor se actualiza pero no hace rollback cuando falla.
Sin un healthcheck configurado, Podman no tiene forma de saber si el contenedor nuevo está funcionando. Añade HealthCmd al Quadlet. Además, asegúrate de que el servicio usa Type=notify (Quadlet lo hace por defecto, pero si generaste el servicio manualmente compruébalo).
systemd timer vs cron
Si vienes del mundo clásico de Linux, probablemente pienses en cron para programar tareas. Y sí, podrías añadir 0 6 * * * podman auto-update a tu crontab y funcionaría. Pero los timers de systemd tienen ventajas significativas:
| Aspecto | systemd timer | cron |
|---|---|---|
| Integración | Nativa con systemd (logs, estados, deps) | Independiente |
| Logs | journalctl -u podman-auto-update.service | Logs a syslog o a un archivo |
| Gestión de estado | systemctl list-timers, status | crontab -l (solo el programador) |
| Arranque perdido | Persistent=true hace que ejecute tareas saltadas | Se salta si el sistema estaba apagado |
| Dependencias | Puede esperar a que otros servicios arranquen | No tiene dependencias |
| Gestión | Se puede activar/desactivar con systemctl | Hay que editar crontab |
| Rootless | systemctl --user funciona de maravilla | Más engorroso con cron de usuario |
Recomendación: usa el timer de systemd. Es lo que Podman espera, está integrado, y te da visibilidad total.
Rollbacks automáticos
Esta es, probablemente, la funcionalidad más potente del auto-update de Podman. Y es lo que lo diferencia de casi cualquier otra solución.
¿Cómo funciona el rollback?
Cuando podman auto-update actualiza un contenedor, el proceso es:
- Preparación: Podman descarga la nueva imagen y guarda el digest de la imagen anterior.
- Actualización: Detiene el contenedor, lo elimina (si es necesario) y arranca uno nuevo con la imagen actualizada.
- Monitorización: Espera a que el contenedor indique que está listo. Aquí entran en juego dos mecanismos:
- Healthcheck: Si el contenedor tiene un healthcheck definido, Podman espera a que pase antes de considerar la actualización como exitosa.
- sdnotify: Si el contenedor usa
Type=notifyen systemd (que es el comportamiento por defecto con Quadlets), Podman espera a que el proceso dentro del contenedor envíe la señalREADY=1.
- Rollback: Si el contenedor no se vuelve saludable en un tiempo razonable (definido por
TimeoutStartSec), Podman:
- Detiene el contenedor nuevo.
- Vuelve a etiquetar la imagen anterior con el nombre original (
podman tag $oldDigest $name). - Arranca un nuevo contenedor con la imagen anterior.
- Marca la actualización como fallida.
Para que el rollback funcione de forma fiable, necesitas:
- Un healthcheck bien configurado en el contenedor.
Type=notifyen el servicio systemd (Quadlet lo hace automáticamente).TimeoutStartSecsuficiente para que el healthcheck tenga tiempo de ejecutarse.
Ejemplo de Quadlet con healthcheck y auto-update
[Unit]
Description=API con auto-update y rollback
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/miusuario/mi-api:stable
ContainerName=mi-api
PublishPort=3000:3000
AutoUpdate=registry
HealthCmd=curl -f http://localhost:3000/health || exit 1
HealthInterval=30s
HealthRetries=3
HealthStartPeriod=60s
HealthTimeout=10s
[Service]
Restart=always
TimeoutStartSec=120
Type=notify
[Install]
WantedBy=default.target
Con esta configuración:
podman auto-updatedescargará la nueva imagen dedocker.io/miusuario/mi-api:stable.- Arrancará el contenedor y esperará hasta 120 segundos (
TimeoutStartSec). - El contenedor debe enviar
READY=1por sdnotify y pasar el healthcheck. - Si todo va bien → actualización completada.
- Si algo falla → rollback automático a la versión anterior.
Limitaciones del rollback
El rollback no es mágico. Tiene limitaciones que debes conocer:
- Rollback a nivel de imagen, no de datos. Si la nueva versión modificó la base de datos (migraciones), el rollback no deshace esas migraciones.
- Requiere healthcheck o sdnotify. Sin ellos, Podman no tiene forma de saber si el contenedor nuevo funciona correctamente.
- TimeoutStartSec. Si el contenedor tarda más de lo esperado en arrancar, Podman asume que falló y hace rollback, aunque el contenedor estuviera a punto de estar listo.
- El rollback es automático, no manual. El flag
--rollbackdepodman auto-updatees un booleano (por defectotrue) que activa o desactiva el rollback automático ante un fallo del contenedor actualizado. No es un comando que «fuerce» un rollback a mano: si lo pones afalse(podman auto-update --rollback=false), Podman no volverá a la versión anterior cuando una actualización falle. El rollback manual de una actualización que sí funcionó pero resultó problemática no se hace desde el auto-update, sino a mano.
Para deshacer una actualización que sí funcionó pero resultó problemática, puedes hacerlo manualmente:
# Ver el historial de imágenes
podman history mi-api
# Volver a una versión anterior
podman tag docker.io/miusuario/mi-api:@sha256:anterior mi-api:stable
podman stop mi-api && podman rm mi-api
podman run -d --name mi-api ... mi-api:stable
Notificaciones
Podman auto-update no incluye un sistema de notificaciones integrado. No te envía un correo ni un mensaje de Telegram cuando se actualiza un contenedor. Si quieres notificaciones, tienes que construirlas tú mismo.
Algunas opciones:
1. Script wrapper con notificaciones
Creas un script que envuelve podman auto-update y envía notificaciones:
#!/bin/bash
# /usr/local/bin/podman-auto-update-with-notify.sh
OUTPUT=$(podman auto-update 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
# Enviar notificación de error
curl -s -X POST "https://api.telegram.org/bot$TOKEN/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="❌ Podman auto-update falló:%0A$OUTPUT"
elif echo "$OUTPUT" | grep -qi "updated"; then
# Enviar notificación de actualización
curl -s -X POST "https://api.telegram.org/bot$TOKEN/sendMessage" \
-d chat_id="$CHAT_ID" \
-d text="✅ Contenedores actualizados:%0A$OUTPUT"
fi
Y luego usas este script en lugar del timer por defecto:
# Sobrescribes el servicio
systemctl --user edit podman-auto-update.service
[Service]
ExecStart=
ExecStart=/usr/local/bin/podman-auto-update-with-notify.sh
2. Systemd OnFailure
Puedes usar la funcionalidad de systemd para ejecutar algo cuando el servicio falla:
[Unit]
Description=Notificar fallo de auto-update
OnFailure=notify-failure@%n.service
3. Apprise o similar
Herramientas como Apprise te permiten enviar notificaciones a decenas de servicios (Telegram, Slack, Email, Pushover, Discord, Matrix, y más de 80 servicios) con una sola línea:
podman auto-update || apprise -t "Auto-update falló" -b "Revisa los logs" \
telegram://BOT_TOKEN/CHAT_ID/
Puedes incluso configurarlo para que avise tanto si hay actualizaciones como si falla:
#!/bin/bash
OUTPUT=$(podman auto-update 2>&1)
RET=$?
if [ $RET -eq 0 ]; then
if echo "$OUTPUT" | grep -qi "updated"; then
apprise -t "✅ Contenedores Actualizados" -b "$OUTPUT" \
slack://tokenA/tokenB/tokenC/channel
fi
else
apprise -t "❌ Error en Auto-Update" -b "$OUTPUT" \
slack://tokenA/tokenB/tokenC/channel \
mailto://user:pass@gmail.com
fi
4. Integración con healthchecks.io
Healthchecks.io es un servicio de monitorización que espera «pulsos» periódicos de tus sistemas. Si un pulso no llega, te avisa. Puedes integrarlo con auto-update:
#!/bin/bash
# Enviar ping de inicio
curl -fsS -m 10 --retry 5 "https://hc-ping.com/UUID/start"
# Ejecutar auto-update
podman auto-update
EXIT_CODE=$?
# Enviar ping de éxito o fracaso
if [ $EXIT_CODE -eq 0 ]; then
curl -fsS -m 10 --retry 5 "https://hc-ping.com/UUID"
else
curl -fsS -m 10 --retry 5 "https://hc-ping.com/UUID/fail"
fi
Esto te da un dashboard donde ves si las actualizaciones automáticas se están ejecutando correctamente día tras día.
Auto-update con podman run (sin Quadlet)
Si no usas Quadlets, puedes configurar auto-update igualmente, pero el rollback automático solo funciona si el contenedor está gestionado por systemd (que es el caso cuando usas podman generate systemd).
# Crear el contenedor con la etiqueta
podman run -d \
--name mi-app \
--label io.containers.autoupdate=registry \
-p 3000:3000 \
docker.io/miusuario/mi-app:latest
# Generar el servicio systemd
podman generate systemd --new --name mi-app > ~/.config/systemd/user/mi-app.service
# Recargar y habilitar
systemctl --user daemon-reload
systemctl --user enable --now mi-app.service
Con --new en podman generate systemd, cada vez que el servicio arranque, crea un contenedor nuevo. Esto es clave para que el auto-update funcione correctamente, porque necesita reemplazar el contenedor por uno nuevo con la imagen actualizada.
Diferencias con Watchtower
Si vienes de Docker, seguramente conozcas Watchtower, una herramienta que monitoriza tus contenedores Docker y los actualiza automáticamente. Watchtower fue durante años la solución de facto para mantener contenedores actualizados. Pero tiene problemas conocidos: requiere un contenedor con acceso al socket de Docker (un riesgo de seguridad enorme), no tiene rollback automático, y depende de un daemon externo.
¿Cómo se compara con el auto-update de Podman?
| Aspecto | Podman Auto-Update | Watchtower |
|---|---|---|
| Arquitectura | Nativo de Podman, sin daemon extra | Contenedor adicional que debe estar corriendo |
| Dependencias | Ninguna. Viene con Podman. | Requiere Docker, un contenedor extra, y permisos elevados |
| Rollback | Integrado (con healthcheck/sdnotify) | No tiene rollback automático |
| Seguridad | Sin daemon, menor superficie de ataque | Contenedor con acceso a Docker socket (riesgo de seguridad masivo) |
| Integración systemd | Completa (timer, journalctl, dependencias) | Ninguna. Gestiona los reinicios por su cuenta |
| Políticas | registry, local, disabled | Varias (con labels, monitor, etc.) |
| Notificaciones | No incluye (hay que añadirlas) | Shoutrrr integrado (Slack, Telegram, email, 90+ servicios) |
| Configuración | Labels o Quadlet | Variables de entorno en el contenedor Watchtower |
| Rootless | Sí, funciona de serie | Complicado (necesita docker socket rootless) |
| Actualización selectiva | Sí, mediante labels | Sí, mediante labels |
| Actualización manual | podman auto-update | docker run containrrr/watchtower --run-once |
| Ciclo de actualización | Timer systemd personalizable | Intervalo configurable en el contenedor |
| Actualización sin reinicio | No (necesita recrear contenedor) | No (necesita recrear contenedor) |
| Soporte de registros privados | Nativo (usa ~/.docker/config.json) | Requiere pasar credenciales al contenedor |
| Actualización de pods completos | Sí, vía podman auto-update --pod | No tiene concepto de pods |
¿Cuándo usar cada uno?
- Usa Podman Auto-Update si ya estás usando Podman (y deberías). Es más seguro, más integrado, y te da rollback automático sin esfuerzo adicional. No necesitas instalar nada más:
podman auto-updateviene con Podman. - Usa Watchtower solo si estás atado a Docker y no puedes migrar a Podman. O si necesitas su sistema de notificaciones integrado sin querer montar nada por tu cuenta.
Pero siendo honestos: si estás leyendo este tutorial, probablemente ya has migrado a Podman o estás en ello. Y en ese caso, no necesitas Watchtower. El auto-update de Podman hace lo mismo, mejor, y sin riesgos de seguridad.
La ventaja decisiva: sin socket expuesto
El mayor problema de seguridad de Watchtower es que necesita acceso al socket de Docker. Esto significa que cualquier vulnerabilidad en Watchtower expone el control total de todos tus contenedores al atacante. Es como darle las llaves de tu casa al cartero para que te meta el correo por dentro.
Podman, al ser sin daemon, no tiene este problema. El auto-update lo ejecuta systemd con los permisos del usuario (o root), y los contenedores se actualizan mediante el mismo mecanismo que usas tú manualmente. No hay un socket especial que exponer, no hay un contenedor privilegiado corriendo 24/7, no hay superficie de ataque adicional.
Buenas prácticas con Auto-Update
- Usa imágenes fully-qualified. Siempre.
docker.io/library/nginx:stable-alpine, nonginx:latest. - Usa tags estables, no
latest.stable-alpine,20.04,lts. El taglatestcambia impredeciblemente y puede traerte sorpresas. - Configura healthchecks. Sin healthcheck, el rollback no puede determinar si el contenedor funciona correctamente.
- Prueba las actualizaciones en staging primero. Si puedes, ten un entorno de pruebas que reciba las actualizaciones antes que producción.
- Monitoriza los resultados. Aunque el rollback automático te salve de un desastre, deberías saber cuándo ocurre. Implementa las notificaciones que vimos antes.
- Ten un plan para migraciones de base de datos. El rollback restaura la versión anterior del software, pero no los cambios en la base de datos. Si tu aplicación hace migraciones automáticas al arrancar, el rollback podría dejarte en un estado inconsistente.
- Usa
RandomizedDelaySecpara evitar que todos los contenedores se actualicen a la vez y para no saturar los registros de imágenes.
API con secretos y auto-update
Llegamos a la parte práctica definitiva. Vamos a construir un despliegue completo que combina todo lo aprendido:
- Un contenedor Quadlet con una API hecha en Node.js.
- Secretos para la clave de API y la URL de la base de datos.
- Auto-update con política
registry. - Healthcheck para detectar fallos.
- Rollback automático si algo sale mal.
- Un timer systemd para las actualizaciones diarias.
Paso 1: Crear los secretos
# Secreto para la base de datos
echo "postgresql://user:pass@db-server:5432/miapp" | \
podman secret create db_url -
# Secreto para la API key
echo "sk-proj-4eC39HqLyjWDarjtT1zdp7dc" | \
podman secret create api_key -
# Verificar que están creados
podman secret ls
Paso 2: Crear el Quadlet
Archivo ~/.config/containers/systemd/mi-api.container:
[Unit]
Description=API de Mi Aplicación (Cloe)
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/miusuario/mi-api:stable
ContainerName=mi-api-cloe
PublishPort=8080:3000
# Secretos
Secret=db_url,type=env,target=DATABASE_URL
Secret=api_key,type=file,target=/run/secrets/api-key
# Auto-update
AutoUpdate=registry
# Healthcheck
HealthCmd=curl -f http://localhost:3000/healthz || exit 1
HealthInterval=30s
HealthRetries=3
HealthStartPeriod=90s
HealthTimeout=10s
# Recursos
Memory=512m
CPUs=1.0
[Service]
Restart=always
TimeoutStartSec=180
Type=notify
[Install]
WantedBy=default.target
Paso 3: Cargar el Quadlet
# Recargar systemd para que reconozca el nuevo Quadlet
systemctl --user daemon-reload
# Iniciar el servicio
systemctl --user start mi-api
# Verificar que funciona
systemctl --user status mi-api
journalctl --user -u mi-api -f
Paso 4: Activar auto-update diario
# Habilitar el timer de auto-update
systemctl --user enable --now podman-auto-update.timer
# Verificar el timer
systemctl --user list-timers --all | grep auto-update
Paso 5: Probar el auto-update manualmente
# Forzar una actualización (simula lo que haría el timer)
podman auto-update
# Ver qué pasó
podman auto-update --dry-run # Solo muestra lo que haría
Paso 6: Script de notificaciones (opcional)
Creamos un script que notifica en Telegram:
#!/bin/bash
# ~/.local/bin/podman-auto-update-notify.sh
TOKEN="tu_bot_token"
CHAT_ID="tu_chat_id"
OUTPUT=$(podman auto-update 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -ne 0 ]; then
MESSAGE="❌ *Auto-Update falló*%0A\`\`\`%0A$OUTPUT%0A\`\`\`"
elif echo "$OUTPUT" | grep -qi "updated"; then
MESSAGE="✅ *Contenedores actualizados*%0A\`\`\`%0A$OUTPUT%0A\`\`\`"
else
MESSAGE="ℹ️ *Auto-Update: sin cambios*%0A\`\`\`%0A$OUTPUT%0A\`\`\`"
fi
curl -s -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-d chat_id="$CHAT_ID" \
-d parse_mode="Markdown" \
-d text="$MESSAGE"
Y sobreescribimos el servicio de auto-update para que use este script:
systemctl --user edit podman-auto-update.service
Añadimos:
[Service]
ExecStart=
ExecStart=/home/tu-usuario/.local/bin/podman-auto-update-notify.sh
Paso 7: Verificar el rollback
Para probar que el rollback funciona, puedes desplegar una imagen que falle deliberadamente:
# Crear una imagen que falla al arrancar
podman run -d --name mi-api-rota \
--entrypoint /bin/false \
docker.io/miusuario/mi-api:stable
# Forzar auto-update (Podman detectará que la nueva imagen no arranca y hará rollback)
podman auto-update
Si todo está bien configurado, verás en los logs algo como:
$ journalctl --user -u mi-api -n 50
...
mi-api[]: Health check failed: exit code 1
mi-api[]: Performing rollback...
mi-api[]: Restarting with previous image docker.io/miusuario/mi-api@sha256:abc123...
mi-api[]: Container started successfully with rolled-back image
Resumen del capítulo
En este capítulo has aprendido dos de las funcionalidades más potentes de Podman:
Sobre Podman Secrets:
- Los secretos permiten inyectar datos sensibles en los contenedores sin quemarlos en imágenes ni en archivos de configuración.
- Hay dos tipos de montaje:
env(variable de entorno) yfile(archivo en/run/secrets/). - El driver por defecto es
file(cifrado en disco), pero puedes usarpass(cifrado GPG) odbus(llavero del sistema). - Se usan con
--secretenpodman runy conSecret=en Quadlets. - Se gestionan con
podman secret create,list,inspectyrm.
Sobre Podman Auto-Update:
podman auto-updateactualiza contenedores automáticamente consultando el registro de imágenes.- Las políticas disponibles son
registry,localydisabled. - En Quadlets se configura con
AutoUpdate=registry. - Incluye rollback automático si el contenedor actualizado no pasa el healthcheck.
- Se programa con el timer
podman-auto-update.timerde systemd. - No incluye notificaciones, pero puedes añadirlas fácilmente.
- Es una alternativa superior a Watchtower: más segura, más integrada, sin daemon extra.
El patrón Cloe completo te ha mostrado cómo combinar secretos, auto-update, healthchecks y Quadlets en un solo despliegue que es seguro, auto-actualizable y con rollback automático.
En el próximo capítulo veremos cómo llevar todo esto un paso más allá con la integración de Podman y systemd a nivel avanzado: notificaciones de estado, dependencias entre servicios, y cómo orquestar aplicaciones complejas sin Kubernetes.
Más información
- Documentación oficial de Podman Secrets
- Documentación oficial de Podman Auto-Update
- Gestión de secretos en Quadlets
- Watchtower (alternativa para Docker)