Llevas 44 lecciones construyendo imágenes con Docker, y hay algo que conviene decir con claridad: esas imágenes no son «de Docker». Son artefactos que cumplen un estándar abierto, y funcionan igual en Podman, en containerd, en CRI-O o en un nodo de Kubernetes sin cambiar un byte. Esta lección explica por qué, desmonta el ecosistema pieza a pieza y ejecuta ghcr.io/auroralibros/aurora-api:2.0.0 fuera de Docker para demostrarlo.

Contenido

  1. La pregunta correcta: ¿de quién es una imagen?
  2. El estándar OCI y sus tres especificaciones
  3. Qué significa esto en la práctica
  4. El mapa de piezas: runtimes de bajo y alto nivel
  5. CRI: por qué Kubernetes no habla con Docker
  6. Podman: la arquitectura sin daemon
  7. Rootless por defecto
  8. Compatibilidad de comandos y qué se rompe
  9. Pods, generate kube y play kube
  10. Compose, el socket compatible y Testcontainers
  11. Docker frente a Podman, y la migración de Aurora Libros
  12. containerd y nerdctl
  13. Buildah y Skopeo
  14. Aislamiento reforzado: gVisor y Kata Containers
  15. Tabla de decisión

  1. La pregunta correcta: ¿de quién es una imagen?

Cuando ejecutaste docker build -t aurora-api:2.0.0 ., Docker no inventó un formato propio. Produjo un artefacto con una estructura pública: un manifiesto JSON, una configuración con las variables de entorno, el ENTRYPOINT, el usuario y el historial, y una lista de capas comprimidas, todo identificado por digests SHA-256.

docker buildx imagetools inspect ghcr.io/auroralibros/aurora-api:2.0.0 --raw | jq '.'
# {
#   "mediaType": "application/vnd.oci.image.index.v1+json",
#   "manifests": [
#     { "platform": { "architecture": "amd64", "os": "linux" }, "digest": "sha256:6b1..." },
#     { "platform": { "architecture": "arm64", "os": "linux" }, "digest": "sha256:c47..." }
#   ]
# }

Fíjate en el mediaType: pone vnd.oci.image.index.v1+json, no «docker». Ese prefijo es la prueba literal de que tu imagen es un artefacto OCI, y de que la multiarquitectura que montaste en 05-05 es una función del estándar, no de Docker.

  1. El estándar OCI y sus tres especificaciones

La Open Container Initiative nació en 2015 dentro de la Linux Foundation, impulsada por la propia Docker, que donó su formato de imagen y su runtime runc. El motivo fue evitar una guerra de formatos incompatibles (había un competidor serio, rkt, con su propio formato). El resultado es que hoy el contenedor es un estándar y no un producto.

Especificación Qué normaliza Dónde la has visto en el curso
Image Spec Formato de la imagen: manifiesto, config, capas, digests, índices multiarquitectura Cada docker build y cada docker inspect
Runtime Spec Cómo se ejecuta un bundle: el config.json con namespaces, cgroups, capabilities y montajes El config.json de runc que abriste en 05-07
Distribution Spec El protocolo HTTP de los registros: push, pull, tags, digests, autenticación Cada docker push a ghcr.io (02-06)
graph TB
    subgraph oci["Estándar OCI"]
        IS["Image Spec<br/>formato de la imagen"]
        RS["Runtime Spec<br/>config.json y ejecución"]
        DS["Distribution Spec<br/>protocolo del registro"]
    end
    subgraph constructores["Construyen (Image Spec)"]
        BK["BuildKit"]; BAH["Buildah"]; KANIKO["Kaniko"]; PACK["Buildpacks"]
    end
    subgraph registros["Almacenan (Distribution Spec)"]
        GHCR["ghcr.io"]; HARBOR["Harbor"]; ZOT["Zot"]; REG["registry:2"]
    end
    subgraph ejecutores["Ejecutan (Runtime Spec)"]
        DOCKER["Docker Engine"]; PODMAN["Podman"]; CTD["containerd"]; CRIO["CRI-O"]
    end
    constructores --> IS --> registros
    registros --> DS
    IS --> ejecutores
    ejecutores --> RS

Cualquier constructor produce imágenes que cualquier registro almacena y cualquier runtime ejecuta. Esa cuadrícula completa es lo que compra el estándar.

  1. Qué significa esto en la práctica

Afirmación ¿Cierta?
«Necesito Docker para ejecutar una imagen de Docker» Falsa: la ejecuta cualquier runtime OCI
«Kubernetes ejecuta imágenes de Docker» Imprecisa: ejecuta imágenes OCI, y no usa Docker desde 2022
«Si cambio de runtime, tengo que reconstruir» Falsa: el mismo digest sirve
«Docker Hub solo sirve para Docker» Falsa: implementa la Distribution Spec
«Cosign y los SBOM son cosa de Docker» Falsa: son artefactos OCI en el registro

Consecuencia directa para Aurora Libros: ghcr.io/auroralibros/aurora-api:2.0.0 —104 MB, dos arquitecturas, firmada con Cosign, con SBOM y attestation de procedencia— corre exactamente igual en Docker, en Podman, en containerd o en cualquier nodo de Kubernetes, y su firma se verifica igual en todos. No has aprendido un producto: has aprendido un estándar y una de sus implementaciones.

  1. El mapa de piezas: runtimes de bajo y alto nivel

graph TB
    U["Tú: docker / podman / nerdctl / kubectl"]
    subgraph alto["Runtimes de ALTO nivel (gestores)"]
        DE["Docker Engine"]; PM["Podman"]; CD["containerd"]; CO["CRI-O"]
    end
    subgraph bajo["Runtimes de BAJO nivel (OCI Runtime Spec)"]
        RC["runc (C/Go)"]; CR["crun (C)"]; YK["youki (Rust)"]
        GV["gVisor"]; KT["Kata Containers"]
    end
    K["Kernel de Linux: namespaces, cgroups, seccomp, capabilities"]
    U --> alto
    DE --> CD
    alto --> bajo --> K
Capa Qué hace Piezas
Bajo nivel Recibe un bundle + config.json y crea el proceso aislado. Vive milisegundos runc, crun, youki, gVisor, Kata
Alto nivel Descarga imágenes, gestiona capas, redes, volúmenes y ciclo de vida; llama al de bajo nivel containerd, CRI-O, Podman, Docker Engine
Interfaz Lo que tecleas o la API que consume Kubernetes docker, podman, nerdctl, ctr, CRI
Runtime de bajo nivel Lenguaje Rasgo
runc Go La implementación de referencia; la que usas
crun C Más rápido y ligero; menos memoria. Por defecto en Podman en Fedora
youki Rust Seguridad de memoria por diseño; joven pero funcional
gVisor (runsc) Go Kernel en espacio de usuario: aislamiento fuerte, coste alto
Kata Go Una microVM por contenedor: aislamiento de VM

Cambiar el de bajo nivel es una línea de configuración, y las imágenes no se enteran:

// /etc/docker/daemon.json
{ "default-runtime": "runc",
  "runtimes": { "crun": { "path": "/usr/bin/crun" } } }
docker run --rm --runtime=crun alpine echo "ejecutado con crun"

  1. CRI: por qué Kubernetes no habla con Docker

Kubernetes necesita hablar con algún runtime en cada nodo. En vez de acoplarse a uno, definió la Container Runtime Interface, una API gRPC con dos servicios: RuntimeService (ciclo de vida de Pods y contenedores) e ImageService (imágenes).

Docker Engine nunca implementó CRI, así que existió un adaptador llamado dockershim dentro del propio kubelet. Era código de Kubernetes manteniendo compatibilidad con un producto concreto, y se retiró en la versión 1.24 (2022).

graph LR
    subgraph antes["Antes de 1.24"]
        KL1["kubelet"] --> DS["dockershim"] --> DE["Docker Engine"] --> CD1["containerd"] --> RC1["runc"]
    end
    subgraph ahora["Desde 1.24"]
        KL2["kubelet"] -->|CRI| CD2["containerd o CRI-O"] --> RC2["runc"]
    end

Aquello se comunicó fatal y generó titulares del tipo «Kubernetes deja de soportar Docker». Lo que realmente ocurrió fue que se eliminó un intermediario: containerd, que ya estaba ahí debajo, pasó a hablar directamente con el kubelet. Tus imágenes no se vieron afectadas en absoluto, porque son OCI. Es la mejor ilustración posible de la tesis de esta lección.

  1. Podman: la arquitectura sin daemon

Podman (Red Hat) es la alternativa más relevante a Docker, y su diferencia estructural es que no hay daemon.

graph TB
    subgraph docker["Docker"]
        DC["cliente docker"] -->|"socket, root"| DD["dockerd (root, siempre vivo)"]
        DD --> C1["contenedor"]
    end
    subgraph podman["Podman"]
        PC["podman (tu usuario)"] --> CN["conmon"] --> C2["contenedor"]
        SD["systemd --user"] -.->|"supervisa"| C2
    end
Consecuencia Docker Podman
Proceso siempre en marcha dockerd como root Ninguno
Dueño de los contenedores El daemon Tu usuario
Si el gestor muere Riesgo para los contenedores Siguen vivos: los supervisa conmon
Superficie de ataque Un socket = root en el host (05-03) No hay socket privilegiado
Arranque al inicio restart: gestionado por el daemon Unidades de systemd generadas
Auditoría del sistema Todo aparece como acción del daemon Aparece como acción de tu usuario

La cuarta fila es la razón de fondo. El «el grupo docker es equivalente a root» que aprendiste en 05-03 simplemente no existe en Podman: no hay socket privilegiado al que pertenecer. Y la última importa en entornos regulados: los registros de auditoría atribuyen las acciones a personas, no a un demonio.

  1. Rootless por defecto

Docker admite modo rootless desde la 20.10, pero hay que activarlo. En Podman es lo normal desde el principio, apoyado en user namespaces (05-07) y en los rangos de /etc/subuid y /etc/subgid.

podman run -d --name aurora-cache redis:7-alpine
podman top aurora-cache huser user
# HUSER       USER
# 100998      redis      ← UID 999 dentro; UID 100998 en el host
id -u
# 1000                   ← y el proceso pertenece a tu usuario

El contenedor cree ser el usuario redis (UID 999) y el kernel lo ve como el 100998, un UID sin ningún privilegio. Una fuga del contenedor no da root: da un usuario que no puede hacer nada.

Limitación del modo rootless Solución
No se pueden publicar puertos < 1024 sysctl net.ipv4.ip_unprivileged_port_start=80 o proxy delante
El rendimiento de red pasa por slirp4netns/pasta pasta (por defecto en versiones recientes) lo mejora mucho
Algunos sistemas de ficheros no funcionan igual Usar fuse-overlayfs
Montar dispositivos o cambiar sysctl del host Requiere privilegios: replantear el diseño

  1. Compatibilidad de comandos y qué se rompe

Podman replica la CLI de Docker de forma deliberada:

alias docker=podman
docker run -d -p 8080:8080 ghcr.io/auroralibros/aurora-api:2.0.0
docker ps ; docker logs ; docker exec ; docker build ; docker inspect   # todo igual

Funciona para la inmensa mayoría del trabajo diario. Lo que no es idéntico:

Diferencia Detalle
docker.io implícito Podman pregunta el registro o usa registries.conf; escribe siempre el nombre completo
Almacén de imágenes Separado por usuario en ~/.local/share/containers; lo que construyas como root no lo ve tu usuario
--privileged Menos privilegios reales en rootless: sigues limitado por tu usuario
Redes netavark en lugar del bridge de Docker; el DNS interno funciona igual pero se configura distinto
restart: always No hay daemon que reinicie: se genera una unidad de systemd
Swarm No existe; el camino es Kubernetes
Docker Desktop El equivalente es Podman Desktop

  1. Pods, generate kube y play kube

Podman toma prestada de Kubernetes su unidad de agrupación: el pod, un conjunto de contenedores que comparten namespace de red (y por tanto localhost y los puertos).

podman pod create --name aurora --publish 8080:8080
podman run -d --pod aurora --name aurora-cache redis:7-alpine
podman run -d --pod aurora --name aurora-api \
  -e REDIS_URL=redis://localhost:6379 \
  ghcr.io/auroralibros/aurora-api:2.0.0
podman pod ps
# POD ID   NAME     STATUS    INFRA ID   # OF CONTAINERS
# 4f2a...  aurora   Running   9c1b...    3

Fíjate en REDIS_URL=redis://localhost:6379: dentro de un pod, los contenedores comparten la interfaz de red, así que se alcanzan por localhost. Es exactamente el modelo de un Pod de Kubernetes, ensayado en tu portátil.

Y de ahí sale el puente más útil de Podman:

podman generate kube aurora > aurora-pod.yaml   # de contenedores a manifiesto K8s
podman play kube aurora-pod.yaml                # y de manifiesto K8s a contenedores
kubectl apply -f aurora-pod.yaml                # el mismo fichero, en el clúster

generate kube produce un Pod (o Deployment con --type deployment) listo para revisar. No sustituye al trabajo del módulo 6 —no genera Ingress, HPA, PDB ni sondas afinadas— pero como borrador es mucho mejor que kompose, porque parte de algo que ya funciona.

  1. Compose, el socket compatible y Testcontainers

Dos caminos para ejecutar tu compose.yaml:

# Opción A: podman-compose (implementación en Python, cobertura parcial)
podman-compose -f compose.yaml up -d

# Opción B (recomendada): el socket compatible con la API de Docker
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
docker compose up -d              # el docker compose de verdad, contra Podman

La opción B es superior porque usa el Compose oficial v2, con toda su cobertura de claves, hablando con Podman a través de un socket que emula la API de Docker. Y ese mismo socket habilita lo demás:

# Testcontainers (07-04) funcionando contra Podman
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
export TESTCONTAINERS_RYUK_DISABLED=true    # Ryuk necesita ajustes en rootless
node --test test/                            # PostgreSQL 16 y Redis 7 reales
Herramienta ¿Funciona contra Podman? Nota
docker compose v2 , vía socket La mejor opción
podman-compose Parcialmente Claves avanzadas no cubiertas
Testcontainers Desactivar Ryuk o configurarlo
Trivy, dive, hadolint Leen imágenes por el socket o del registro
Portainer Sí, con matices Apunta al socket de Podman

  1. Docker frente a Podman, y la migración de Aurora Libros

Aspecto Docker Podman
Arquitectura Cliente + daemon Sin daemon, fork/exec
Root por defecto Sí (rootless opcional) Rootless por defecto
Grupo con poder de root Sí: el grupo docker No existe
Compose Nativo, v2, completo Vía socket (recomendado) o podman-compose
Orquestación propia Swarm Ninguna: apunta a Kubernetes
Pods No , al estilo Kubernetes
Puente a K8s kompose (limitado) generate kube / play kube
Construcción BuildKit (excelente) Buildah (integrado, sin daemon)
Arranque automático restart: Unidades de systemd
Windows y macOS Docker Desktop podman machine / Podman Desktop
Ecosistema y documentación Enorme Bueno, menor
Por defecto en Casi todas partes RHEL, Fedora, CentOS Stream

Migración de Aurora Libros, paso a paso y con la comprobación al final:

# 1. La misma imagen, sin reconstruir nada
podman pull ghcr.io/auroralibros/aurora-api:2.0.0
podman image inspect ghcr.io/auroralibros/aurora-api:2.0.0 \
  --format '{{.Digest}}  {{.Architecture}}'
# sha256:c47e...  arm64      ← idéntico digest al que verifica Cosign

# 2. La firma se verifica igual: es un artefacto OCI en el registro
cosign verify ghcr.io/auroralibros/aurora-api:2.0.0 \
  --certificate-identity-regexp 'auroralibros' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

# 3. La pila entera, con el compose.yaml sin tocar una línea
systemctl --user enable --now podman.socket
export DOCKER_HOST="unix://$XDG_RUNTIME_DIR/podman/podman.sock"
docker compose up -d --wait

# 4. La prueba de que funciona de verdad
curl -s localhost:8080/libros | jq -r '.libros[] | .titulo' | head -3
# El jardín de senderos que se bifurcan
# Rayuela
# Cien años de soledad
curl -s localhost:8080/libros/4 | jq -r '.origen'   # db
curl -s localhost:8080/libros/4 | jq -r '.origen'   # cache  ← cache-aside intacto

# 5. Que sobrevivan al reinicio, con systemd en vez de restart:
podman generate systemd --new --files --name aurora-api
systemctl --user enable --now container-aurora-api.service
loginctl enable-linger $USER    # que arranquen sin iniciar sesión

Cero cambios en la imagen, cero cambios en el compose.yaml, misma firma verificable y el cache-aside devolviendo db y luego cache. La única diferencia real de operación está en el paso 5: donde Docker tenía restart: unless-stopped, aquí hay unidades de systemd de usuario, y el enable-linger es el detalle que se olvida siempre.

  1. containerd y nerdctl

containerd es un proyecto graduado de la CNCF, y es la pieza que ya usas sin saberlo: Docker Engine delega en él. Gestiona imágenes, snapshots, redes básicas y el ciclo de vida, y llama a runc. Es también el runtime por defecto de la mayoría de los Kubernetes gestionados.

Su CLI nativa, ctr, es una herramienta de depuración, no de trabajo:

sudo ctr images pull ghcr.io/auroralibros/aurora-api:2.0.0
sudo ctr run --rm ghcr.io/auroralibros/aurora-api:2.0.0 prueba
# Sin red cómoda, sin volúmenes, sin publicación de puertos: es de bajo nivel

nerdctl es lo que hace containerd usable: replica la CLI de Docker, incluye BuildKit y trae Compose.

nerdctl run -d -p 8080:8080 ghcr.io/auroralibros/aurora-api:2.0.0
nerdctl compose -f compose.yaml up -d       # sí: Compose sobre containerd
nerdctl build -t aurora-api:2.1.0 .          # BuildKit por debajo
Herramienta Nivel Para qué
ctr Muy bajo Depurar containerd; nunca para el día a día
crictl CRI Depurar nodos de Kubernetes: ver Pods desde el nodo
nerdctl Alto Trabajar con containerd como si fuera Docker

nerdctl además expone funciones que Docker no tiene: imágenes cifradas, lazy-pulling con stargz (arranque sin descargar la imagen entera) y contenedores Wasm. Es la vía habitual para experimentar con lo que viene.

  1. Buildah y Skopeo

Buildah construye imágenes OCI sin daemon y sin Dockerfile si no lo quieres:

buildah bud -t aurora-api:2.0.0 .        # usa tu Dockerfile tal cual
# o de forma imperativa, útil en scripts:
ctr=$(buildah from node:22-alpine)
buildah copy   "$ctr" src /app/src
buildah config --user 10001 --port 8080 --entrypoint '["node","/app/src/index.js"]' "$ctr"
buildah commit "$ctr" aurora-api:2.0.0

Skopeo es el que más se echa de menos cuando no se conoce: inspecciona y copia imágenes entre registros sin descargarlas al disco local.

# Inspeccionar una imagen remota sin hacer pull
skopeo inspect docker://ghcr.io/auroralibros/aurora-api:2.0.0 \
  | jq '{Digest, Architecture, Created, Labels}'

# Copiar entre registros directamente (no pasa por tu disco)
skopeo copy --all \
  docker://ghcr.io/auroralibros/aurora-api:2.0.0 \
  docker://registro.interno:5000/aurora/aurora-api:2.0.0

# Comprobar si la etiqueta de producción cambió, sin descargar 104 MB
skopeo inspect --format '{{.Digest}}' docker://ghcr.io/auroralibros/aurora-api:2.0.0

El --all copia el índice multiarquitectura completo, no solo la variante de tu máquina: sin él, replicarías media imagen y el servidor amd64 fallaría. Y el último comando es oro puro en un pipeline: verificar el digest de producción cuesta una petición HTTP en lugar de una descarga completa.

  1. Aislamiento reforzado: gVisor y Kata Containers

Los contenedores comparten el kernel del host. Si aparece una vulnerabilidad de escalada en el kernel, el aislamiento se rompe. Estos dos runtimes atacan justo eso.

gVisor (runsc) Kata Containers
Cómo aísla Kernel reimplementado en espacio de usuario que intercepta syscalls Una microVM con kernel propio por contenedor
Aislamiento Alto Muy alto (frontera de hipervisor)
Sobrecoste de arranque ~50-150 ms ~100-500 ms
Sobrecoste de E/S Notable Moderado
Compatibilidad Algunas syscalls no soportadas Total: es Linux de verdad
Requiere virtualización No Sí (anidada en la nube)
Uso típico Ejecutar código no confiable Multi-tenencia dura, cumplimiento
docker run --rm --runtime=runsc alpine dmesg | head -1
# Starting gVisor...          ← no es el kernel del host

Para Aurora Libros no hacen falta: tu código es tuyo y ya corre sin privilegios, con read_only, cap_drop: [ALL] y no-new-privileges. Se justifican cuando ejecutas código que no controlas —funciones de clientes, CI de repositorios públicos, cuadernos de terceros— y ahí el coste de rendimiento se paga sin discusión.

  1. Tabla de decisión

Situación Elección natural Por qué
Desarrollo en equipo, ecosistema amplio Docker Documentación, Desktop, Compose nativo, todo el mundo lo conoce
RHEL, Fedora o CentOS Stream Podman Es el estándar del sistema, integrado con systemd
Política de «nada como root» Podman Rootless por defecto, sin grupo con poder de root
Nodo de Kubernetes containerd o CRI-O Hablan CRI directamente
Trabajar con containerd a mano nerdctl CLI de Docker sobre containerd, con BuildKit
Construir en CI sin daemon Buildah, Kaniko o BuildKit Sin privilegios ni socket expuesto
Copiar o auditar imágenes entre registros Skopeo Sin descargar nada
Ejecutar código no confiable gVisor o Kata Aislamiento reforzado
Compose, Swarm o Docker Desktop Docker Podman no tiene equivalente pleno

La conclusión honesta: en 2026 Docker sigue siendo la elección natural para desarrollo por ecosistema y comodidad, Podman gana en entornos donde la seguridad y la política del sistema mandan, y containerd es lo que ejecuta el mundo en producción sin que casi nadie lo teclee. Y las tres cosas ejecutan tu misma imagen.

Errores Comunes y Consejos

  • Creer que hay que reconstruir para cambiar de runtime. No. El digest es el mismo y la firma de Cosign se verifica igual.
  • Repetir que «Kubernetes dejó de soportar Docker». Lo que se retiró fue dockershim, un adaptador. Las imágenes OCI nunca estuvieron en cuestión.
  • Mezclar contenedores de root y de usuario en Podman. Los almacenes están separados: lo que construyes con sudo podman no lo ve podman. Es la confusión número uno al empezar.
  • Esperar restart: always en Podman. No hay daemon. Genera unidades con podman generate systemd y no olvides loginctl enable-linger.
  • Publicar el puerto 80 en rootless sin ajustar nada. Los puertos privilegiados están vetados. Ajusta ip_unprivileged_port_start o pon un proxy delante.
  • Copiar imágenes con skopeo copy sin --all. Te llevas solo una arquitectura y el otro entorno falla al desplegar.
  • Usar ctr para el día a día. Es una herramienta de depuración de containerd. Usa nerdctl.
  • Consejo: ejecuta docker buildx imagetools inspect --raw sobre tu imagen y lee el mediaType. Ver vnd.oci en pantalla fija la idea mejor que cualquier explicación.
  • Consejo: si te interesa Podman, empieza por el socket compatible y docker compose. Migras la ejecución sin migrar las herramientas.
  • Consejo: podman generate kube es el mejor borrador de manifiestos que existe, porque parte de algo que ya funciona.

Ejercicios

Ejercicio 1 — Demuestra la portabilidad. Ejecuta ghcr.io/auroralibros/aurora-api:2.0.0 con Docker y con Podman (o nerdctl) en la misma máquina. Compara el digest de la imagen en ambos, comprueba que el endpoint /libros devuelve los nueve títulos en los dos casos y verifica la firma de Cosign contra el registro. Escribe qué es idéntico y qué cambia entre ambas ejecuciones.

Ejercicio 2 — Un pod al estilo Kubernetes con Podman. Crea un pod aurora que contenga aurora-cache (redis:7-alpine) y aurora-api, comunicándose por localhost, con el puerto 8080 publicado. Comprueba que el cache-aside funciona (origen: db la primera vez, cache la segunda). Después genera el manifiesto de Kubernetes con podman generate kube, léelo y anota tres cosas que le faltan para ser apto para producción según lo aprendido en el módulo 6.

Ejercicio 3 — Skopeo en el pipeline. Escribe un script que, sin descargar ninguna imagen, compruebe si el digest de ghcr.io/auroralibros/aurora-api:2.0.0 coincide con el que hay desplegado en producción (léelo de un fichero digest-produccion.txt), y que en caso de discrepancia replique la imagen completa, con sus dos arquitecturas, al registro interno registro.interno:5000. El script debe devolver 0 si todo coincide y 10 si tuvo que replicar.

Soluciones

Solución 1.

IMG=ghcr.io/auroralibros/aurora-api:2.0.0

docker pull "$IMG" && podman pull "$IMG"
docker image inspect "$IMG" --format 'docker: {{index .RepoDigests 0}}'
podman image inspect "$IMG" --format 'podman: {{index .RepoDigests 0}}'
# docker: ghcr.io/auroralibros/aurora-api@sha256:c47e...
# podman: ghcr.io/auroralibros/aurora-api@sha256:c47e...   ← idéntico

docker run -d --name api-d -p 8080:8080 "$IMG"
podman run -d --name api-p -p 8081:8080 "$IMG"
curl -s localhost:8080/libros | jq '.libros | length'   # 9
curl -s localhost:8081/libros | jq '.libros | length'   # 9

cosign verify "$IMG" \
  --certificate-identity-regexp 'auroralibros' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

Idéntico: el digest, el contenido, el comportamiento de la API y la verificación de la firma —porque Cosign valida un artefacto que vive en el registro, no en el runtime—. Cambia lo que rodea a la ejecución: en Docker el contenedor es hijo de dockerd (root) y aparece en ps aux como tal; en Podman rootless es hijo de conmon bajo tu UID, el proceso interno se mapea a un UID alto del host, y podman ps solo muestra tus contenedores. También cambia la red (netavark frente al bridge de Docker) y la gestión del arranque automático. Ninguna de esas diferencias afecta al artefacto: lo que se ejecuta es exactamente lo mismo.

Solución 2.

podman pod create --name aurora --publish 8080:8080
podman run -d --pod aurora --name aurora-cache redis:7-alpine
podman run -d --pod aurora --name aurora-api \
  -e REDIS_URL=redis://localhost:6379 \
  -e DB_HOST=host.containers.internal -e DB_NAME=aurora_libros \
  ghcr.io/auroralibros/aurora-api:2.0.0

curl -s localhost:8080/libros/6 | jq -r '.titulo, .origen'
# La casa de los espíritus
# db
curl -s localhost:8080/libros/6 | jq -r '.origen'
# cache

podman generate kube aurora > aurora-pod.yaml

Lo que le falta al manifiesto generado para ser apto para producción: (1) las tres sondas —genera como mucho lo que dedujo del contenedor, no las startupProbe, livenessProbe y readinessProbe sobre /salud/vivo y /salud/listo que separaste en 06-01—; (2) resources con requests y limits, sin los cuales el planificador no puede colocar el Pod con criterio ni el HPA calcular el porcentaje de uso; (3) es un Pod suelto, no un Deployment, así que no hay réplicas, ni rolling updates, ni rollback, ni recuperación si el nodo cae. Añadiría dos más: no hay ConfigMap/Secret (la configuración va como variables literales, incluidas las credenciales) y no hay Service ni Ingress. Sirve como borrador excelente porque parte de algo que funciona, y como recordatorio de que todo lo que hace un despliegue apto para producción es trabajo deliberado.

Solución 3.

#!/usr/bin/env bash
# sincronizar-registro.sh — replica solo si el digest cambió. Sin descargar nada.
set -euo pipefail
ORIGEN="docker://ghcr.io/auroralibros/aurora-api:2.0.0"
DESTINO="docker://registro.interno:5000/aurora/aurora-api:2.0.0"
FICHERO="digest-produccion.txt"

REMOTO=$(skopeo inspect --format '{{.Digest}}' "$ORIGEN")
ACTUAL=$(cat "$FICHERO" 2>/dev/null || echo "ninguno")

if [ "$REMOTO" = "$ACTUAL" ]; then
  echo "Sin cambios: $REMOTO"
  exit 0
fi

echo "Digest nuevo detectado"
echo "  producción: $ACTUAL"
echo "  registro:   $REMOTO"
skopeo copy --all "$ORIGEN" "$DESTINO"       # --all = las dos arquitecturas
skopeo inspect --raw "$DESTINO" | jq -r '.manifests[].platform.architecture'
# amd64
# arm64
echo "$REMOTO" > "$FICHERO"
exit 10

Las claves. skopeo inspect --format '{{.Digest}}' hace una única petición HTTP al manifiesto: comprobar si algo cambió cuesta milisegundos en lugar de descargar 104 MB, y eso permite ejecutarlo cada cinco minutos sin coste. skopeo copy transfiere de registro a registro sin materializar capas en el disco local, algo que docker pull seguido de docker push no puede hacer. El --all es imprescindible: sin él copiarías solo el manifiesto que corresponde a la arquitectura del ejecutor, y el día que un nodo arm64 tirase de la copia interna, el pull fallaría con un error de plataforma que cuesta bastante diagnosticar; la verificación posterior con --raw confirma que las dos arquitecturas llegaron. Y el código de salida 10 distingue «no había nada que hacer» de «he replicado», que es lo que permite encadenar un paso de notificación en el pipeline solo cuando hubo cambio real.

Conclusión

Ya sabes por qué las imágenes que has construido durante todo el curso no son «de Docker». Lo has comprobado leyendo el mediaType de tu propio índice multiarquitectura: application/vnd.oci.image.index.v1+json. La Open Container Initiative normaliza tres cosas —la Image Spec que da forma a tus imágenes, la Runtime Spec que define el config.json que abriste en 05-07, y la Distribution Spec que rige cada push a ghcr.io—, y de ahí sale la cuadrícula completa: cualquier constructor, cualquier registro, cualquier runtime.

Tienes el mapa de piezas ordenado por capas: runtimes de bajo nivel que viven milisegundos (runc, crun, youki) frente a gestores de alto nivel (containerd, CRI-O, Podman, Docker Engine), con CRI en medio explicando por qué el kubelet habla con containerd y por qué la retirada de dockershim en la 1.24 fue eliminar un intermediario, no dejar de soportar tus imágenes.

Conoces Podman a fondo: sin daemon, con los contenedores como hijos de tu usuario y no de un proceso root, lo que hace que el «grupo docker es root» de 05-03 simplemente no exista; rootless por defecto sobre user namespaces, con sus cuatro limitaciones reales y sus soluciones; los pods que ensayan el modelo de Kubernetes en tu portátil con localhost entre contenedores; generate kube y play kube como el mejor borrador de manifiestos que existe, porque parte de algo que funciona; y el socket compatible que te deja usar el docker compose oficial y Testcontainers sin cambiar de herramientas. Y has migrado Aurora Libros de verdad: misma imagen, mismo digest, misma firma verificada, mismo compose.yaml, con el cache-aside devolviendo db y después cache, y systemd de usuario ocupando el lugar de restart: unless-stopped.

Completan el cuadro containerd —la pieza que ya usabas sin saberlo— con ctr para depurar y nerdctl para trabajar, incluido nerdctl compose; Buildah para construir sin daemon; Skopeo para inspeccionar y copiar entre registros sin descargar nada, con el --all que evita replicar media imagen; y gVisor y Kata cuando hay que ejecutar código que no controlas y el coste de rendimiento se paga a gusto. La tabla de decisión te dice cuándo Docker sigue siendo la elección natural y cuándo compensa otra cosa.

En la última lección del curso levantamos la vista del todo: hacia dónde va el ecosistema de contenedores, qué tendencias tienen sustancia y cuáles son apuesta, qué no va a cambiar —y por eso es donde conviene invertir el aprendizaje—, y el cierre completo del camino que ha recorrido Aurora Libros desde aquellos quince pasos manuales de onboarding.

Docker: De Principiante a Avanzado

Módulo 1: Introducción a Docker

Módulo 2: Trabajando con Imágenes Docker

Módulo 3: Contenedores Docker

Módulo 4: Docker Compose

Módulo 5: Conceptos Avanzados de Docker

Módulo 6: Docker en Producción

Módulo 7: Ecosistema y Herramientas de Docker

© Copyright 2026. Todos los derechos reservados