7

Pods en Podman

Vistas: 1
Tutorial: Tutorial de Podman
Pods en Podman

Cuando empecé con Podman, una de las cosas que más me chocó fue tener que olvidarme de Docker Compose. Durante años había usado docker-compose up -d sin pensarlo dos veces, y de repente me encontraba con una herramienta que me pedía repensar cómo orquestaba mis servicios. La cuestión es que, después de darle varias vueltas, descubrí que los pods no solo resuelven el mismo problema, sino que lo hacen de una forma más elegante. Si vienes de Docker, probablemente has usado Docker Compose para orquestar multicontenedores. Pero Podman tiene una propuesta diferente y, para muchos casos, más elegante: los pods. Y lo mejor es que los pods no son un invento de Podman, sino que heredan directamente el concepto de Kubernetes.

En este capítulo vas a descubrir qué son los pods, cómo se diferencian de los contenedores individuales, cómo crearlos, iniciarlos, detenerlos y eliminarlos, cómo comparten recursos entre sí, y cómo migrar desde Docker Compose a pods. Al finalizar, serás capaz de levantar un stack completo —web, base de datos y Redis— dentro de un mismo pod, usando únicamente Podman.

Qué es un pod

El término pod viene directamente de Kubernetes. En Kubernetes, un pod es la unidad más pequeña y básica de despliegue. Representa un grupo de uno o más contenedores que comparten el mismo contexto de ejecución: misma dirección IP, mismo espacio de nombres de red, mismos puntos de montaje y, opcionalmente, otros espacios de nombres como PID o IPC.

Podman adopta este mismo concepto. Cuando creas un pod en Podman, estás creando un «contenedor de infraestructura» —un contenedor especial, normalmente llamado pause container o contenedor de pausa— que mantiene vivos los espacios de nombres compartidos. Luego, dentro de ese pod, puedes añadir todos los contenedores que quieras, y todos ellos compartirán esos espacios de nombres.

Para entenderlo mejor, imagina una casa. Los contenedores individuales serían como casas independientes: cada una tiene su propia dirección, su propia puerta, sus propias instalaciones. Un pod, en cambio, sería como un edificio de apartamentos: todos los apartamentos comparten la misma dirección (la IP del pod), el mismo portal (el espacio de nombres de red) y pueden verse entre sí por los pasillos comunes (localhost).

Contenedor de infraestructura (pause)

Cuando ejecutas podman pod create, Podman crea automáticamente un contenedor especial llamado contenedor de infraestructura o infra container. Este contenedor ejecuta una imagen mínima (normalmente k8s.gcr.io/pause o similar) cuyo único propósito es mantener abiertos los espacios de nombres del pod. Mientras este contenedor de infraestructura esté vivo, el pod existe. Si lo detienes o eliminas, el pod se deshace.

Te puedo asegurar que la primera vez que ves ese contenedor con podman ps -a te entra la duda. Un contenedor de unos pocos kilobytes con un nombre extraño tipo mi-pod-infra. No te alarmes: es normal y esperado. Es el pegamento que mantiene unido el pod.

Diferencia fundamental entre contenedor individual y pod

En un contenedor individual, el contenedor es la unidad. Tiene su propia IP, su propio stack de red, su propio espacio de procesos. Para que dos contenedores se comuniquen, necesitan una red —como un puente o --link— o exponer puertos.

En un pod, el pod es la unidad. Los contenedores dentro del pod se comunican entre sí a través de localhost, sin necesidad de exponer puertos ni configurar redes. Comparten la misma IP de cara al exterior y el mismo rango de puertos. Y esto es una diferencia fundamental que, cuando la asimilas, cambia completamente cómo diseñas tus despliegues.

Ciclo de vida de un pod

Al igual que los contenedores individuales, los pods tienen su propio ciclo de vida: se crean, se inician, se detienen, se reinician y se eliminan. Los comandos siguen una sintaxis muy similar a la de podman para contenedores. Por suerte, no tienes que aprenderlo todo desde cero.

Crear un pod

El comando básico para crear un pod es:

podman pod create --name mi-pod

Esto crea un pod llamado mi-pod con los valores por defecto. Por defecto, los contenedores dentro de un pod comparten los espacios de nombres net (red), uts (nombre de host) e ipc (comunicación entre procesos).

Puedes personalizar la creación con varias opciones:

podman pod create \
    --name web-stack \
    --hostname webapp \
    --label app=mi-app \
    --label env=produccion \
    --publish 8080:80 \
    --publish 5432:5432 \
    --share net,pid,ipc,uts

Opciones más útiles:

  • --name: nombre del pod
  • --hostname: nombre de host que compartirán todos los contenedores del pod
  • --label: metadatos en formato clave=valor
  • --publish / -p: publica puertos del pod hacia el host. Al compartir red, basta con publicar el puerto una vez a nivel de pod
  • --share: lista de espacios de nombres a compartir (net, pid, ipc, uts, cgroup)
  • --network: red a la que conectar el pod
  • --volume / -v: montar volúmenes en el pod
  • --infra-image: imagen de infraestructura personalizada

Ver pods

Para ver los pods que tienes creados:

podman pod list

O su alias:

podman pod ps

La salida muestra algo como:

POD ID        NAME        STATUS    CREATED         INFRA ID       # OF CONTAINERS
a1b2c3d4e5    mi-pod      Running   2 minutes ago   5f6g7h8i9j     1

La columna # OF CONTAINERS indica cuántos contenedores de usuario tiene el pod. El contenedor de infraestructura no cuenta.

Iniciar, detener, reiniciar y eliminar

podman pod start mi-pod
podman pod stop mi-pod
podman pod restart mi-pod
podman pod rm mi-pod        # Solo si está detenido
podman pod rm --force mi-pod  # Lo elimina aunque esté corriendo
podman pod prune            # Elimina todos los pods detenidos

Compartir espacios de nombres en un pod

La magia de los pods reside en los espacios de nombres (namespaces) de Linux. Un espacio de nombres es una característica del kernel de Linux que aísla y virtualiza los recursos del sistema. Cuando creas un pod y le dices qué espacios de nombres compartir, todos los contenedores dentro de ese pod verán los mismos recursos.

Tipos de espacios de nombres

El parámetro --share acepta una lista separada por comas. Los valores posibles son:

  • net (red): Todos los contenedores comparten la misma pila de red. Tienen la misma dirección IP, la misma tabla de rutas, los mismos puertos abiertos. Pueden comunicarse entre sí a través de localhost sin necesidad de exponer puertos. Es el espacio de nombres que se comparte por defecto.
  • pid (procesos): Todos los contenedores pueden verse los procesos entre sí. Con pid compartido, desde un contenedor puedes hacer ps aux y ver los procesos de los otros contenedores del pod. Esto es útil para depuración y monitorización, pero reduce el aislamiento.
  • ipc (comunicación entre procesos): Permite que los procesos de diferentes contenedores se comuniquen mediante mecanismos IPC de Linux —como memoria compartida (shared memory o shm) o semáforos—. Es necesario para aplicaciones que usan IPC del Sistema V o POSIX.
  • uts (nombre de host): Todos los contenedores comparten el mismo nombre de host (hostname) y dominio. Por defecto se comparte junto con net e ipc.
  • cgroup: Comparte el grupo de control. Se usa en escenarios avanzados de monitorización de recursos.

Por defecto, Podman comparte net, ipc y uts. Para compartir también pid, tienes que indicarlo explícitamente:

podman pod create --name mon-pod --share net,pid,ipc,uts

Espacio de nombres de red compartido

El espacio de nombres de red compartido es, con diferencia, el más útil. Cuando los contenedores comparten la red:

  1. Se comunican por localhost: El contenedor web puede conectar a localhost:5432 para llegar a la base de datos, y a localhost:6379 para llegar a Redis. No hace falta configurar redes bridge, ni usar --link, ni conocer IPs.
  2. Los puertos se publican a nivel de pod: Publicas los puertos una vez al crear el pod, y todos los contenedores internos pueden usarlos. Si tu pod tiene un servidor web en el puerto 80 y lo publicas como --publish 8080:80, cualquier contenedor dentro del pod que escuche en el puerto 80 será accesible desde el host en el puerto 8080.
  3. No hay conflictos de puertos entre pods diferentes: Cada pod tiene su propia IP, así que aunque dos pods tengan un contenedor escuchando en el puerto 80, no hay conflicto hasta que publicas los puertos hacia el host.

Espacio de nombres PID compartido

Cuando compartes el espacio de nombres PID, los contenedores pueden verse los procesos entre sí. Esto es útil para:

  • Depurar: desde un contenedor puedes ejecutar strace o gdb sobre procesos de otro contenedor
  • Monitorizar: herramientas como htop ven todos los procesos del pod
  • Gestión de señales: puedes enviar señales (SIGTERM, SIGKILL, etc.) a procesos de otros contenedores

La contrapartida es que reduces el aislamiento entre contenedores. Y esto es algo a considerar seriamente en entornos de producción. En mi caso particular, solo comparto PID cuando estoy depurando un problema concreto. En producción, prefiero mantener el aislamiento.

# Crear un pod compartiendo PID
podman pod create --name debug-pod --share net,pid,ipc,uts

# Añadir un contenedor que ejecuta un servicio
podman run -d --pod debug-pod --name servicio alpine sleep 9999

# Desde otro contenedor, ver los procesos del servicio
podman run -it --pod debug-pod --name debugger alpine ps aux

Verás el proceso sleep 9999 del otro contenedor. Sin --share pid, cada contenedor vería solo sus propios procesos.

Espacio de nombres IPC y UTS

El espacio de nombres IPC permite el uso de mecanismos de comunicación entre procesos del Sistema V —semáforos, memoria compartida, colas de mensajes—. Es esencial para aplicaciones como bases de datos o servidores de aplicaciones que usan estos mecanismos internamente. PostgreSQL, por ejemplo, usa memoria compartida para su caché de buffers. Si ejecutas PostgreSQL en un contenedor dentro de un pod con IPC compartido, esa memoria compartida está disponible para otros contenedores del pod.

Compartir UTS significa que todos los contenedores ven el mismo nombre de host. Esto es más útil de lo que parece: muchas aplicaciones usan el nombre de host para identificarse o generar identificadores. Si todos los contenedores del pod ven el mismo hostname, se comportan de manera consistente.

Añadir contenedores a un pod

Una vez creado el pod, añades contenedores con podman run usando la opción --pod:

# Crear el pod
podman pod create --name mi-pod --publish 8080:80

# Añadir contenedor web
podman run -d --pod mi-pod --name web nginx:alpine

# Añadir contenedor de base de datos
podman run -d --pod mi-pod --name db postgres:16-alpine

# Añadir contenedor de caché
podman run -d --pod mi-pod --name cache redis:7-alpine

Los tres contenedores comparten la misma IP y red. El contenedor web puede acceder a PostgreSQL en localhost:5432 y a Redis en localhost:6379.

Importante: Cuando usas --pod, la opción --publish del podman run no tiene efecto porque la red se gestiona a nivel de pod. Los puertos se publican a nivel de pod, no a nivel de contenedor individual. Si necesitas publicar puertos adicionales después de crear el pod, tendrás que recrearlo. Y créeme, descubrir esto por las malas es una faena.

Ejemplo práctico: Stack web + PostgreSQL + Redis en un pod

Vamos a construir un ejemplo completo de una aplicación web típica: un frontend con Nginx, una aplicación backend (Python/Flask), una base de datos PostgreSQL y una caché Redis, todo dentro de un mismo pod.

Paso 1: Crear el pod

podman pod create \
    --name app-stack \
    --hostname miapp \
    --label app=mi-aplicacion \
    --publish 8080:80 \
    --publish 5432:5432 \
    --publish 6379:6379 \
    --share net,ipc,uts

Aquí publicamos los puertos 80 (web), 5432 (PostgreSQL) y 6379 (Redis). Todos los contenedores dentro del pod compartirán red, IPC y UTS.

Paso 2: Añadir la base de datos PostgreSQL

podman run -d \
    --pod app-stack \
    --name db \
    --volume pgdata:/var/lib/postgresql/data \
    --env POSTGRES_DB=miapp \
    --env POSTGRES_USER=appuser \
    --env POSTGRES_PASSWORD=secreto123 \
    postgres:16-alpine

La base de datos estará disponible en localhost:5432 para el resto de contenedores del pod.

Paso 3: Añadir Redis

podman run -d \
    --pod app-stack \
    --name cache \
    --volume redisdata:/data \
    redis:7-alpine \
    redis-server --appendonly yes

Redis estará disponible en localhost:6379.

Paso 4: Añadir la aplicación backend

Para el backend, crearemos una pequeña aplicación Flask. Primero, necesitamos una imagen.

Crea un Dockerfile:

FROM python:3.12-alpine
WORKDIR /app
RUN pip install flask psycopg2-binary redis
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]

Construye la imagen:

podman build -t miapp-backend:latest .

El contenido de app.py podría ser algo como:

from flask import Flask
import psycopg2
import redis
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return "¡Hola desde Podman!"

@app.route('/db')
def check_db():
    try:
        conn = psycopg2.connect(
            host="localhost",
            port=5432,
            dbname="miapp",
            user="appuser",
            password="secreto123"
        )
        return "Conectado a PostgreSQL"
    except Exception as e:
        return f"Error: {e}"

@app.route('/cache')
def check_cache():
    try:
        r = redis.Redis(host='localhost', port=6379)
        r.set('test', 'ok')
        return f"Redis conectado. test = {r.get('test').decode()}"
    except Exception as e:
        return f"Error: {e}"

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000)

Ahora añade el contenedor backend al pod:

podman run -d \
    --pod app-stack \
    --name backend \
    miapp-backend:latest

El backend estará disponible en localhost:5000.

Paso 5: Añadir Nginx como proxy inverso

podman run -d \
    --pod app-stack \
    --name web \
    --volume ./nginx.conf:/etc/nginx/conf.d/default.conf:ro \
    nginx:alpine

El archivo nginx.conf redirige el tráfico del puerto 80 al backend:

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://localhost:5000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

Verificar el pod

podman pod list
podman ps --pod

Deberías ver un pod con cuatro contenedores, más el de infraestructura:

POD ID        NAME         STATUS    CREATED         INFRA ID       # OF CONTAINERS
x1y2z3w4      app-stack    Running   5 minutes ago   a1b2c3d4       4

Ahora puedes acceder a tu aplicación desde el host en:

  • Web/Nginx: http://localhost:8080
  • PostgreSQL (cliente externo): localhost:5432
  • Redis (cliente externo): localhost:6379

Y desde dentro del pod, todo se comunica por localhost. Sin configuraciones raras, sin redes bridge. Me la juego a que, si pruebas este ejemplo, todo funciona a la primera.

Migración de docker-compose.yml a pods

Si ya tienes un docker-compose.yml y quieres migrar a pods, tienes varias opciones. Yo he probado las tres, y cada una tiene su momento.

Opción 1: Usar podman-compose (la vía fácil)

podman-compose es una herramienta que interpreta archivos docker-compose.yml y los ejecuta con Podman. Esencialmente, un reemplazo directo:

# Instalar podman-compose
pip install podman-compose

# Ejecutar el stack
podman-compose -f docker-compose.yml up -d

Esto es útil como paso intermedio, pero no aprovecha los pods. podman-compose ejecuta los servicios como contenedores independientes, no dentro de un pod. Sin embargo, para una migración rápida, te saca del apuro.

Opción 2: Migrar manualmente a un pod (recomendada)

La migración manual te da control total y aprovecha las ventajas de los pods. Es la que uso en mis despliegues reales.

Paso 1: Identificar los servicios que forman parte de la misma aplicación.

Un docker-compose.yml típico podría tener servicios como web, api, db y redis. Si todos forman parte de la misma aplicación y se comunican entre sí, pueden ir en el mismo pod.

Paso 2: Traducir cada servicio a un contenedor dentro del pod.

Cada servicio se convierte en un podman run --pod <nombre>.

Paso 3: Mapear puertos.

En Docker Compose, los puertos se publican por servicio. En un pod, los puertos se publican a nivel de pod. La diferencia clave: dos servicios no pueden publicar el mismo puerto hacia el host —porque el pod tiene una sola IP—, pero sí pueden escuchar en el mismo puerto internamente.

Paso 4: Gestionar volúmenes.

Los volúmenes se montan igual que en Docker Compose, usando --volume en cada podman run o directamente a nivel de pod con podman pod create --volume.

Ejemplo de migración paso a paso

docker-compose.yml original:

version: '3.8'
services:
  web:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./html:/usr/share/nginx/html:ro
    depends_on:
      - api

  api:
    build: ./api
    ports:
      - "5000:5000"
    environment:
      DB_HOST: db
      REDIS_HOST: redis
    depends_on:
      - db
      - redis

  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: miapp
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: secreto123
    volumes:
      - pgdata:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redisdata:/data

volumes:
  pgdata:
  redisdata:

Equivalente en Podman con pods:

# 1. Crear el pod con los puertos publicados
podman pod create \
    --name stack \
    --publish 8080:80 \
    --publish 5000:5000 \
    --share net,ipc,uts

# 2. Base de datos (sin puertos expuestos al host, solo interna)
podman run -d \
    --pod stack \
    --name db \
    --volume pgdata:/var/lib/postgresql/data \
    --env POSTGRES_DB=miapp \
    --env POSTGRES_USER=appuser \
    --env POSTGRES_PASSWORD=secreto123 \
    postgres:16-alpine

# 3. Redis
podman run -d \
    --pod stack \
    --name redis \
    --volume redisdata:/data \
    redis:7-alpine \
    redis-server --appendonly yes

# 4. API (ya no necesita variables DB_HOST/REDIS_HOST, usa localhost)
# Primero construimos la imagen si es necesario
podman build -t miapp-api ./api

# Y la ejecutamos dentro del pod
podman run -d \
    --pod stack \
    --name api \
    --env DB_HOST=localhost \
    --env REDIS_HOST=localhost \
    miapp-api

# 5. Web (Nginx)
podman run -d \
    --pod stack \
    --name web \
    --volume ./html:/usr/share/nginx/html:ro \
    nginx:alpine

Fíjate en las diferencias clave:

  • Ya no hay depends_on. Los contenedores dentro del pod se inician todos juntos. Si tu aplicación necesita que la base de datos esté lista antes, debes implementar un mecanismo de reintento (retry) o verificación de salud (healthcheck) a nivel de aplicación. Es uno de los cambios que más cuesta asimilar cuando vienes de Docker Compose.
  • Las variables de entorno DB_HOST y REDIS_HOST ahora apuntan a localhost en lugar de al nombre del servicio.
  • Los puertos 5432 y 6379 no se publican al exterior a menos que los necesites explícitamente. Los contenedores internos acceden por localhost sin necesidad de exponerlos.

Opción 3: Usar podman kube play (la vía Kubernetes)

Esta es, probablemente, la opción más elegante. Puedes escribir un manifiesto YAML de Kubernetes directamente y ejecutarlo con Podman:

# stack.yaml
apiVersion: v1
kind: Pod
metadata:
  name: stack
  labels:
    app: mi-aplicacion
spec:
  containers:
    - name: web
      image: nginx:alpine
      ports:
        - containerPort: 80
      volumeMounts:
        - name: html
          mountPath: /usr/share/nginx/html
    - name: db
      image: postgres:16-alpine
      env:
        - name: POSTGRES_DB
          value: miapp
        - name: POSTGRES_USER
          value: appuser
        - name: POSTGRES_PASSWORD
          value: secreto123
      volumeMounts:
        - name: pgdata
          mountPath: /var/lib/postgresql/data
    - name: redis
      image: redis:7-alpine
      args:
        - redis-server
        - --appendonly
        - "yes"
      volumeMounts:
        - name: redisdata
          mountPath: /data
  volumes:
    - name: html
      hostPath:
        path: ./html
    - name: pgdata
      emptyDir: {}
    - name: redisdata
      emptyDir: {}

Y lo ejecutas con:

podman kube play stack.yaml

Este comando crea el pod y todos sus contenedores exactamente como están definidos en el YAML. Además, si más adelante quieres desplegarlo en Kubernetes, el mismo YAML funciona directamente.

Para generar un YAML de Kubernetes desde un pod en ejecución:

podman generate kube stack > stack-k8s.yaml

Esto produce un manifiesto listo para kubectl apply -f stack-k8s.yaml. Te adelanto que esta es mi opción favorita cuando sé que el stack terminará en un clúster Kubernetes.

Errores comunes y cómo evitarlos

Publicar puertos en contenedores individuales dentro de un pod

Cuando usas --pod, la opción --publish en podman run no tiene efecto y Podman ni siquiera te avisará. Te hablo por experiencia: la primera vez que lo hice, estuve veinte minutos mirando el log de Nginx preguntándome por qué no respondía en el puerto que había publicado. Publica los puertos únicamente al crear el pod con podman pod create --publish. Si necesitas añadir un puerto después, tendrás que recrear el pod. Y esto es una faena.

Confundir podman pod con los comandos de contenedores

Los comandos de pod tienen su propia sintaxis: es podman pod create, no podman create. Y podman pod ps, no podman ps pod. Recuerda que los pods tienen su propio subcomando: podman pod <acción>. Es fácil confundirse al principio, pero te acostumbras rápido.

Asumir que los contenedores se inician en orden

A diferencia de Docker Compose con depends_on, los contenedores dentro de un pod no tienen garantía de orden de inicio. Todos se inician simultáneamente. Implementa lógica de reintento en tu aplicación:

import time
import psycopg2

def esperar_base_datos(max_intentos=30, espera=1):
    for i in range(max_intentos):
        try:
            conn = psycopg2.connect(
                host="localhost",
                dbname="miapp",
                user="appuser",
                password="secreto123"
            )
            conn.close()
            return True
        except Exception:
            time.sleep(espera)
    raise Exception("No se pudo conectar a la base de datos")

Exponer puertos internos innecesariamente al host

Si publicas todos los puertos del pod (5432, 6379, etc.) al host, estás exponiendo servicios que quizás solo deberían ser internos. Redis y PostgreSQL no deberían ser accesibles desde fuera del pod en muchos escenarios. Publica solo los puertos estrictamente necesarios. Es sentido común, pero cuando uno viene de Docker Compose, donde todo se exponía, cuesta romper el hábito.

Los nombres de contenedor deben ser únicos

Aunque los contenedores estén en pods diferentes, los nombres de contenedor son globales en Podman. No puedes tener dos contenedores llamados db aunque estén en pods distintos. Usa nombres descriptivos que incluyan el contexto: app-stack-db, app-stack-redis, etc.

Perder datos al eliminar el pod sin gestionar volúmenes

Si montas volúmenes directamente en el contenedor sin usar volúmenes nombrados o bind mounts, los datos se pierden al eliminar el pod. Usa siempre volúmenes nombrados (--volume pgdata:/var/lib/postgresql/data) o bind mounts (--volume ./data:/data) para datos persistentes. Me ha pasado, y no es agradable tener que reconstruir una base de datos desde cero.

Verificación

Para confirmar que tus pods funcionan correctamente, ejecuta estas comprobaciones:

# 1. Listar todos los pods
podman pod ps

# 2. Ver detalles de un pod específico
podman pod inspect app-stack

# 3. Ver los contenedores dentro de un pod
podman ps --pod

# 4. Probar comunicación entre contenedores
# Desde un contenedor dentro del pod, accede a otro por localhost
podman exec backend curl -s http://localhost:5000/

# 5. Verificar que los puertos publicados responden
curl -s http://localhost:8080/

# 6. Probar la base de datos desde dentro del pod
podman exec backend python -c "
import psycopg2
conn = psycopg2.connect(host='localhost', dbname='miapp', user='appuser', password='secreto123')
cur = conn.cursor()
cur.execute('SELECT 1')
print('DB OK:', cur.fetchone())
"

# 7. Generar manifiesto Kubernetes desde el pod
podman generate kube app-stack > app-stack.yaml
echo "Manifiesto Kubernetes generado: app-stack.yaml"

# 8. Estadísticas del pod
podman pod stats app-stack

Si todos los comandos responden correctamente, tu pod está funcionando como debería. Y si todo funciona, la sensación es muy satisfactoria. Te lo aseguro.

Más información

Deja una respuesta