8

Contenedores como servicios systemd

Vistas: 2
Tutorial: Tutorial de Podman
Contenedores como servicios systemd

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í:

  1. Escribes un archivo mi-servicio.container con la configuración de tu contenedor.
  2. Lo guardas en ~/.config/containers/systemd/ (para rootless) o en /etc/containers/systemd/ (para root).
  3. Ejecutas systemctl --user daemon-reload (o systemctl daemon-reload para root).
  4. Quadlet genera automáticamente un archivo .service de systemd.
  5. 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ónPropósitoSección principal
.containerDefine un contenedor[Container]
.podDefine un pod (grupo de contenedores)[Pod]
.volumeDefine un volumen nombrado[Volume]
.networkDefine una red virtual[Network]
.kubeDefine un despliegue desde YAML Kubernetes[Kube]
.buildDefine una construcción de imagen[Build]
.imageDefine 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 a Requires, 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:

ClaveEquivalente podman runDescripción
ImageArgumento de imagen en podman runImagen del contenedor
ContainerName--nameNombre del contenedor
PublishPort-p / --publishPuertos publicados (host:contenedor)
Volume-v / --volumeVolúmenes montados
Environment-e / --envVariables de entorno
EnvironmentFile--env-fileArchivo con variables de entorno
ExecComando a ejecutarSobrescribe el CMD de la imagen
User-u / --userUsuario dentro del contenedor
GroupAdd--group-addAñadir grupos adicionales
Label-l / --labelEtiquetas para el contenedor
DNS--dnsServidor DNS
DNSOption--dns-optionOpciones de DNS
DNSSearch--dns-searchDominios de búsqueda DNS
AddHost--add-hostAñadir entradas a /etc/hosts
Network--networkRed del contenedor
NetworkAlias--network-aliasAlias de red
Pod--podAsignar a un pod
Capability--cap-addAñadir capacidades Linux
DropCapability--cap-dropQuitar capacidades Linux
SecurityLabel--security-optOpciones de seguridad SELinux/AppArmor
ReadOnly--read-onlySistema de archivos raíz de solo lectura
TmpFs--tmpfsMontar tmpfs
Secret--secretMontar secretos de Podman
LogDriver--log-driverDriver de logging (journald, k8s-file, etc.)
AutoUpdate--auto-updateHabilitar actualización automática de imagen
HealthCmd--health-cmdComando para healthcheck
HealthInterval--health-intervalIntervalo de healthcheck
HealthRetries--health-retriesReintentos de healthcheck
HealthStartPeriod--health-start-periodPeriodo de gracia inicial
HealthTimeout--health-timeoutTimeout del healthcheck
ShmSize--shm-sizeTamaño de /dev/shm
Ulimit--ulimitLímites del sistema (nofile, nproc, etc.)
Sysctl--sysctlParámetros del kernel en el contenedor
WorkingDir-w / --workdirDirectorio de trabajo dentro del contenedor
ExposeHostPort--exposeExponer 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 (systemctl sin --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.volume y After=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 para podman 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:

  1. Escanea los directorios de Quadlet en orden de prioridad.
  2. Lee los archivos .container, .pod, .volume, .network, .kube, .build e .image.
  3. Genera archivos .service (y .mount, .network de systemd para volúmenes y redes) temporales en /run/systemd/generator/.
  4. 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):

  1. /etc/containers/systemd/ — Configuración local del administrador.
  2. /usr/share/containers/systemd/ — Configuración distribuida por paquetes.
  3. /usr/local/share/containers/systemd/ — Configuración local adicional.

Para servicios de usuario (rootless):

  1. ~/.config/containers/systemd/ — Configuración del usuario.
  2. /etc/containers/systemd/users/ — Configuración global para todos los usuarios.
  3. /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 como generar-quadlets.sh, dale permisos de ejecución con chmod +x y ejecútalo directamente. Si prefieres una versión adaptada a Fish, puedes traducir los bucles for y 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, .build y .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, EnvironmentFile y secretos de Podman.
  • Dependencias: Cómo declarar dependencias entre contenedores, volúmenes y redes con Requires, After y BindsTo.
  • Auto-arranque: Cómo usar systemctl enable y loginctl enable-linger para rootless.
  • Logs con journald: Cómo ver y filtrar logs con journalctl.
  • El generador systemd: Cómo Quadlet transforma archivos .container en 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,

Deja una respuesta