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
- La pregunta correcta: ¿de quién es una imagen?
- El estándar OCI y sus tres especificaciones
- Qué significa esto en la práctica
- El mapa de piezas: runtimes de bajo y alto nivel
- CRI: por qué Kubernetes no habla con Docker
- Podman: la arquitectura sin daemon
- Rootless por defecto
- Compatibilidad de comandos y qué se rompe
- Pods,
generate kubeyplay kube - Compose, el socket compatible y Testcontainers
- Docker frente a Podman, y la migración de Aurora Libros
- containerd y
nerdctl - Buildah y Skopeo
- Aislamiento reforzado: gVisor y Kata Containers
- Tabla de decisión
- 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.
- 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.
- 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.
- 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" } } }
- 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.
- 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.
- 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 usuarioEl 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 |
- 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 igualFunciona 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 |
- Pods,
generate kube y play kube
generate kube y play kubePodman 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... 3Fí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ústergenerate 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.
- 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 PodmanLa 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 |
Sí, vía socket | La mejor opción |
podman-compose |
Parcialmente | Claves avanzadas no cubiertas |
| Testcontainers | Sí | Desactivar Ryuk o configurarlo |
Trivy, dive, hadolint |
Sí | Leen imágenes por el socket o del registro |
| Portainer | Sí, con matices | Apunta al socket de Podman |
- 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 | Sí, 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ónCero 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.
- containerd y
nerdctl
nerdctlcontainerd 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 nivelnerdctl 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.
- 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.0Skopeo 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.0El --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.
- 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 hostPara 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.
- 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 podmanno lo vepodman. Es la confusión número uno al empezar. - Esperar
restart: alwaysen Podman. No hay daemon. Genera unidades conpodman generate systemdy no olvidesloginctl enable-linger. - Publicar el puerto 80 en rootless sin ajustar nada. Los puertos privilegiados están vetados. Ajusta
ip_unprivileged_port_starto pon un proxy delante. - Copiar imágenes con
skopeo copysin--all. Te llevas solo una arquitectura y el otro entorno falla al desplegar. - Usar
ctrpara el día a día. Es una herramienta de depuración de containerd. Usanerdctl. - Consejo: ejecuta
docker buildx imagetools inspect --rawsobre tu imagen y lee elmediaType. Vervnd.ocien 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 kubees 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.comIdé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.yamlLo 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 10Las 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
- ¿Qué es Docker?
- Instalando Docker
- Arquitectura de Docker
- Comandos Básicos de Docker
- Entendiendo las Imágenes de Docker
- Creando tu Primer Contenedor Docker
- El Proyecto del Curso: la Plataforma Aurora Libros
Módulo 2: Trabajando con Imágenes Docker
- Docker Hub y Repositorios
- Construyendo Imágenes Docker
- Conceptos Básicos de Dockerfile
- Instrucciones Avanzadas del Dockerfile
- Gestionando Imágenes Docker
- Etiquetado y Publicación de Imágenes
Módulo 3: Contenedores Docker
- Ejecutando Contenedores
- Ciclo de Vida del Contenedor
- Gestionando Contenedores
- Inspección y Depuración de Contenedores
- Redes en Docker
- Persistencia de Datos con Volúmenes
- Límites de Recursos y Políticas de Reinicio
Módulo 4: Docker Compose
- Introducción a Docker Compose
- Definiendo Servicios en Docker Compose
- Comandos de Docker Compose
- Aplicaciones Multi-Contenedor
- Variables de Entorno en Docker Compose
- Perfiles, Overrides y Múltiples Entornos
- Desarrollo Local con Docker Compose
Módulo 5: Conceptos Avanzados de Docker
- Profundización en Redes Docker
- Opciones de Almacenamiento Docker
- Mejores Prácticas de Seguridad en Docker
- Optimizando Imágenes Docker
- Builds Avanzadas con BuildKit y Buildx
- Registro y Monitoreo en Docker
- El Runtime por Dentro: Namespaces, Cgroups y Capas
Módulo 6: Docker en Producción
- Preparar una Imagen para Producción
- CI/CD con Docker
- Orquestando Contenedores con Docker Swarm
- Introducción a Kubernetes
- Desplegando Contenedores Docker en Kubernetes
- Escalado y Balanceo de Carga
- Estrategias de Despliegue y Rollback
