13

Red avanzada en Podman

Vistas: 0
Tutorial: Tutorial de Podman
Red avanzada en Podman

Durante meses tuve mis contenedores funcionando uno a uno, cada uno con su IP y su puerto, hasta que el desorden de puertos y la falta de TLS me obligaron a buscar algo mejor. Y entonces descubrí que Podman podía mucho más de lo que le estaba pidiendo. Hasta ahora hemos trabajado con contenedores individuales y pods locales. En este capítulo damos el salto a escenarios reales de producción: redes complejas, proxies inversos con TLS automatizado, interfaces gráficas de gestión, y la integración de herramientas del ecosistema Docker adaptadas a Podman.

Cubriremos cuatro grandes bloques que, combinados, te permitirán montar infraestructuras completas con Podman:

  1. El modelo de red de Podman — bridge, host, none, macvlan, ipvlan, slirp4netns, pasta, netavark y aardvark-dns.
  2. Redes personalizadas — subredes, puertas de enlace, DNS interno, integración con systemd-resolved.
  3. Traefik con Podman rootless — proxy inverso, descubrimiento automático de servicios, TLS vía Let’s Encrypt, HTTP/3, middlewares y socket activation.
  4. Dockge y Podman Desktop — interfaz web para docker-compose en Podman, y la GUI nativa de escritorio.

El modelo de red de Podman

Podman ofrece una arquitectura de red modular y pluggable. A diferencia de Docker, que usa un único demonio (dockerd) para gestionar la red, Podman separa el plano de control (netavark) del plano de datos (los distintos backends de red). Esto permite elegir el modo que mejor se adapte a cada carga de trabajo.

Netavark y Aardvark-dns

Netavark es el sustituto moderno de CNI (Container Network Interface) en Podman. Introducido como predeterminado en Podman 4.0, Netavark es un backend de red escrito en Rust que ofrece:

  • Configuración de bridges Linux nativos.
  • Soporte para IPv4 e IPv6.
  • Resolución DNS integrada mediante Aardvark-dns.
  • Capacidad de redes personalizadas tanto en modo rootful como rootless (a partir de Podman 4.0+).

Aardvark-dns es el servidor DNS ligero que acompaña a Netavark. Cuando creas una red personalizada, Aardvark se ejecuta como un proceso separado y registra automáticamente los nombres de los contenedores. Esto permite que los contenedores se resuelvan entre sí por nombre, sin necesidad de --link ni scripts externos.

Nota histórica: Antes de Netavark, Podman usaba el plugin CNI tradicional (proyecto containernetworking/plugins). Netavark es más rápido, más seguro (Rust vs C/Go) y está mejor integrado con el ecosistema Podman.

Modos de red (--network)

Los siguientes modos son los disponibles con podman run --network=<modo>.

bridge

Es el modo por defecto en rootful (ejecución como root). Crea un puente de red Linux (cni-podman0 o similar) y conecta cada contenedor a él mediante interfaces veth. Los contenedores obtienen una IP privada y pueden comunicarse entre sí a través del bridge.

# Rootful: bridge es el default
sudo podman run -d --name web --network bridge nginx:alpine

En rootless, el modo bridge también está disponible desde Podman 4.0 mediante Netavark, pero con limitaciones: los contenedores en una red bridge rootless se comunican entre sí pero no son accesibles desde el host directamente. La comunicación hacia el exterior se hace a través de slirp4netns o pasta.

# Rootless: bridge funciona con red personalizada
podman network create mired
podman run -d --name app1 --network mired nginx:alpine

host

El contenedor comparte directamente la pila de red del host. No hay aislamiento de red. Útil cuando necesitas máximo rendimiento de red o cuando el contenedor debe escuchar en una interfaz específica del host.

podman run --rm --network host alpine ip addr
# Verás las interfaces del host, no un namespace separado

Ventajas: Latencia cero, sin NAT, acceso directo a todas las interfaces.
Desventajas: Sin aislamiento, conflictos de puertos, menor seguridad.

none

El contenedor se inicia sin interfaz de red alguna (solo loopback). Ideal para tareas batch, utilidades de sistema o contenedores que no necesitan red.

podman run --rm --network none alpine sh -c "ip addr"
# Solo verás lo (loopback)

macvlan

Asigna una dirección MAC virtual al contenedor, haciéndolo aparecer como un dispositivo físico independiente en la red local. El contenedor recibe una IP del DHCP de la red física (o puedes asignarla estáticamente).

# Requiere root para crear la interfaz macvlan
sudo podman network create -d macvlan -o parent=eth0 macvlan-net
sudo podman run --rm --network macvlan-net alpine ip addr

Limitación rootless: No funciona en modo rootless porque los usuarios no privilegiados no pueden crear interfaces de red en el host.

Casos de uso: Servidores DHCP, servicios que necesitan su propia IP en la LAN, contenedores que deben ser accesibles directamente desde otros equipos de la red.

ipvlan

Similar a macvlan, pero todas las interfaces hijas comparten la misma dirección MAC del padre. La diferenciación se hace por IP (L3 en lugar de L2). Más eficiente que macvlan porque evita la saturación de la tabla MAC del switch.

sudo podman network create -d ipvlan -o parent=eth0 -o mode=l3 ipvlan-net

Diferencia clave: ipvlan no permite tráfico entre contenedores en el mismo segmento a menos que el router lo permita (L3 puro), mientras que macvlan sí lo permite (L2).

slirp4netns

Es el modo predeterminado en rootless cuando no se especifica ninguna red explícita (hasta Podman 5.0). Proporciona conectividad NAT hacia el exterior traduciendo tráfico TCP/UDP a través del espacio de nombres de red del usuario.

# Explícitamente slirp4netns (aunque es el default rootless en Podman <5)
podman run --rm --network slirp4netns alpine ping -c 3 8.8.8.8

Funcionamiento: Crea un espacio de nombres de red aislado y ejecuta slirp4netns como un proceso en user-space que traduce paquetes entre el namespace del contenedor y la pila del host.

Limitaciones:

  • Mayor latencia y menor throughput (~50–70% del rendimiento nativo).
  • No hay resolución DNS nativa (depende del /etc/resolv.conf del host copiado).
  • No soporta IPv6 de forma predeterminada (hay que activarlo).
  • Los contenedores no son visibles desde el host directamente.

pasta (passt)

Introducido como backend alternativo en Podman 4.4 y convertido en predeterminado rootless desde Podman 5.0. Pasta es una evolución de passt (Plug A Simple Socket Transport) adaptada para contenedores.

# En Podman 5.0+, este es el default rootless
podman run --rm --network pasta alpine ping -c 3 8.8.8.8

# O explícitamente
podman run --rm --network pasta alpine ip addr

Diferencias clave con slirp4netns:

Característicaslirp4netnspasta
Rendimiento~50–70% nativo~80–90% nativo
NATSí (user-space)Copia configuración host
IPv6LimitadoCompleto
Resolución DNS/etc/resolv.confHereda del host
Latencia+1–5 ms+0.1–0.5 ms
Uso de CPUAlto (user-space)Bajo (kernel bypass)

Pasta funciona reflejando (no traduciendo) la configuración de red del host dentro del contenedor. Esto elimina la sobrecarga de NAT y mejora drásticamente el rendimiento.

Cuándo usar cada uno:

  • Usa pasta como predeterminado en Podman 5.0+. Es más rápido y tiene mejor soporte de IPv6.
  • Usa slirp4netns solo si encuentras problemas de compatibilidad con aplicaciones antiguas o necesitas opciones muy específicas (port_handler=slirp4netns).

Puedes cambiar el backend predeterminado editando ~/.config/containers/containers.conf:

[network]
default_rootless_network_cmd = "slirp4netns"  # o "pasta"

Tabla comparativa de modos de red

ModoRootfulRootlessAislamientoRendimientoAcceso desde LAN
bridge✅*Alto~90%❌ (vía NAT)
hostNinguno100%Sí (directo)
noneTotalN/A
macvlanMedio~95%Sí (IP propia)
ipvlanMedio~95%Sí (IP propia)
slirp4netnsAlto~50–70%
pastaAlto~80–90%

✅* Bridge rootless requiere una red personalizada explícita (podman network create). No hay bridge por defecto para rootless.

Redes personalizadas

Creación de redes

El comando podman network create te permite definir redes con control granular sobre IP, subred, gateway, DNS y driver.

# Red bridge personalizada básica
podman network create frontend

# Red con subred y gateway explícitos
podman network create \
  --subnet 172.22.0.0/24 \
  --gateway 172.22.0.1 \
  --ip-range 172.22.0.64/26 \
  backend

# Red con IPv6 habilitado
podman network create \
  --subnet 10.99.0.0/24 \
  --subnet fd00:dead:beef::/64 \
  --ipv6 \
  dual-stack-net

Parámetros importantes:

  • --subnet: Define el rango de IPs (IPv4 y/o IPv6).
  • --gateway: IP del gateway dentro de la subred.
  • --ip-range: Rango específico para asignar IPs a contenedores (útil para reservar IPs fuera de ese rango).
  • --driver (-d): bridge, macvlan o ipvlan.
  • --internal: Red sin acceso al exterior (sin gateway NAT).
  • --dns: Servidor DNS personalizado para la red.

Conectar contenedores a redes

# Crear contenedor asignado a una red específica
podman run -d --name api --network backend alpine sleep 3600

# Conectar contenedor existente a otra red
podman network connect frontend api

# Ver las redes del contenedor
podman inspect api --format '{{json .NetworkSettings.Networks}}' | jq .

# Desconectar
podman network disconnect frontend api

Resolución DNS con Aardvark

Cuando usas Netavark, Aardvark-dns se encarga de la resolución de nombres dentro de las redes personalizadas.

# Crear dos contenedores en la misma red
podman network create demo-net
podman run -d --name server --network demo-net alpine sleep 3600
podman run -d --name client --network demo-net alpine sleep 3600

# El cliente puede resolver "server" por nombre
podman exec client ping -c 2 server

Aardvark asigna automáticamente un nombre DNS igual al nombre del contenedor. No necesitas --link ni archivos /etc/hosts manuales.

Puedes personalizar el alias DNS con:

podman run -d --name web --network demo-net --network-alias www.example.com nginx

Redes internas

Para servicios que no deben tener acceso al exterior:

podman network create --internal db-net
podman run -d --name postgres --network db-net postgres:16-alpine

Los contenedores en db-net se comunican entre sí pero no tienen salida a internet. El único contenedor que puede alcanzarlos es otro que esté conectado también a db-net.

Integración con systemd-resolved

Hasta aquí, la resolución DNS de Aardvark funciona dentro de la red de contenedores, pero el host sigue sin enterarse de nada. Si desde tu máquina haces ping server, el sistema no sabe quién es server porque ese nombre solo existe en el namespace de la red de Podman. La cuestión es que esto es un dolor cuando trabajas con servicios que quieres consultar directamente desde el host, así que te voy a contar cómo cerrar ese círculo.

En sistemas que usan systemd-resolved (Ubuntu, Fedora, Arch), puedes hacer que los nombres de contenedores sean resolubles desde el host de dos maneras distintas, y conviene que sepas cuándo usar cada una.

La vía rápida: --dns y el DNS del host

La primera opción es la más sencilla y la que te saca de un apuro en dos minutos. Consiste en crear la red apuntando a un servidor DNS que el host ya conoce, y que a su vez pueda resolver los nombres de los contenedores:

# Crear red con DNS habilitado para systemd-resolved
podman network create --dns 192.168.1.1 host-net

Esto le dice a Aardvark-dns que, para los nombres que no conozca dentro de la red, delegue en el servidor 192.168.1.1. Es una solución pragmática, pero tiene un límite claro: el host sigue sin resolver los nombres de los contenedores por sí mismo. Solo consigues que los contenedores resuelvan nombres externos a través de tu DNS de confianza, que es algo que por defecto ya hacen.

La vía completa: systemd-resolved como DNS de la red

La integración de verdad, la que te permite hacer ping server desde el host y que responda, pasa por hacer que Aardvark-dns escuche en un puerto que systemd-resolved pueda consultar. Para ello hay que tocar containers.conf:

# ~/.config/containers/containers.conf
[network]
network_backend = "netavark"
dns_bind_port = 53

Con dns_bind_port = 53, Aardvark-dns intenta escuchar en el puerto 53 del host. Y aquí viene el primer escollo, porque en la mayoría de las distros modernas ese puerto ya lo ocupa systemd-resolved escuchando en 127.0.0.53. Si ambos intentan quedarse con el 53, tendrás un conflicto de puertos que te va a dar más de un quebradero de cabeza.

La solución que a mí me funciona es liberar el puerto 53 para Aardvark y dejar que systemd-resolved haga de intermediario. El flujo completo es este:

  1. Detener el stub resolver de systemd-resolved para que suelte el puerto 53.
  2. Arrancar Aardvark-dns en el 53 para que sirva los nombres de los contenedores.
  3. Configurar systemd-resolved para que use Aardvark como upstream y así el host resuelva los nombres de los contenedores.

El primer paso se hace editando /etc/systemd/resolved.conf:

# /etc/systemd/resolved.conf
[Resolve]
DNSStubListener=no

Después reinicias el servicio y compruebas que el puerto 53 queda libre:

sudo systemctl restart systemd-resolved
sudo ss -tulpn | grep ':53'

Ahora que el 53 está libre, creas la red y compruebas que Aardvark se queda con el puerto:

podman network create host-net
podman run -d --name server --network host-net alpine sleep 3600
sudo ss -tulpn | grep ':53'
# Verás que ahora escucha aardvark-dns

El último paso es decirle a systemd-resolved que consulte a Aardvark. Añades una sección de DNS en /etc/systemd/resolved.conf apuntando al puerto 53 local:

# /etc/systemd/resolved.conf
[Resolve]
DNS=127.0.0.1#localhost
DNSStubListener=no

Reinicias de nuevo y, si todo ha ido bien, desde el host ya puedes resolver el nombre del contenedor:

sudo systemctl restart systemd-resolved
getent hosts server
# 10.89.0.2    server
ping -c 2 server

💡 Truco: El formato 127.0.0.1#localhost en DNS= le indica a systemd-resolved que use TLS hacia ese servidor. Si no quieres liarte con TLS, puedes usar el puerto alternativo de Aardvark con dns_bind_port = 5353 y apuntar DNS=127.0.0.1:5353. Menos elegante, pero te ahorra el dolor de cabeza del stub resolver.

⚠️ Advertencia: Si tu sistema usa systemd-resolved escuchando en 127.0.0.53, es posible que necesites reconfigurarlo o usar un puerto DNS alternativo para evitar conflictos. No te saltes este paso, porque el error típico es que Aardvark no arranca y no entiendes por qué. La culpa casi siempre es del puerto 53 ocupado.

Traefik con Podman rootless

Traefik es un proxy inverso moderno con descubrimiento automático de servicios, soporte nativo de Let’s Encrypt, HTTP/3 y un sistema de middlewares para modificar peticiones sobre la marcha.

El desafío de ejecutar Traefik con Podman rootless es doble:

  1. Puertos privilegiados: Los puertos 80 y 443 requieren privilegios especiales en modo rootless.
  2. Descubrimiento de servicios: Traefik necesita acceder al socket de Podman para leer las labels de los contenedores.

Preparación del sistema

Puertos no privilegiados

Para que un usuario rootless pueda escuchar en los puertos 80 y 443, hay que ajustar el rango de puertos no privilegiados del kernel:

# Como root, crear o editar /etc/sysctl.d/99-unprivileged-ports.conf
sudo tee /etc/sysctl.d/99-unprivileged-ports.conf << 'EOF'
net.ipv4.ip_unprivileged_port_start=80
EOF

# Recargar configuración
sudo sysctl --system

⚠️ Este cambio es global en el sistema. Alternativamente, puedes usar socket activation que evita esta modificación.

Habilitar el socket de Podman

Traefik necesita acceder a la API de Podman para descubrir contenedores:

# Habilitar el socket de Podman para el usuario
systemctl --user enable --now podman.socket

# Verificar que funciona
curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://localhost/_ping

Configuración básica de Traefik

Archivo traefik.yml

# ~/traefik/traefik.yml
api:
  dashboard: true
  debug: true

entryPoints:
  web:
    address: ":80"
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: ":443"

providers:
  podman:
    exposedByDefault: false
    endpoint: "unix:///run/user/1000/podman/podman.sock"

certificatesResolvers:
  letsencrypt:
    acme:
      email: tu@email.com
      storage: /etc/traefik/acme.json
      httpChallenge:
        entryPoint: web

Detalles importantes:

  • providers.podman.exposedByDefault: false — Solo exponer servicios que tengan traefik.enable=true.
  • endpoint — Apunta al socket de Podman del usuario (ajusta el UID 1000 si es diferente).
  • certificatesResolvers — Configura Let’s Encrypt vía HTTP-01 challenge.

Archivo de reglas dinámicas

# ~/traefik/dynamic.yml
http:
  middlewares:
    secure-headers:
      headers:
        frameDeny: true
        sslRedirect: true
        browserXssFilter: true
        contentTypeNosniff: true
        forceSTSHeader: true
        stsIncludeSubdomains: true
        stsPreload: true
        stsSeconds: 31536000

    rate-limit:
      rateLimit:
        average: 100
        burst: 50

Ejecutar Traefik con Podman

# Red para los proxies
podman network create traefik-net

# Directorio para almacenar certificados
mkdir -p ~/traefik/data

# Ejecutar Traefik
podman run -d \
  --name traefik \
  --network traefik-net \
  -p 80:80 \
  -p 443:443 \
  -v ~/traefik/traefik.yml:/etc/traefik/traefik.yml:ro,Z \
  -v ~/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro,Z \
  -v ~/traefik/data:/etc/traefik:Z \
  -v $XDG_RUNTIME_DIR/podman/podman.sock:/var/run/docker.sock:ro,z \
  --label "traefik.enable=true" \
  docker.io/traefik:v3.1

🔑 SELinux: Las etiquetas :Z y :z son importantes en sistemas con SELinux. :Z desetiqueta el volumen para el contenedor, :z lo comparte entre múltiples contenedores.

Descubrimiento de servicios mediante labels

Con el provider podman activo, Traefik lee las labels de los contenedores para configurar rutas automáticamente.

Ejemplo: Servicio whoami

podman run -d \
  --name whoami \
  --network traefik-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.whoami.rule=Host(\`whoami.tudominio.com\`)" \
  --label "traefik.http.routers.whoami.entrypoints=websecure" \
  --label "traefik.http.routers.whoami.tls=true" \
  --label "traefik.http.routers.whoami.tls.certresolver=letsencrypt" \
  --label "traefik.http.services.whoami.loadbalancer.server.port=80" \
  docker.io/traefik/whoami:v1.10

Labels clave explicadas:

LabelPropósito
traefik.enable=trueHabilita el descubrimiento para este contenedor
traefik.http.routers.whoami.ruleRegla de enrutamiento (host, path, headers, etc.)
traefik.http.routers.whoami.entrypointsPunto de entrada (web, websecure)
traefik.http.routers.whoami.tlsHabilita TLS para esta ruta
traefik.http.routers.whoami.tls.certresolverResolvedor de certificados
traefik.http.services.*.loadbalancer.server.portPuerto del servicio destino

Middlewares avanzados

Los middlewares permiten modificar peticiones antes de que lleguen al servicio destino.

Ejemplo: Autenticación básica + rate limiting

# Generar contraseña hasheada
htpasswd -nb admin MiPasswordSeguro

# Contenedor con middlewares
podman run -d \
  --name app-protegida \
  --network traefik-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.app.rule=Host(\`app.tudominio.com\`)" \
  --label "traefik.http.routers.app.entrypoints=websecure" \
  --label "traefik.http.routers.app.tls=true" \
  --label "traefik.http.routers.app.tls.certresolver=letsencrypt" \
  --label "traefik.http.routers.app.middlewares=auth,rate-limit,secure-headers@file" \
  --label "traefik.http.middlewares.auth.basicauth.users=admin:\$2y\$05\$..." \
  myapp:latest

Middlewares más útiles:

  • redirectRegex — Redirecciones por patrón.
  • replacePathRegex — Reescribir paths.
  • addPrefix — Añadir prefijo a la ruta.
  • headers — Modificar cabeceras HTTP (CORS, HSTS, Security).
  • rateLimit — Limitar peticiones.
  • circuitBreaker — Cortocircuito ante fallos.
  • retry — Reintentos automáticos.

Hasta aquí la teoría. Ahora te voy a mostrar cómo se montan en la práctica los cuatro que más juego dan en un homelab, con sus labels completas para que las copies y las adaptes. Todos se definen como middlewares en el provider de Traefik, ya sea en el archivo dinámico o directamente como labels en el contenedor.

redirectRegex — redirigir por patrón

Imagina que has movido un servicio de blog.tudominio.com a notas.tudominio.com y quieres que quien visite la URL antigua acabe en la nueva sin romper los enlaces que ya están publicados. Con redirectRegex capturas el patrón y lo reescribes:

podman run -d \
  --name blog-antiguo \
  --network traefik-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.redirect.rule=Host(\`blog.tudominio.com\`)" \
  --label "traefik.http.routers.redirect.entrypoints=websecure" \
  --label "traefik.http.routers.redirect.tls=true" \
  --label "traefik.http.routers.redirect.tls.certresolver=letsencrypt" \
  --label "traefik.http.routers.redirect.middlewares=redirect-blog" \
  --label "traefik.http.middlewares.redirect-blog.redirectregex.regex=^https://blog\\.tudominio\\.com/(.*)" \
  --label "traefik.http.middlewares.redirect-blog.redirectregex.replacement=https://notas.tudominio.com/\${1}" \
  --label "traefik.http.middlewares.redirect-blog.redirectregex.permanent=true" \
  docker.io/traefik/whoami:v1.10

Fíjate en el \${1}: el $ hay que escaparlo para que el shell no lo interprete como una variable y Traefik reciba la referencia al grupo capturado. El permanent=true hace que la redirección sea 301 (movido permanentemente), que es lo correcto para que los buscadores actualicen el índice.

replacePathRegex — reescribir el path

Este es el que te salva cuando el backend espera una ruta distinta de la que expones. Un ejemplo clásico: tienes una API que solo responde en /v1 pero quieres que los clientes la llamen sin el prefijo. Reescribes el path antes de que llegue al servicio:

podman run -d \
  --name api \
  --network traefik-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.api.rule=Host(\`api.tudominio.com\`)" \
  --label "traefik.http.routers.api.entrypoints=websecure" \
  --label "traefik.http.routers.api.tls=true" \
  --label "traefik.http.routers.api.tls.certresolver=letsencrypt" \
  --label "traefik.http.routers.api.middlewares=strip-v1" \
  --label "traefik.http.middlewares.strip-v1.replacepathregex.regex=^/v1/(.*)" \
  --label "traefik.http.middlewares.strip-v1.replacepathregex.replacement=/\${1}" \
  docker.io/traefik/whoami:v1.10

Con esto, una petición a api.tudominio.com/v1/usuarios llega al backend como /usuarios. Es la diferencia entre tener que adaptar la aplicación o resolverlo en el proxy, y te aseguro que resolverlo en el proxy es mucho menos doloroso.

circuitBreaker — cortocircuito ante fallos

Cuando un servicio empieza a fallar, lo último que quieres es que Traefik siga mandándole tráfico y que la experiencia se degrade para todo el mundo. El circuit breaker vigila el estado del backend y, si se supera un umbral de errores, deja de enviarle peticiones durante un tiempo. Es el equivalente a un fusible:

podman run -d \
  --name app-fragil \
  --network traefik-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.fragil.rule=Host(\`app.tudominio.com\`)" \
  --label "traefik.http.routers.fragil.entrypoints=websecure" \
  --label "traefik.http.routers.fragil.tls=true" \
  --label "traefik.http.routers.fragil.tls.certresolver=letsencrypt" \
  --label "traefik.http.routers.fragil.middlewares=breaker" \
  --label "traefik.http.middlewares.breaker.circuitbreaker.expression=NetworkErrorRatio() > 0.5 || LatencyAtQuantileMS(50.0) > 5000" \
  myapp:latest

La expresión se lee así: se abre el circuito si más de la mitad de las peticiones dan error de red, o si la latencia mediana supera los 5 segundos. Cuando se dispara, Traefik responde con un 503 en lugar de seguir golpeando a un servicio que está en las últimas. Puedes ajustar los umbrales a tu tolerancia, pero esta configuración es un buen punto de partida.

retry — reintentos automáticos

El complemento natural del breaker. Si un servicio falla de forma puntual y transitoria, un reintento automático puede salvar la petición sin que el usuario note nada. Eso sí, úsalo con cabeza, porque reintentar una petición que no es idempotente (por ejemplo, un POST que crea un recurso) puede duplicar efectos secundarios:

podman run -d \
  --name app-reintentos \
  --network traefik-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.retry.rule=Host(\`app.tudominio.com\`)" \
  --label "traefik.http.routers.retry.entrypoints=websecure" \
  --label "traefik.http.routers.retry.tls=true" \
  --label "traefik.http.routers.retry.tls.certresolver=letsencrypt" \
  --label "traefik.http.routers.retry.middlewares=retry-3" \
  --label "traefik.http.middlewares.retry-3.retry.attempts=3" \
  --label "traefik.http.middlewares.retry-3.retry.initialinterval=500ms" \
  myapp:latest

Aquí Traefik intenta hasta 3 veces, esperando medio segundo entre intento e intento. Si el servicio sigue caído, devuelve el error al cliente. Es una red de seguridad barata para esos fallos que aparecen y desaparecen.

💡 Consejo: Combina retry con circuitBreaker con cuidado. Si el breaker ya está abierto, el retry no tiene sentido porque Traefik ni siquiera intentará conectar. La combinación que funciona es breaker en el router principal y retry para errores transitorios de un servicio que sabemos que a veces titubea.

HTTP/3 (QUIC)

HTTP/3 sobre QUIC requiere configuración específica. A partir de Traefik v3.0, se puede habilitar directamente. Y aquí va un aviso que te ahorrará un rato de búsquedas: si has usado Traefik v2, quizá recuerdes que había que activar HTTP/3 con el flag experimental.http3: true. Pues bien, en v3 esa opción desapareció. En la versión actual basta con declarar http3: {} en el entrypoint, y Traefik levanta el socket UDP de QUIC él solo. No hay que tocar nada más:

# En traefik.yml
entryPoints:
  web:
    address: ":80"
  websecure:
    address: ":443"
    http3: {}

Qué es QUIC y por qué te importa

Antes de meternos en el fregado de la configuración, conviene que sepas qué estás activando. QUIC es el protocolo de transporte sobre el que se asienta HTTP/3, y su gran diferencia con TCP es que va sobre UDP. En lugar de abrir una conexión TCP y luego negociar TLS por encima, QUIC integra el cifrado en el propio protocolo y multiplexa varias peticiones en una sola conexión sin el problema de head-of-line blocking que sufre HTTP/2. El resultado es una latencia de conexión menor, sobre todo en redes con pérdida de paquetes, y una reconexión más ágil cuando cambias de red en el móvil.

La cuestión es que ese cambio de TCP a UDP no es gratuito en nuestro escenario, porque el puerto 443 deja de ser solo TCP y pasa a necesitar también un socket UDP en el mismo número de puerto. Y aquí es donde empiezan los problemas con Podman rootless.

Por qué el socket UDP colisiona con socket activation

Te voy a recomendar usar socket activation para no tener que tocar ip_unprivileged_port_start. Pues bien, la socket activation de systemd escucha en los puertos que le declares en el archivo .socket, y lo hace tanto en TCP como en UDP si así se lo pides con ListenDatagram. Hasta aquí todo bien.

El problema aparece cuando Traefik, dentro del contenedor, intenta escuchar en el socket UDP que systemd le ha pasado por socket activation y a la vez en la red personalizada de Podman. Podman, para las redes personalizadas, gestiona sus propios puertos publicados, y cuando Traefik intenta abrir dos sockets UDP sobre el mismo puerto (el de socket activation y el de la red), se produce una colisión que acaba en un panic: connection already exists. Es un error que te va a hacer rascar la cabeza un buen rato si no sabes qué lo provoca, porque el contenedor arranca, pero Traefik se cae en cuanto intenta levantar el entrypoint UDP.

El workaround completo

La solución es separar las responsabilidades: que la escucha UDP externa la gestione systemd mediante socket activation, y que la red interna de Podman se quede solo con TCP. Así no hay dos sockets compitiendo por el mismo puerto. El archivo de socket queda así:

# ~/.config/systemd/user/traefik.socket
[Unit]
Description=Traefik socket activation (TCP + UDP)

[Socket]
ListenStream=80
ListenStream=443
ListenDatagram=443
FreeBind=true

[Install]
WantedBy=sockets.target

Y en el contenedor, publicas solo el puerto TCP en la red interna, dejando que el UDP lo sirva systemd:

podman run -d \
  --name traefik \
  --network traefik-net \
  --sdnotify=conmon \
  -p 443:443/tcp \
  -v ~/traefik/traefik.yml:/etc/traefik/traefik.yml:ro,Z \
  -v ~/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro,Z \
  -v ~/traefik/data:/etc/traefik:Z \
  -v $XDG_RUNTIME_DIR/podman/podman.sock:/var/run/docker.sock:ro,z \
  docker.io/traefik:v3.1

Fíjate en el detalle de -p 443:443/tcp: publicamos solo TCP en la red de Podman, y el UDP queda en manos de systemd, que se lo pasa a Traefik por socket activation. De esta forma cada uno gestiona su socket y no hay colisión.

Puedes comprobar que HTTP/3 responde consultando el puerto UDP directamente:

# Desde el host, verificar que el socket UDP 443 está activo
ss -ulpn | grep 443

# Y con un cliente HTTP/3 (por ejemplo, curl compilado con soporte HTTP/3)
curl --http3-only https://tudominio.com

⚠️ Problema conocido: Con socket activation y redes personalizadas, HTTP/3 puede lanzar panic: connection already exists porque el socket UDP compartido colisiona. La solución es separar la escucha externa (socket activation) de la interna (red personalizada). Consulta el issue #11805 de Traefik para más detalles.

💡 Realismo: HTTP/3 es una mejora notable en latencia, pero si tu homelab es para uso personal y no tienes usuarios golpeando la web desde redes móviles con pérdida de paquetes, la diferencia la vas a notar poco. Actívalo si te apetece experimentar, pero no te sientas obligado. HTTP/2 sobre TCP sigue siendo perfectamente válido para la mayoría de los casos.

Socket activation (práctica recomendada)

La socket activation de systemd permite que el sistema operativo escuche en los puertos 80 y 443 y active Traefik bajo demanda. Esto elimina la necesidad de modificar ip_unprivileged_port_start.

Archivo de socket:

# ~/.config/systemd/user/traefik.socket
[Unit]
Description=Traefik socket activation

[Socket]
ListenStream=80
ListenStream=443
ListenDatagram=443
FreeBind=true

[Install]
WantedBy=sockets.target

Archivo de servicio:

# ~/.config/systemd/user/traefik.service
[Unit]
Description=Traefik reverse proxy
After=network-online.target
Wants=network-online.target

[Service]
ExecStartPre=/usr/bin/podman pull docker.io/traefik:v3.1
ExecStart=/usr/bin/podman run \
  --name traefik \
  --network traefik-net \
  --sdnotify=conmon \
  -v %h/traefik/traefik.yml:/etc/traefik/traefik.yml:ro,Z \
  -v %h/traefik/dynamic.yml:/etc/traefik/dynamic.yml:ro,Z \
  -v %h/traefik/data:/etc/traefik:Z \
  -v %t/podman/podman.sock:/var/run/docker.sock:ro,z \
  docker.io/traefik:v3.1
Type=notify
NotifyAccess=all

[Install]
WantedBy=default.target

Activar socket activation:

systemctl --user daemon-reload
systemctl --user enable --now traefik.socket
# El servicio se activa automáticamente al recibir la primera conexión
# systemctl --user start traefik.service  # opcional

Escenario completo: WordPress con Traefik

Como ejemplo integrador, despleguemos WordPress con Traefik como proxy inverso, TLS automático y base de datos separada.

Red y volúmenes:

podman network create wp-net
podman volume create wp-db
podman volume create wp-files

Base de datos:

podman run -d \
  --name wp-db \
  --network wp-net \
  --label "traefik.enable=false" \
  -e MYSQL_ROOT_PASSWORD=rootpass \
  -e MYSQL_DATABASE=wordpress \
  -e MYSQL_USER=wpuser \
  -e MYSQL_PASSWORD=wppass \
  -v wp-db:/var/lib/mysql:Z \
  docker.io/mariadb:11

WordPress:

podman run -d \
  --name wordpress \
  --network wp-net \
  --label "traefik.enable=true" \
  --label "traefik.http.routers.wp.rule=Host(\`blog.tudominio.com\`)" \
  --label "traefik.http.routers.wp.entrypoints=websecure" \
  --label "traefik.http.routers.wp.tls=true" \
  --label "traefik.http.routers.wp.tls.certresolver=letsencrypt" \
  --label "traefik.http.services.wp.loadbalancer.server.port=80" \
  --label "traefik.http.routers.wp.middlewares=secure-headers@file" \
  -e WORDPRESS_DB_HOST=wp-db \
  -e WORDPRESS_DB_USER=wpuser \
  -e WORDPRESS_DB_PASSWORD=wppass \
  -e WORDPRESS_DB_NAME=wordpress \
  -v wp-files:/var/www/html:Z \
  docker.io/wordpress:6

Con esta configuración, blog.tudominio.com sirve WordPress vía HTTPS con certificados automáticos de Let’s Encrypt, sin exponer puertos adicionales.

Dockge como interfaz para Podman

Dockge (de Louis Lam, creador de Uptime Kuma) es una interfaz web orientada a stacks de docker-compose. Su filosofía es simple: cada stack es un directorio con su compose.yaml y Dockge te permite editarlo, iniciarlo, detenerlo y ver sus logs desde el navegador.

Aunque está diseñado originalmente para Docker, Dockge funciona perfectamente con Podman gracias a que Podman expone un socket compatible con la API de Docker.

Instalación de Dockge sobre Podman

Dockge se despliega a sí mismo desde un compose.yaml. En lugar de Docker Compose, usaremos podman-compose o docker-compose apuntando al socket de Podman.

Opción A: Usando podman-compose

# Instalar podman-compose
pip install --user podman-compose

# Crear directorio para Dockge
mkdir -p ~/dockge
cd ~/dockge

compose.yaml para Dockge:

# ~/dockge/compose.yaml
version: "3.8"

services:
  dockge:
    image: docker.io/louislam/dockge:1
    container_name: dockge
    restart: unless-stopped
    ports:
      - "5001:5001"
    environment:
      - DOCKGE_STACKS_DIR=/opt/dockge/stacks
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./data:/app/data:Z
      - ./stacks:/opt/dockge/stacks:Z
    networks:
      - dockge-net

networks:
  dockge-net:

⚠️ Adaptación para Podman: El socket en sistemas Podman rootless no está en /var/run/docker.sock. Hay que montar el socket de Podman:

volumes:
  - $XDG_RUNTIME_DIR/podman/podman.sock:/var/run/docker.sock:ro,z

Desplegar Dockge:

# Opción A: con docker-compose apuntando a Podman
DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock \
  docker compose up -d

# Opción B: con podman-compose
podman-compose up -d

Opción B: Usando Quadlet (recomendada para producción)

# ~/.config/containers/systemd/dockge.container
[Unit]
Description=Dockge container
After=network-online.target

[Container]
Image=docker.io/louislam/dockge:1
ContainerName=dockge
PublishPort=5001:5001
Volume=%h/dockge/data:/app/data:Z
Volume=%h/dockge/stacks:/opt/dockge/stacks:Z
Volume=%t/podman/podman.sock:/var/run/docker.sock:ro,z
Environment=DOCKGE_STACKS_DIR=/opt/dockge/stacks

[Service]
Restart=always

[Install]
WantedBy=default.target
systemctl --user daemon-reload
systemctl --user start dockge

Usar Dockge con Podman

Una vez instalado, accede a http://localhost:5001. La interfaz te permite:

  1. Crear stacks — Escribe o pega un compose.yaml directamente en el editor web.
  2. Desplegar — Dockge ejecuta docker compose up -d contra el socket de Podman.
  3. Gestionar — Iniciar, detener, reiniciar y eliminar stacks completos.
  4. Logs — Ver logs en tiempo real desde el navegador.
  5. Terminal — Acceder a una shell dentro de cualquier contenedor.

El flujo de creación de un stack desde la UI

Déjame contarte cómo es el flujo real, porque Dockge tiene una particularidad que al principio descoloca. En la pantalla principal ves una lista de stacks y un botón para crear uno nuevo, pero no es como en Portainer, donde escribes el compose en un formulario y listo. Dockge, fiel a su filosofía de cada stack es un directorio, primero te pide un nombre y crea la carpeta, y después te abre el editor con el compose.yaml dentro.

El proceso paso a paso es este:

  1. Pulsas Create Stack y le das un nombre, por ejemplo mi-app. Dockge crea el directorio ~/dockge/stacks/mi-app/ con un compose.yaml vacío.
  2. Se abre el editor web con el archivo. Escribes o pegas tu compose, y Dockge te va marcando los errores de sintaxis YAML en tiempo real, que es un detalle que se agradece cuando llevas un rato peleándote con la indentación.
  3. Pulsas Deploy y Dockge ejecuta el equivalente a docker compose up -d contra el socket de Podman. En la pestaña de logs ves en directo cómo se descargan las imágenes y arrancan los contenedores.
  4. A partir de ahí, cada vez que edites el compose y pulses Deploy de nuevo, Dockge aplica los cambios. Si solo cambias una variable de entorno, solo se recrea el contenedor afectado, no todo el stack.

Lo que más me gusta es que cada stack tiene su propia pestaña con sus logs y su terminal, así que no tienes que andar saltando entre contenedores. Y como todo queda en directorios, si un día Dockge desaparece, tus stacks siguen ahí, intactos, listos para levantarlos a mano con podman-compose up -d. Nada de datos atrapados en una base de datos propietaria.

Ejemplo de stack desplegado desde Dockge:

# Stack: "mi-app" en Dockge
services:
  nginx:
    image: docker.io/nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:Z

  api:
    image: docker.io/node:20-alpine
    command: node server.js
    working_dir: /app
    volumes:
      - ./api:/app:Z
    environment:
      - NODE_ENV=production

Cuando despliegues este stack desde Dockge, Podman ejecutará los contenedores usando el backend de red por defecto (pasta en Podman 5.0+ o slirp4netns en versiones anteriores). Si necesitas redes personalizadas, puedes definirlas en el compose. Un detalle que me costó pillar: si defines una red en el compose y quieres que sea la misma que ya creaste con podman network create, tienes que marcarla como external: true, porque si no, Podman intentará crearla y te dará un error de que ya existe.

Limitaciones de Dockge con Podman

  • Redes bridge personalizadas: Los contenedores en un stack de Dockge usan la red por defecto del compose. Para redes avanzadas, debes crearlas antes con podman network create y referenciarlas como external: true en el compose.
  • Labels de Traefik: Dockge no muestra las labels directamente en la UI. Debes escribirlas manualmente en el compose.yaml.
  • Rendimiento: Dockge ejecuta docker compose sobre el socket, lo que añade una ligera latencia. Para entornos de alto rendimiento, Quadlet directo es preferible.
  • Contenedores rootless: Dockge funciona en modo rootless, pero algunas imágenes que requieren capacidades privilegiadas pueden fallar.

Alternativa: Portainer con Podman

Si Dockge se queda corto (por ejemplo, si necesitas gestionar múltiples hosts), Portainer también funciona con Podman:

podman volume create portainer-data

podman run -d \
  --name portainer \
  -p 9443:9443 \
  -v portainer-data:/data:Z \
  -v $XDG_RUNTIME_DIR/podman/podman.sock:/var/run/docker.sock:ro,z \
  docker.io/portainer/portainer-ce:latest

Portainer ofrece una interfaz más completa (gestión de imágenes, volúmenes, redes, usuarios) pero es más pesado que Dockge.

Dockge vs Portainer, ¿cuál te conviene?

Llevo tiempo usando ambos y te puedo decir que no compiten exactamente por lo mismo, así que la elección depende más de tu forma de trabajar que de las features. Te lo resumo con la comparación que a mí me sirve:

CaracterísticaDockgePortainer
FilosofíaStacks como directoriosGestión global de contenedores
Editor composeSí, integrado y con validaciónSí, en el formulario de stacks
Múltiples hostsNo (un solo socket)Sí (agentes y endpoints)
Gestión de imágenesNo (solo vía compose)Sí (pull, push, etiquetado)
Gestión de volúmenesIndirecta (vía compose)Sí, dedicada
Usuarios y rolesNoSí, con RBAC
Peso y recursosMuy ligeroMás pesado
Curva de aprendizajeBajaMedia

Dockge es, ante todo, un gestor de stacks. Si tu homelab se organiza en proyectos compose y lo que quieres es editar, desplegar y ver logs de cada uno sin salir del navegador, Dockge es una gozada por su simplicidad. No te distrae con decenas de paneles; te pone el compose delante y ya.

Portainer, en cambio, es un gestor de contenedores en sentido amplio. Si necesitas administrar varios hosts desde una sola interfaz, gestionar imágenes de forma visual, crear usuarios con permisos distintos o inspeccionar redes y volúmenes con detalle, Portainer te lo da todo. El precio es que es más pesado y que su modelo mental es más de panel de control que de editor de stacks.

En mi caso particular, para un homelab de una sola máquina con Podman rootless, me quedo con Dockge por esa combinación de ligereza y foco en el compose. Pero si algún día tengo que gestionar tres servidores a la vez, no me lo pienso dos veces y monto Portainer. Son herramientas que se complementan más de lo que compiten, y nada te impide tener Dockge para los stacks y Portainer para la administración global si te sobra memoria.

💡 Tip: Si montas Portainer sobre Podman rootless, recuerda que el socket de Podman es por usuario. Portainer verá los contenedores de ese usuario, no los de todo el sistema. Para gestionar contenedores rootful tendrías que apuntar al socket del sistema, con las implicaciones de seguridad que eso conlleva.


Podman Desktop

Podman Desktop es la GUI oficial del proyecto Podman, desarrollada por Red Hat y la comunidad. Es multiplataforma (Linux, macOS, Windows) y ofrece una experiencia similar a Docker Desktop pero 100% open source y sin demonio centralizado.

Instalación

# Fedora
sudo dnf install podman-desktop

# Ubuntu/Debian
# Descargar .deb desde https://podman-desktop.io/downloads
sudo dpkg -i podman-desktop-*.deb

# Arch Linux
sudo pacman -S podman-desktop

# También disponible como Flatpak
flatpak install flathub io.podman_desktop.PodmanDesktop

Funcionalidades principales

Gestión de contenedores:

  • Listar, iniciar, detener, reiniciar y eliminar contenedores.
  • Ver logs en tiempo real con coloreado.
  • Acceder a terminal integrada.
  • Inspeccionar variables de entorno, montajes, redes.
  • Gráfico de recursos (CPU, memoria, red).

Gestión de imágenes:

  • Buscar y descargar imágenes desde registros (Docker Hub, Quay, GHCR).
  • Construir imágenes desde Dockerfile con feedback visual.
  • Historial de capas y análisis de tamaño.

Gestión de redes y volúmenes:

  • Crear y eliminar redes personalizadas.
  • Conectar contenedores a redes desde la UI.
  • Gestionar volúmenes (crear, inspeccionar, limpiar).

Pods:

  • Crear pods con múltiples contenedores.
  • Añadir/eliminar contenedores de un pod existente.
  • Ver topología del pod.

Kubernetes:

  • Convertir contenedores/pods en recursos YAML de Kubernetes.
  • Desplegar directamente en clusters locales (Kind, Minikube).
  • Integración con OpenShift Local (CRC).

Extensiones:

  • Catálogo de extensiones: Podman AI Lab, Kind, Minikube, LLM Studio, etc.
  • Las extensiones permiten añadir funcionalidades como gestión de modelos de IA, clusters Kubernetes, etc.

Compose:

  • Soporte para docker-compose.yaml (arrastra y suelta o selecciona archivos).
  • Visualización de stacks completos.

Podman Desktop como sustituto de Docker Desktop

CaracterísticaDocker DesktopPodman Desktop
Demoniodockerd (obligatorio)Sin demonio (socket opcional)
RootlessLimitadoNativo
PrecioLicencia comercial100% gratuito
KubernetesIncluidoIncluido
ExtensionesSí (marketplace)Sí (open source)
Consumo de recursosAltoBajo
Compatibilidad CLIdocker CLIPodman + alias docker

Para migrar desde Docker Desktop:

  1. Instala Podman y Podman Desktop.
  2. Crea el alias: alias docker=podman.
  3. Configura el socket de Podman como compatible con Docker.
  4. Tus compose.yaml existentes funcionan sin cambios (salvo features específicas de Docker).

Migración práctica de Docker a Podman, paso a paso

La teoría de la tabla está muy bien, pero la migración real tiene sus trampas y quiero contarte el flujo que a mí me funciona para no dejarme nada por el camino. No es que sea complicado, es que hay detalles que si no los conoces te hacen perder una tarde.

Paso 1: Verifica que tienes Podman y el socket activo. Antes de nada, confirma que Podman está instalado y que el socket compatible con Docker responde:

podman --version
systemctl --user enable --now podman.socket
curl -s --unix-socket $XDG_RUNTIME_DIR/podman/podman.sock http://localhost/_ping

Si el _ping devuelve OK, el socket está listo y cualquier herramienta que hable con la API de Docker (Docker Compose, Portainer, Dockge, etc.) funcionará contra Podman.

Paso 2: Crea el alias de CLI. Para que tus hábitos de docker sigan funcionando sin cambiar ni una tecla, añade el alias a tu shell:

# En ~/.bashrc o ~/.zshrc
alias docker=podman

A partir de ahí, docker ps, docker build, docker compose (si tienes el plugin) apuntan a Podman. Eso sí, no te confíes del todo: hay comandos de Docker que Podman no implementa igual, así que si algo se comporta raro, prueba con podman <comando> directamente.

Paso 3: Migra tus volúmenes. Aquí está la trampa que más me ha hecho sudar. Los volúmenes de Docker y los de Podman no se comparten, porque cada uno gestiona sus datos en rutas distintas (/var/lib/docker/volumes frente a ~/.local/share/containers/storage/volumes). Si tienes datos importantes en un volumen de Docker, tienes que exportarlos y reimportarlos. Lo más limpio es usar un contenedor temporal como puente:

# Exportar el volumen de Docker a un tar
docker run --rm -v midato:/data -v $(pwd):/backup alpine \
  tar czf /backup/midato.tar.gz -C /data .

# Importar el tar en un volumen de Podman
podman volume create midato
podman run --rm -v midato:/data -v $(pwd):/backup alpine \
  tar xzf /backup/midato.tar.gz -C /data

Paso 4: Recrea los contenedores con Podman. Con los datos ya en volúmenes de Podman, levanta los servicios. Si usabas compose, el mismo archivo suele valer, pero presta atención a los volúmenes con nombre y a los puertos:

# Antes con Docker
docker compose up -d

# Ahora con Podman (apuntando al socket de Podman)
DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock docker compose up -d
# o directamente
podman-compose up -d

Paso 5: Verifica en Podman Desktop. Abre Podman Desktop y comprueba que todos los contenedores aparecen en la pestaña Containers con su estado correcto. Es el momento de revisar que los puertos publicados responden y que los volúmenes montan bien. Si algo no arranca, los logs en tiempo real de la GUI te van a decir por qué mucho más rápido que peleándote con la terminal.

Paso 6: Desactiva Docker. Cuando estés seguro de que todo funciona, puedes parar el demonio de Docker para liberar recursos:

sudo systemctl stop docker
sudo systemctl disable docker

💡 Truco: No borres los volúmenes de Docker hasta pasadas un par de semanas. La migración suele ir bien, pero tener la red de seguridad de poder volver atrás sin drama te quita un peso de encima. Yo lo dejé todo intacto un mes entero antes de limpiar.

Escenario integrador: Homelab completo

Para cerrar el capítulo, combinemos todo lo aprendido en un homelab real con Traefik, Dockge y Podman Desktop trabajando juntos.

Arquitectura:

Internet → [Puertos 80/443] → Traefik (proxy inverso)
                                   ├── blog.tudominio.com → WordPress
                                   ├── dockge.tudominio.com → Dockge
                                   └── grafana.tudominio.com → Grafana
                                            └── Todos con TLS automático

Paso 1: Red compartida

podman network create proxy-net

Paso 2: Traefik

# ~/traefik/compose.yaml
services:
  traefik:
    image: docker.io/traefik:v3.1
    container_name: traefik
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    networks:
      - proxy-net
    volumes:
      - ./traefik.yml:/etc/traefik/traefik.yml:ro,Z
      - ./dynamic.yml:/etc/traefik/dynamic.yml:ro,Z
      - ./data:/etc/traefik:Z
      - $XDG_RUNTIME_DIR/podman/podman.sock:/var/run/docker.sock:ro,z
    labels:
      - "traefik.enable=true"

networks:
  proxy-net:
    external: true

Paso 3: Dockge

# ~/dockge/compose.yaml
services:
  dockge:
    image: docker.io/louislam/dockge:1
    container_name: dockge
    restart: unless-stopped
    networks:
      - proxy-net
    environment:
      - DOCKGE_STACKS_DIR=/opt/dockge/stacks
    volumes:
      - ./data:/app/data:Z
      - ./stacks:/opt/dockge/stacks:Z
      - $XDG_RUNTIME_DIR/podman/podman.sock:/var/run/docker.sock:ro,z
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.dockge.rule=Host(`dockge.tudominio.com`)"
      - "traefik.http.routers.dockge.entrypoints=websecure"
      - "traefik.http.routers.dockge.tls=true"
      - "traefik.http.routers.dockge.tls.certresolver=letsencrypt"
      - "traefik.http.services.dockge.loadbalancer.server.port=5001"

networks:
  proxy-net:
    external: true

Paso 4: Despliegue

# Iniciar Traefik
cd ~/traefik && podman-compose up -d

# Iniciar Dockge
cd ~/dockge && podman-compose up -d

# Desde Dockge, añadir stacks adicionales (WordPress, Grafana, etc.)

Paso 5: Grafana

Para que el homelab tenga un panel donde mirar las métricas, añadimos Grafana detrás de Traefik. El compose es sencillo, pero fíjate en dos detalles: la red compartida proxy-net y las labels que le dicen a Traefik cómo enrutar:

# ~/grafana/compose.yaml
services:
  grafana:
    image: docker.io/grafana/grafana:latest
    container_name: grafana
    restart: unless-stopped
    networks:
      - proxy-net
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
      - GF_SERVER_ROOT_URL=https://grafana.tudominio.com
    volumes:
      - grafana-data:/var/lib/grafana:Z
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.grafana.rule=Host(`grafana.tudominio.com`)"
      - "traefik.http.routers.grafana.entrypoints=websecure"
      - "traefik.http.routers.grafana.tls=true"
      - "traefik.http.routers.grafana.tls.certresolver=letsencrypt"
      - "traefik.http.services.grafana.loadbalancer.server.port=3000"

volumes:
  grafana-data:

networks:
  proxy-net:
    external: true

El GF_SERVER_ROOT_URL es importante: si no le dices a Grafana cuál es su URL pública, te va a generar enlaces internos con el puerto y la IP del contenedor, y las redirecciones se romperán. Es uno de esos detalles que te hacen perder una hora hasta que caes.

Paso 6: Gestión visual

Abre Podman Desktop para monitorear el estado de todos los contenedores, ver logs, y gestionar la infraestructura desde una interfaz gráfica. Puedes observar cómo Traefik descubre automáticamente cada nuevo contenedor y genera los certificados TLS sin intervención manual.

Resolución de problemas en el homelab

Llegados a este punto, te voy a dejar un pequeño arsenal de comandos para cuando algo no funcione, porque en un homelab con Traefik, Dockge y Podman Desktop las cosas fallan, y fallan de formas que no son obvias. Estos son los tres escenarios que más veces me han salvado el día.

El certificado no se emite. Si entras en blog.tudominio.com y te da error de certificado o te redirige a un aviso de conexión no segura, lo primero es comprobar que Traefik ve el contenedor y que el challenge HTTP-01 puede llegar. Revisa los logs de Traefik:

podman logs traefik 2>&1 | grep -i acme

Si ves errores de challenge o de no route to host, casi seguro es que el contenedor no está en la misma red que Traefik o que la regla Host() no coincide con el dominio real. Comprueba la red con podman inspect y la regla con podman inspect <contenedor> --format '{{json .Config.Labels}}' | jq.

Un contenedor no arranca. Cuando un stack de Dockge no levanta, el error suele estar en el compose o en los volúmenes. Desde la terminal, el diagnóstico más directo es:

podman ps -a
podman logs <contenedor>
podman inspect <contenedor> --format '{{.State.Status}} {{.State.Error}}'

El State.Error te dice exactamente qué ha fallado, desde un puerto ocupado hasta un volumen con permisos de SELinux mal puestos. Y hablando de SELinux: si ves errores de permission denied al montar volúmenes, la solución casi siempre es añadir :Z o :z al montaje, como ya vimos.

No resuelves los nombres de los contenedores. Si un contenedor no encuentra a otro por nombre, el problema está en Aardvark-dns o en la red. Verifica que ambos están en la misma red y que el DNS interno responde:

podman network inspect proxy-net | jq '.[0].containers'
podman exec <contenedor> getent hosts <otro-contenedor>

Si getent hosts no devuelve nada, comprueba que la red es la misma y que no has mezclado redes por error. Es el fallo más tonto y el más común: dos contenedores en redes distintas que se miran sin encontrarse.

Conclusión

En este capítulo hemos explorado las capacidades de red avanzadas de Podman y las herramientas que conforman su ecosistema:

  • Modos de red: Cada modo (bridge, host, none, macvlan, ipvlan, slirp4netns, pasta) responde a un caso de uso específico. Pasta es el nuevo predeterminado rootless por su rendimiento superior.
  • Netavark + Aardvark-dns: El stack moderno que reemplaza a CNI, con soporte de DNS interno y configuración declarativa de redes.
  • Traefik rootless: Proxy inverso con descubrimiento automático, TLS vía Let’s Encrypt, HTTP/3 y un potente sistema de middlewares — todo funcionando sin privilegios de root gracias a socket activation o puertos no privilegiados.
  • Dockge: Interfaz web que aprovecha el socket compatible de Podman para gestionar stacks docker-compose con una experiencia limpia y minimalista.
  • Podman Desktop: La GUI nativa que combina gestión de contenedores, imágenes, pods, redes, volúmenes, Kubernetes y extensiones en una sola aplicación.

Con estas herramientas, Podman deja de ser una alternativa a Docker y se convierte en una plataforma completa para entornos de producción, desarrollo y homelab, con la ventaja adicional de ser rootless, open source y sin demonio centralizado.

Más información

  • Documentación oficial de Podman: https://docs.podman.io/
  • Netavark: https://github.com/containers/netavark
  • Aardvark-dns: https://github.com/containers/aardvark-dns
  • Traefik + Podman socket activation: https://github.com/eriksjolund/podman-traefik-socket-activation
  • Dockge: https://github.com/louislam/dockge
  • Podman Desktop: https://podman-desktop.io/
  • Tutorial de redes rootless (español): https://www.josedomingo.org/pledin/2024/05/redes-rootless-podman/
  • Guía de Traefik v3: https://doc.traefik.io/traefik/

Artículos relacionados

Deja una respuesta