Guía9 min

Pin de imágenes Docker para un agente IA: nada de :latest

Resumen

Cómo no redeployar un agente distinto al de ayer: pin por digest sha256, no :latest ni node:22. Actions con SHA de actions, no @v4 flotante. Fly y Railway despliegan lo que el tag resuelva hoy. El tag miente; el digest no.

DockerGitHub
Imagen Docker anclada a un digest frente a un tag latest que cambia

Qué resuelve

Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.

Ayer el agente arrancaba. Hoy el mismo Dockerfile con FROM node:22 bajó otro layer y el healthcheck se rompió. No fue tu código. Fue :latest mental. Rollback vuelve un release. Esta guía cubre que el FROM no se mueva solo. Fuentes oficiales consultadas el 3 de septiembre de 2026.

La regla

La referencia de Dockerfile admite tres formas de FROM: imagen sola, imagen con tag, o imagen con digest (<image>[@<digest>]). Si omites tag y digest, el builder asume el tag latest. Eso no es un rumor de Twitter: está en la página de FROM.

El digest es un identificador criptográfico SHA-256 del contenido. El tag se reusa y se mueve. docker image pull acepta NAME[:TAG|@DIGEST]. Tiras exactamente ese blob.

En Actions: actions/checkout@<commit-sha de 40 caracteres>, no @v4. GitHub documenta que anclar al SHA completo es hoy la única forma de tratar una action como release inmutable.

Dockerfile

Mal: FROM node:22 (sin tag el builder pone latest; con :22 el publisher puede retarget).

Menos mal: FROM node:22.14.0-alpine (sigue siendo un puntero).

Bien: FROM node:22-alpine@sha256:…. Cuando actualices Node, un PR cambia el digest a propósito, con build y health (healthchecks).

--platform=linux/amd64 en FROM elige la arquitectura del manifiesto multi-arch. Un Mac ARM que pineó el digest linux/arm64 no es el VPS linux/amd64. El índice y cada plataforma tienen SHA distintos.

npx / pnpm dlx en el runtime de prod es el mismo pecado: hoy baja otra CLI. El build pincha versiones en lockfile (pnpm frozen). El FROM también.

Cómo leer el digest

En el registry: docker buildx imagetools inspect repo:tag. Copia el Digest: del índice (manifest list), no un layer interno.

La doc de inspect usa alpine:latest como ejemplo: el índice sale sha256:21a3deaa0d32a8057914f36584b5288d2e5ecc984380bc0118285c70fa8c9300; el manifiesto linux/amd64 es otro (sha256:e7d88de73db3d3fd9b2d63aa7f447a10fd0220b7cbf39803c803f2af9ba256b3). Pin el índice si quieres “la misma lista multi-arch”; pin la plataforma si tu Machine es una sola arch. Un digest amd64 en un Pi es “no matching manifest”.

Pull por digest, según la página de Image digests: docker pull <image>@sha256:<digest>. El ejemplo oficial de pull de debian:latest imprime un Digest: sha256:3f1d6c17… después de bajar layers: ese SHA es lo que anclas, no el tag que usaste para descubrirlo.

El daemon, por defecto, baja tres layers a la vez (--max-concurrent-downloads). Eso no ancla identidad; solo ancho de banda. Un timeout de pull no es un pin.

Compose / Fly / Railway

Compose image: me/agente:latest en el VPS hace pull del surprise. Pon digest o un tag inmutable que tú publiques (2026-09-03.1) y no reuses. Fly fly deploy usa el Dockerfile del repo: si el FROM flota, el Machine nuevo no es el de ayer. Railway igual.

No “rebuild nightly” contra :latest para “estar al día”. Eso es un cron de incidentes (cron UTC).

Vercel Functions y Cloudflare Workers no usan tu FROM de Node para el runtime serverless. Esta guía es para el daemon (Compose, Fly, Railway, VPS). El Function pincha el runtime de Vercel, no tu Alpine. No mezcles los dos mundos en el mismo README.

Actions

Terceros @v4 se mueven. GitHub: anclar al SHA completo mitiga que alguien meta un backdoor en el repo de la action (haría falta una colisión SHA-1 de un objeto Git válido). Elige el SHA del repo de la action, no de un fork.

Anclar a un tag solo si confías en el autor. Un badge de “Verified creator” es señal, no candado: el tag se puede mover o borrar si un atacante entra al repo. Hay políticas de org/repo que exigen SHA completo.

Tu workflow de deploy del agente no es el lugar para “flex version”.

Tabla

Tag flotante versus digest anclado

Referencia¿Se mueve?Uso
omitir tag (FROM node)sí: el builder asume latestnunca prod
:latestsí, siemprenunca prod
:22 / @v4sí, retagno
:22.14.0-alpineraro, posiblemínimo
@sha256:… (índice o plataforma)noprod
tag tuyo 2026-09-03.1no si no lo re-pushok
action @<SHA 40 hex>no (objeto Git)prod
DCT / DOCKER_CONTENT_TRUST=1Notary v1 cierra 8 dic 2026no bases el agente ahí

Content trust no es el pin

Docker Content Trust firma tags con Notary v1. La propia página de trust avisa: DCT se retira; notary.docker.io apaga el 8 de diciembre de 2026 (brownouts en julio y agosto). Si nunca exportaste DOCKER_CONTENT_TRUST=1, el apagón no te toca. Alternativas que lista Docker: Sigstore/Cosign y Notation. El digest SHA-256 del manifiesto no depende de Notary: empieza por el digest, no por DCT.

Errores comunes

Deploy idéntico en git y runtime distinto por :latest

SíntomaCausaFix
Health roto sin diffFROM retag / latest implícitodigest
“En mi laptop sí”pull distinto o arch distintamismo digest de plataforma
Actions deprecó un comando@v4 se moviópin SHA de 40 chars
Rebuild arregló / rompiócache + latest--no-cache no es pin
Alpine “sin wget” otra vezlayer nuevoHEALTHCHECK con node (healthchecks)
Pi / ARM “no matching manifest”pineaste amd64digest linux/arm64 o el índice
docker trust en CINotary v1 deprecadodigest + Cosign si firmas

Relación con el resto

Checklist

  • FROM con digest en el Dockerfile de prod (no omitir tag)
  • Compose/Fly/Railway no usan :latest
  • El digest es del índice o de la arch que corre el Machine
  • Actions de terceros con SHA de 40 caracteres
  • Lockfile frozen en el build (pnpm)
  • PR para cambiar el digest, no un cron
  • Ensayo: docker image inspect muestra el mismo sha en laptop y prod
  • Nada de DOCKER_CONTENT_TRUST=1 como plan de 2027

FAQ

¿Dependabot? Útil: PR que mueve el digest. Tú mergeas. No auto-merge a main del FROM.

¿Distroless / alpine? Da igual si el tag flota. Pin primero, distro después.

CI: el job de deploy debe fallar si el Dockerfile tiene FROM sin @sha256. Un grep en el pipeline es más barato que el postmortem. No es un linter sofisticado; es una línea.

El ensayo: en dos máquinas, docker pull repo@sha256:… y compara Id. Igual. Luego pull :latest y no te sorprendas si diverge.

Un agente reproducible no es “el mismo repo”. Es el mismo blob. Git pincha el código; el digest pincha el runtime. Si el PR de código pasó tests y el FROM no está pineado, los tests mintieron sobre prod.

No dejes node:22 “porque Alpine es chico”. El tamaño no ancla la identidad.

Siguiente paso: si el digest ya está y prod igual cambió, logs y rollback. Sin runtime: curso.