Llevas varios capítulos utilizando Podman para gestionar contenedores, imágenes y volúmenes. Has aprendido a ejecutar contenedores, a crear tus propias imágenes y a gestionar aplicaciones multicontenedor, aunque no siempre de la forma más elegante. Sin embargo, hasta ahora, cuando apagabas el sistema, tenías que acordarte de levantar los contenedores manualmente. O quizá te hayas peleado con scripts de inicio, con cron o con systemd para que tus contenedores arrancaran solos al encender el equipo.
¿Y si te dijera que existe una forma mucho más elegante, declarativa y profundamente integrada con systemd de gestionar tus contenedores? Bienvenido al mundo de los Quadlets.
Los Quadlets son probablemente la característica más potente y menos conocida de Podman. Te permiten definir contenedores, pods, volúmenes y redes como simples archivos de configuración que systemd convierte automáticamente en servicios gestionables con systemctl, journalctl y el resto del ecosistema systemd.
En este capítulo verás qué son los Quadlets, cómo utilizarlos, cómo vienen a sustituir a herramientas como docker-compose en muchos escenarios, y cómo convertir tus contenedores en servicios systemd de primera clase.
¿Qué son los Quadlets?
Un Quadlet no es más que un archivo de texto con formato INI (el mismo que usan los archivos de unidad de systemd) que describe de forma declarativa un contenedor, un pod, un volumen o una red de Podman. Cuando colocas estos archivos en los directorios adecuados, Podman, a través de su generador systemd, los convierte automáticamente en servicios de systemd.
El nombre «Quadlet» proviene de la combinación de «Podman» y «quadlet» (un juego de palabras con «quad» y «tablet»), y fue originalmente un proyecto externo que Red Hat acabó integrando directamente en Podman a partir de la versión 4.4.
La magia funciona así:
- Escribes un archivo
mi-servicio.containercon la configuración de tu contenedor. - Lo guardas en
~/.config/containers/systemd/(para rootless) o en/etc/containers/systemd/(para root). - Ejecutas
systemctl --user daemon-reload(osystemctl daemon-reloadpara root). - Quadlet genera automáticamente un archivo
.servicede systemd. - Gestionas tu contenedor con
systemctl --user start mi-servicio.
Y lo mejor: no necesitas recordar podman run con mil flags cada vez. El Quadlet lo declara todo de forma limpia y versionable.
Tipos de archivos Quadlet
Quadlet soporta varios tipos de archivos, cada uno con su extensión y su propósito específico:
| Extensión | Propósito | Sección principal |
|---|---|---|
.container | Define un contenedor | [Container] |
.pod | Define un pod (grupo de contenedores) | [Pod] |
.volume | Define un volumen nombrado | [Volume] |
.network | Define una red virtual | [Network] |
.kube | Define un despliegue desde YAML Kubernetes | [Kube] |
.build | Define una construcción de imagen | [Build] |
.image | Define una imagen a descargar | [Image] |
Los más utilizados son .container, .pod, .volume y .network. Con estos cuatro cubres casi cualquier escenario de despliegue.
Ventajas frente a docker-compose
Si vienes del mundo Docker, probablemente estés acostumbrado a docker-compose.yml. Y funciona bien, no te voy a engañar. Sin embargo, los Quadlets ofrecen ventajas significativas:
1. Integración nativa con systemd
Con docker-compose tienes que gestionar tú mismo el restart: always y confiar en que Docker Engine esté corriendo. Con Quadlets, tus contenedores son servicios systemd. Puedes usar systemctl enable, systemctl status, journalctl -u y todo el ecosistema.
2. Sin daemon central
Docker Compose depende del Docker Engine (un daemon monolítico). Podman es sin daemon. Los Quadlets ejecutan contenedores directamente como procesos hijo de systemd. Si systemd está vivo, tu contenedor está vivo. No hay un punto único de fallo.
3. Logs unificados con journald
Los logs de tus contenedores van directamente a journald. journalctl -u mi-servicio te muestra los logs del contenedor mezclados con los del servicio systemd. Puedes filtrar por prioridad, por rango de fechas, por unidad. Todo con las herramientas que ya conoces.
4. Declarativo y versionable
Un archivo .container es texto plano. Lo metes en Git, lo revisas, lo compartes. No necesitas herramientas externas. Otras herramientas como podman-compose también son declarativas, pero Quadlets van un paso más allá al integrarse con el sistema de init.
5. Dependencias explícitas
Quadlet entiende de dependencias entre servicios de forma nativa. Puedes decir «este contenedor necesita que la red X y el volumen Y estén creados antes de arrancar», y systemd se encarga del orden de inicio.
6. Auto-arranque garantizado
systemctl enable asegura que tu contenedor arranque al encender el sistema. No hay configuraciones mágicas de Podman. Es systemd puro y duro.
7. Rootless de serie
Los Quadlets funcionan perfectamente en modo rootless. Solo necesitas systemctl --user y, si quieres que arranquen al inicio sin sesión, activar loginctl enable-linger.
Estructura de un Quadlet
Un archivo Quadlet usa el formato INI, con secciones delimitadas por corchetes y pares clave=valor. La estructura básica es:
[Unit]
Description=Mi contenedor
Documentation=https://atareao.es/tutorial/podman/
After=network-online.target
Wants=network-online.target
[Container]
Image=docker.io/library/nginx:alpine
ContainerName=mi-nginx
PublishPort=8080:80
Volume=./html:/usr/share/nginx/html:Z
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target
Vamos a desglosar cada sección.
Sección [Unit]
Es la sección estándar de systemd. Aquí declaras metadatos y dependencias del servicio:
Description: Texto descriptivo del servicio.Documentation: Enlace a documentación.After: Indica qué unidades deben arrancar antes que este servicio.Wants: Indica dependencias débiles (si fallan, este servicio arranca igual).Requires: Dependencias fuertes (si fallan, este servicio no arranca).BindsTo: Similar aRequires, pero si la dependencia se para, este servicio también se para.PartOf: Si la unidad padre se para o se reinicia, esta unidad también.
Sección [Container]
Es el corazón del Quadlet. Aquí declaras cómo se ejecuta el contenedor. Las claves más importantes son:
| Clave | Equivalente podman run | Descripción |
|---|---|---|
Image | Argumento de imagen en podman run | Imagen del contenedor |
ContainerName | --name | Nombre del contenedor |
PublishPort | -p / --publish | Puertos publicados (host:contenedor) |
Volume | -v / --volume | Volúmenes montados |
Environment | -e / --env | Variables de entorno |
EnvironmentFile | --env-file | Archivo con variables de entorno |
Exec | Comando a ejecutar | Sobrescribe el CMD de la imagen |
User | -u / --user | Usuario dentro del contenedor |
GroupAdd | --group-add | Añadir grupos adicionales |
Label | -l / --label | Etiquetas para el contenedor |
DNS | --dns | Servidor DNS |
DNSOption | --dns-option | Opciones de DNS |
DNSSearch | --dns-search | Dominios de búsqueda DNS |
AddHost | --add-host | Añadir entradas a /etc/hosts |
Network | --network | Red del contenedor |
NetworkAlias | --network-alias | Alias de red |
Pod | --pod | Asignar a un pod |
Capability | --cap-add | Añadir capacidades Linux |
DropCapability | --cap-drop | Quitar capacidades Linux |
SecurityLabel | --security-opt | Opciones de seguridad SELinux/AppArmor |
ReadOnly | --read-only | Sistema de archivos raíz de solo lectura |
TmpFs | --tmpfs | Montar tmpfs |
Secret | --secret | Montar secretos de Podman |
LogDriver | --log-driver | Driver de logging (journald, k8s-file, etc.) |
AutoUpdate | --auto-update | Habilitar actualización automática de imagen |
HealthCmd | --health-cmd | Comando para healthcheck |
HealthInterval | --health-interval | Intervalo de healthcheck |
HealthRetries | --health-retries | Reintentos de healthcheck |
HealthStartPeriod | --health-start-period | Periodo de gracia inicial |
HealthTimeout | --health-timeout | Timeout del healthcheck |
ShmSize | --shm-size | Tamaño de /dev/shm |
Ulimit | --ulimit | Límites del sistema (nofile, nproc, etc.) |
Sysctl | --sysctl | Parámetros del kernel en el contenedor |
WorkingDir | -w / --workdir | Directorio de trabajo dentro del contenedor |
ExposeHostPort | --expose | Exponer puerto sin publicar |
Importante: Las rutas en Volume= pueden ser relativas al directorio donde está el archivo Quadlet, o absolutas. También puedes referenciar un volumen definido en otro Quadlet .volume usando el nombre del volumen.
Sección [Service]
Esta sección también es estándar de systemd. Controla el comportamiento del servicio:
Restart=always— Reinicia el contenedor siempre que se detenga.Restart=on-failure— Solo reinicia si falla.RestartSec=5s— Espera 5 segundos antes de reiniciar.TimeoutStartSec=900— Tiempo máximo para que el contenedor arranque (por defecto 90s, pero la descarga de imágenes puede llevar más).ExecStartPre=/bin/echo "Arrancando..."— Comandos a ejecutar antes de arrancar.
Quadlet establece automáticamente Type=notify para los .container y .kube, Type=forking para los .pod, y Type=oneshot para .volume, .network, .build, .image.
Sección [Install]
La sección de instalación estándar de systemd:
WantedBy=default.target— Hace que el servicio se active cuando se alcanza el target por defecto. Es el que usarás en servicios rootless (systemctl --user).WantedBy=multi-user.target— Para servicios del sistema con permisos de root (systemctlsin--user).RequiredBy=...— Similar a WantedBy, pero como dependencia fuerte.
La diferencia entre default.target y multi-user.target conviene conocerla. En servicios rootless, systemd –user arranca con default.target, que a su vez depende de basic.target. En servicios del sistema (root), el target habitual es multi-user.target, que representa el sistema funcionando sin interfaz gráfica. La regla es sencilla: si usas systemctl --user, usa default.target; si usas systemctl como root, usa multi-user.target. Pero si tienes dudas, default.target funciona en ambos casos.
Esta sección es la que permite ejecutar systemctl enable.
Variables de entorno
Las variables de entorno en Quadlets se manejan de varias formas:
Con la clave Environment
Directamente en el archivo Quadlet, una por línea:
[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=mi-postgres
Environment=POSTGRES_USER=admin
Environment=POSTGRES_PASSWORD=secreto123
Environment=POSTGRES_DB=miapp
PublishPort=5432:5432
Volume=postgres-data:/var/lib/postgresql/data
Con EnvironmentFile
Si prefieres mantener las variables separadas (por ejemplo, para no versionar contraseñas):
[Container]
Image=docker.io/library/postgres:16-alpine
ContainerName=mi-postgres
EnvironmentFile=/home/usuario/postgres.env
PublishPort=5432:5432
El archivo postgres.env tendría el formato:
POSTGRES_USER=admin
POSTGRES_PASSWORD=secreto123
POSTGRES_DB=miapp
Con Secretos de Podman
Para datos realmente sensibles, puedes usar secretos de Podman:
echo "secreto123" | podman secret create pg_password -
Y en el Quadlet:
[Container]
Image=docker.io/library/postgres:16-alpine
Secret=pg_password,type=env,target=POSTGRES_PASSWORD
Dependencias entre servicios
Una de las grandes ventajas de Quadlets es la gestión declarativa de dependencias. Puedes construir aplicaciones multicontenedor donde cada pieza declara de qué depende.
Ejemplo: WordPress con MySQL
Vamos a desplegar WordPress con MySQL usando Quadlets. Primero, el volumen para los datos:
wordpress-data.volume
[Unit]
Description=Volumen de datos de WordPress
[Volume]
Sí, es así de simple. Un Quadlet de volumen necesita poco más que la declaración.
mysql-data.volume
[Unit]
Description=Volumen de datos de MySQL
[Volume]
Ahora la red compartida:
wordpress.network
[Unit]
Description=Red de WordPress
[Network]
Subnet=10.89.0.0/24
El contenedor de MySQL:
mysql.container
[Unit]
Description=Base de datos MySQL para WordPress
Requires=mysql-data.volume
After=mysql-data.volume
Requires=wordpress.network
After=wordpress.network
[Container]
Image=docker.io/library/mysql:8.4
ContainerName=wordpress-db
Environment=MYSQL_ROOT_PASSWORD=rootpassword
Environment=MYSQL_DATABASE=wordpress
Environment=MYSQL_USER=wpuser
Environment=MYSQL_PASSWORD=wppassword
Volume=mysql-data.volume:/var/lib/mysql:Z
Network=wordpress.network
NetworkAlias=db
PublishPort=3306:3306
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
Y el contenedor de WordPress:
wordpress.container
[Unit]
Description=WordPress
Requires=mysql.container
After=mysql.container
Requires=wordpress.network
After=wordpress.network
Requires=wordpress-data.volume
After=wordpress-data.volume
[Container]
Image=docker.io/library/wordpress:6-php8.3-apache
ContainerName=wordpress-app
PublishPort=8080:80
Environment=WORDPRESS_DB_HOST=db:3306
Environment=WORDPRESS_DB_USER=wpuser
Environment=WORDPRESS_DB_PASSWORD=wppassword
Environment=WORDPRESS_DB_NAME=wordpress
Volume=wordpress-data.volume:/var/www/html:Z
Network=wordpress.network
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
Fíjate en cómo funciona la dependencia:
Requires=mysql.container— WordPress necesita que MySQL esté disponible.After=mysql.container— WordPress arranca después de MySQL.Requires=mysql-data.volumeyAfter=mysql-data.volume— El volumen debe estar creado antes.Network=wordpress.network— Ambos contenedores comparten la misma red.
Para activarlo todo:
mkdir -p ~/.config/containers/systemd/
# Copiar todos los archivos .container, .volume y .network
systemctl --user daemon-reload
systemctl --user start wordpress.container
Quadlet detecta las dependencias automáticamente. Al arrancar wordpress.container, systemd sabe que debe arrancar también mysql.container, wordpress-data.volume, mysql-data.volume y wordpress.network, y en el orden correcto.
Uso de pods con Quadlets
Los pods son una de las señas de identidad de Podman. Un pod es un grupo de contenedores que comparten la misma red, espacio de PID, y opcionalmente el mismo sistema de archivos. Con Quadlets, los pods se declaran con archivos .pod.
miapp.pod
[Unit]
Description=Pod para mi aplicación
[Pod]
Network=miapp.network
Ports=3000:3000
Ports=443:8443
backend.container
[Unit]
Description=Backend de la aplicación
Requires=miapp.pod
After=miapp.pod
BindsTo=miapp.pod
[Container]
Image=docker.io/library/node:20-alpine
ContainerName=backend
Pod=miapp.pod
Exec=node /app/server.js
Volume=./backend:/app:Z
Environment=NODE_ENV=production
[Service]
Restart=always
[Install]
WantedBy=default.target
frontend.container
[Unit]
Description=Frontend de la aplicación
Requires=miapp.pod
After=miapp.pod
BindsTo=miapp.pod
[Container]
Image=docker.io/library/nginx:alpine
ContainerName=frontend
Pod=miapp.pod
Volume=./frontend:/usr/share/nginx/html:Z
[Service]
Restart=always
[Install]
WantedBy=default.target
Al usar BindsTo=miapp.pod, si el pod se para, los contenedores se paran automáticamente. Y al compartir el pod, frontend y backend pueden comunicarse entre sí mediante localhost.
Auto-arranque con systemd
Una de las razones principales para usar Quadlets es el auto-arranque garantizado. Vamos a ver cómo configurarlo.
Para servicios del sistema (root)
sudo cp *.container /etc/containers/systemd/
sudo systemctl daemon-reload
sudo systemctl enable mi-servicio
sudo systemctl start mi-servicio
Para servicios de usuario (rootless)
cp *.container ~/.config/containers/systemd/
systemctl --user daemon-reload
systemctl --user enable mi-servicio
systemctl --user start mi-servicio
Importante: linger para rootless
Si usas rootless y quieres que los contenedores arranquen al encender el sistema sin que nadie inicie sesión, necesitas activar linger para tu usuario:
sudo loginctl enable-linger $USER
Esto crea el directorio /var/lib/systemd/linger/$USER, y systemd arrancará el servicio de usuario incluso sin sesión activa.
Puedes comprobarlo con:
loginctl show-user $USER | grep Linger
Logs con journald
Cuando ejecutas contenedores con Quadlets, los logs del contenedor se envían automáticamente a journald. Esto es muy potente, porque unifica toda la gestión de logs en una sola herramienta.
Ver logs de un contenedor Quadlet
# Logs completos del servicio
journalctl --user -u mi-servicio
# Últimas 50 líneas, en modo follow
journalctl --user -u mi-servicio -n 50 -f
# Logs desde hace 1 hora
journalctl --user -u mi-servicio --since "1 hour ago"
# Solo errores
journalctl --user -u mi-servicio -p err
# Logs de ayer
journalctl --user -u mi-servicio --since yesterday --until today
Configurar el driver de logging
Puedes elegir el driver de logging en el Quadlet:
[Container]
Image=docker.io/library/nginx:alpine
LogDriver=journald
Los drivers disponibles son:
journald— Envía logs a journald (recomendado con Quadlets).k8s-file— Escribe logs en archivos con formato Kubernetes.none— No guarda logs.passthrough— Pasa los logs directamente sin procesar (útil parapodman logs).
Diferencia entre journalctl y podman logs
Cuando usas Quadlets con LogDriver=journald, podman logs no funciona directamente porque los logs van a journald, no al archivo del contenedor. En su lugar, usas journalctl --user -u NOMBRE-SERVICIO.
Si quieres poder usar podman logs, puedes usar LogDriver=passthrough (disponible desde Podman 5.0):
[Container]
Image=docker.io/library/nginx:alpine
LogDriver=passthrough
O puedes usar k8s-file y seguir usando podman logs:
[Container]
Image=docker.io/library/nginx:alpine
LogDriver=k8s-file
Sin embargo, mi recomendación es usar journald y acostumbrarte a journalctl. La integración con el sistema es mucho más potente: puedes correlacionar logs del contenedor con logs del sistema, usar filtros avanzados, y aprovechar todas las capacidades de journald.
Logs persistentes
Por defecto, journald guarda los logs en memoria si no está configurada la persistencia. Para hacer los logs persistentes:
sudo mkdir -p /var/log/journal
sudo systemctl restart systemd-journald
El generador systemd de Quadlet
Entender cómo Quadlet genera los servicios systemd te ayudará a depurar problemas y a sacarle más partido.
Cuando systemd arranca (o cuando ejecutas daemon-reload), ejecuta el generador Quadlet (/usr/lib/systemd/system-generators/podman-system-generator). Este generador:
- Escanea los directorios de Quadlet en orden de prioridad.
- Lee los archivos
.container,.pod,.volume,.network,.kube,.builde.image. - Genera archivos
.service(y.mount,.networkde systemd para volúmenes y redes) temporales en/run/systemd/generator/. - Systemd carga estos servicios generados.
Ver el servicio generado
Puedes ver exactamente qué servicio systemd ha generado Quadlet:
# Para servicios rootless
systemctl --user cat mi-servicio
# Para servicios del sistema
systemctl cat mi-servicio
Esto te muestra el archivo .service completo que Quadlet ha generado. Por ejemplo, para un WordPress Quadlet:
systemctl --user cat wordpress
Verás algo como:
# /run/systemd/generator/wordpress-container.service
# autogenerated by Podman Quadlet
[Unit]
Description=WordPress
Documentation=...
Requires=mysql-container.service
After=mysql-container.service network-online.target
RequiresMountsFor=%t/containers
[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Restart=always
TimeoutStartSec=300
ExecStartPre=/bin/rm -f %t/%n.ctr-id
ExecStart=/usr/bin/podman run \
--name wordpress-app \
--cidfile=%t/%n.ctr-id \
--replace \
--sdnotify=conmon \
--network=wordpress.network \
-p 8080:80 \
-v wordpress-data.volume:/var/www/html:Z \
-e WORDPRESS_DB_HOST=db:3306 \
docker.io/library/wordpress:6-php8.3-apache
ExecStop=/usr/bin/podman stop --ignore --cidfile=%t/%n.ctr-id
ExecStopPost=/usr/bin/podman rm -f --ignore --cidfile=%t/%n.ctr-id
Type=notify
NotifyAccess=all
[Install]
WantedBy=default.target
Ver el servicio generado es muy útil para depurar problemas y entender exactamente qué flags se están pasando a podman run.
Directorios donde busca Quadlet
Quadlet busca archivos en los siguientes directorios (en orden de prioridad):
Para servicios del sistema (root):
/etc/containers/systemd/— Configuración local del administrador./usr/share/containers/systemd/— Configuración distribuida por paquetes./usr/local/share/containers/systemd/— Configuración local adicional.
Para servicios de usuario (rootless):
~/.config/containers/systemd/— Configuración del usuario./etc/containers/systemd/users/— Configuración global para todos los usuarios./usr/share/containers/systemd/users/— Configuración distribuida para usuarios.
Si hay archivos con el mismo nombre en varios directorios, el de mayor prioridad gana. Esto permite a los administradores sobrescribir configuraciones por defecto.
Script generador de Quadlets
Para facilitarte la vida, aquí tienes un script en Bash que genera Quadlets a partir de una definición sencilla de servicios. Es perfecto para cuando tienes que desplegar varios contenedores y no quieres escribir cada Quadlet a mano:
#!/bin/bash
# Script: generar-quadlets.sh
# Descripción: Genera archivos Quadlet a partir de una definición de servicios
# Autor: Generado para el tutorial de Podman
# Uso: ./generar-quadlets.sh servicios.conf
CONFIG="$1"
OUTPUT_DIR="${2:-./quadlets}"
if [ ! -f "$CONFIG" ]; then
echo "Error: No se encuentra el archivo de configuración: $CONFIG"
echo "Uso: $0 <archivo_config> [directorio_salida]"
exit 1
fi
mkdir -p "$OUTPUT_DIR"
# Variables globales
declare -A NETWORKS
declare -A VOLUMES
declare -A CONTAINERS
# Leer configuración
source "$CONFIG"
# Generar archivos de red
for net in "${!NETWORKS[@]}"; do
cat > "$OUTPUT_DIR/${net}.network" << EOF
[Unit]
Description=Red ${net}
[Network]
Subnet=${NETWORKS[$net]}
EOF
echo "✓ Generado: ${net}.network"
done
# Generar archivos de volumen
for vol in "${!VOLUMES[@]}"; do
cat > "$OUTPUT_DIR/${vol}.volume" << EOF
[Unit]
Description=Volumen ${vol}
[Volume]
EOF
echo "✓ Generado: ${vol}.volume"
done
# Generar archivos de contenedor
for container in "${!CONTAINERS[@]}"; do
# El formato esperado es: "imagen|puertos|variables|volumenes|red|dependencias"
IFS='|' read -r IMAGEN PUERTOS VARS VOLS RED DEPS <<< "${CONTAINERS[$container]}"
cat > "$OUTPUT_DIR/${container}.container" << EOF
[Unit]
Description=Contenedor ${container}
EOF
# Añadir dependencias si existen
if [ -n "$DEPS" ]; then
IFS=',' read -ra DEP_ARRAY <<< "$DEPS"
DEPS_LIST=""
AFTER_LIST=""
for dep in "${DEP_ARRAY[@]}"; do
DEPS_LIST="${DEPS_LIST} ${dep}.container"
AFTER_LIST="${AFTER_LIST} ${dep}.container"
done
cat >> "$OUTPUT_DIR/${container}.container" << EOF
Requires=${DEPS_LIST}
After=${AFTER_LIST}
EOF
fi
cat >> "$OUTPUT_DIR/${container}.container" << EOF
[Container]
Image=${IMAGEN}
ContainerName=${container}
EOF
# Añadir puertos
if [ -n "$PUERTOS" ]; then
IFS=',' read -ra PORT_ARRAY <<< "$PUERTOS"
for port in "${PORT_ARRAY[@]}"; do
echo "PublishPort=${port}" >> "$OUTPUT_DIR/${container}.container"
done
fi
# Añadir variables de entorno
if [ -n "$VARS" ]; then
IFS=',' read -ra VAR_ARRAY <<< "$VARS"
for var in "${VAR_ARRAY[@]}"; do
echo "Environment=${var}" >> "$OUTPUT_DIR/${container}.container"
done
fi
# Añadir volúmenes
if [ -n "$VOLS" ]; then
IFS=',' read -ra VOL_ARRAY <<< "$VOLS"
for vol in "${VOL_ARRAY[@]}"; do
echo "Volume=${vol}" >> "$OUTPUT_DIR/${container}.container"
done
fi
# Añadir red
if [ -n "$RED" ]; then
echo "Network=${RED}" >> "$OUTPUT_DIR/${container}.container"
fi
cat >> "$OUTPUT_DIR/${container}.container" << EOF
[Service]
Restart=always
TimeoutStartSec=300
[Install]
WantedBy=default.target
EOF
echo "✓ Generado: ${container}.container"
done
echo ""
echo "=== Quadlets generados en: $OUTPUT_DIR ==="
echo "Para usarlos:"
echo " cp $OUTPUT_DIR/* ~/.config/containers/systemd/"
echo " systemctl --user daemon-reload"
echo " systemctl --user start NOMBRE.container"
Y este es un ejemplo de archivo de configuración servicios.conf para el script:
#!/bin/bash
# Archivo de configuración para generar-quadlets.sh
# Define redes, volúmenes y contenedores
# Redes: nombre=subred
NETWORKS=(
["miapp-net"]="10.90.0.0/24"
)
# Volúmenes: nombre=descripción (vacío)
VOLUMES=(
["app-data"]=""
["db-data"]=""
)
# Contenedores: nombre="imagen|puertos|variables|volumenes|red|dependencias"
# - imagen: imagen de contenedor
# - puertos: separados por comas (host:container)
# - variables: separadas por comas (CLAVE=valor)
# - volumenes: separados por comas (nombre:ruta:opciones)
# - red: nombre de la red
# - dependencias: separadas por comas (nombres de contenedores)
CONTAINERS=(
["db"]="docker.io/library/postgres:16-alpine|5432:5432|POSTGRES_USER=admin,POSTGRES_PASSWORD=secret,POSTGRES_DB=miapp|db-data:/var/lib/postgresql/data:Z|miapp-net|"
["app"]="docker.io/library/node:20-alpine|3000:3000|NODE_ENV=production,DATABASE_URL=postgres://admin:secret@db:5432/miapp|app-data:/app/data:Z|miapp-net|db"
)
Nota para usuarios de Fish: El script está escrito en Bash y funciona perfectamente desde Fish porque Fish ejecuta los scripts con su shebang (
#!/bin/bash). No necesitas hacer nada especial. Simplemente guarda el script comogenerar-quadlets.sh, dale permisos de ejecución conchmod +xy ejecútalo directamente. Si prefieres una versión adaptada a Fish, puedes traducir los buclesfory las sintaxis de arrays, pero para este caso concreto el script original cumple su función sin problemas.
Errores comunes y cómo solucionarlos
Los Quadlets son potentes, pero no están exentos de problemas. Aquí tienes los errores más frecuentes y cómo resolverlos.
Error 1: «Neither a .container file nor a .pod file found»
Causa: Has ejecutado systemctl start nombre en lugar de systemctl start nombre.container.
Solución: Asegúrate de incluir la extensión .container al iniciar el servicio Quadlet:
systemctl --user start mi-servicio.container # Correcto
systemctl --user start mi-servicio # Incorrecto (error)
Error 2: El contenedor no arranca porque falta la imagen
Causa: Quadlet intenta arrancar el contenedor, pero la imagen no está descargada. Con TimeoutStartSec bajo (por defecto 90s), puede fallar si la imagen es grande.
Solución: Descarga la imagen primero, o aumenta TimeoutStartSec:
podman pull docker.io/library/wordpress:6-php8.3-apache
# O en el Quadlet:
[Service]
TimeoutStartSec=600
Error 3: No se genera el servicio (rootless)
Causa: El usuario no ha iniciado sesión con linger, o el directorio systemd no existe.
Solución:
# Activar linger
sudo loginctl enable-linger $USER
# Crear directorio si no existe
mkdir -p ~/.config/containers/systemd/
Error 4: podman logs no muestra nada con Quadlets
Causa: Quadlet usa LogDriver=journald por defecto, y podman logs no lee de journald.
Solución: Usa journalctl en su lugar:
journalctl --user -u nombre-servicio
# O cambia el driver en el Quadlet:
[Container]
LogDriver=k8s-file
Error 5: El contenedor se reinicia constantemente
Causa: El contenedor falla al arrancar y systemd lo reinicia en bucle.
Solución: Comprueba los logs para entender por qué falla:
journalctl --user -u mi-servicio --since "5 min ago"
Si el problema es de permisos SELinux, asegúrate de usar :Z (compartir) o :z (privado) en los volúmenes:
Volume=./data:/app/data:Z
Error 6: Conflictos de nombres con --replace
Causa: Si un contenedor con el mismo nombre ya existe, Quadlet falla al crear el nuevo.
Solución: Quadlet añade automáticamente --replace al comando podman run, pero si hay un conflicto con contenedores gestionados fuera de Quadlet, puedes forzar la limpieza:
podman rm -f nombre-contenedor
systemctl --user restart mi-servicio
Error 7: El servicio no arranca al iniciar sesión
Causa: No has ejecutado systemctl --user enable, o no tienes linger activado.
Solución:
systemctl --user enable mi-servicio
sudo loginctl enable-linger $USER
Error 8: Dependencias circulares
Causa: Has declarado dependencias cruzadas (A requiere B, B requiere A).
Solución: Systemd no puede resolver dependencias circulares. Revisa la arquitectura de tu aplicación y elimina la circularidad. Si dos contenedores se necesitan mutuamente, considera usar un pod.container
Resumen
En este capítulo has visto una de las características más potentes de Podman: los Quadlets,
- Qué son los Quadlets: Archivos declarativos que definen contenedores, pods, volúmenes y redes como servicios systemd.
- Los tipos de archivos:
.container,.pod,.volume,.network,.kube,.buildy.image. - Las ventajas frente a docker-compose: Integración nativa con systemd, sin daemon central, logs unificados con journald, dependencias explícitas, auto-arranque garantizado.
- La estructura de un Quadlet: Las secciones
[Unit],[Container],[Pod],[Volume],[Network],[Service]e[Install]. - Variables de entorno: Cómo usar
Environment,EnvironmentFiley secretos de Podman. - Dependencias: Cómo declarar dependencias entre contenedores, volúmenes y redes con
Requires,AfteryBindsTo. - Auto-arranque: Cómo usar
systemctl enableyloginctl enable-lingerpara rootless. - Logs con journald: Cómo ver y filtrar logs con
journalctl. - El generador systemd: Cómo Quadlet transforma archivos
.containeren servicios.service. - Un script generador: Para crear Quadlets automáticamente a partir de configuraciones.
- Errores comunes: Y cómo solucionarlos.
- El patrón Cloe completo: Una aplicación web completa con Flask, PostgreSQL, Redis y Nginx.
Los Quadlets transforman la forma en que gestionas contenedores en producción. Ya no necesitas scripts complejos, ni herramientas externas, ni recordar comandos crípticos. Tus contenedores se convierten en ciudadanos de primera clase en el ecosistema systemd.
En el próximo capítulo veremos cómo ejecutar contenedores sin privilegios de root con Podman rootless, una de las características que hace de Podman una herramienta tan segura. ¡No te lo pierdas!
Más información,