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.

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

| Referencia | ¿Se mueve? | Uso |
|---|---|---|
omitir tag (FROM node) | sí: el builder asume latest | nunca prod |
:latest | sí, siempre | nunca prod |
:22 / @v4 | sí, retag | no |
:22.14.0-alpine | raro, posible | mínimo |
@sha256:… (índice o plataforma) | no | prod |
tag tuyo 2026-09-03.1 | no si no lo re-push | ok |
action @<SHA 40 hex> | no (objeto Git) | prod |
DCT / DOCKER_CONTENT_TRUST=1 | Notary v1 cierra 8 dic 2026 | no 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

| Síntoma | Causa | Fix |
|---|---|---|
| Health roto sin diff | FROM retag / latest implícito | digest |
| “En mi laptop sí” | pull distinto o arch distinta | mismo 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 vez | layer nuevo | HEALTHCHECK con node (healthchecks) |
| Pi / ARM “no matching manifest” | pineaste amd64 | digest linux/arm64 o el índice |
docker trust en CI | Notary v1 deprecado | digest + Cosign si firmas |
Relación con el resto
- Volver atrás el binario: rollback.
- Pipeline: CI/CD.
- Self-host: Docker VPS.
- Apagar si el pull nuevo alucina: kill switch.
Checklist
-
FROMcon 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 inspectmuestra el mismo sha en laptop y prod - Nada de
DOCKER_CONTENT_TRUST=1como 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.
Lecturas relacionadas
Sigue explorando Deploy y otras piezas para builders.



