Docker: Evitar que los contenedores arranquen con actualizaciones — Fija las imágenes responsablemente

¿Te fue útil?

Te despiertas, revisas los paneles y tu servicio “aburrido” ya no es tan aburrido. Un contenedor se reinició tras un parche rutinario del host,
descargó una imagen distinta a la de ayer y ahora el flujo de inicio de sesión está protagonizando una tragedia.

Este es el terror silencioso de las etiquetas de contenedor mutables: todo parece fijado—hasta que no lo está. Arreglémoslo correctamente: evita que los contenedores
cambien con las actualizaciones, sin convertir tu flota en un museo de imágenes inactualizables.

Qué es lo que realmente intentas controlar

«Evitar que los contenedores arranquen con actualizaciones» suele significar una de tres cosas, y deberías nombrar cuál de ellas quieres porque las soluciones difieren:

1) No cambiar los bits cuando algo se reinicia

Aceptas que los contenedores puedan reiniciarse (actualizaciones del kernel, reinicios del demonio, reinicios del nodo, reprogramaciones del orquestador), pero quieres que el contenedor reiniciado use exactamente los mismos bytes de imagen que antes. Este es el problema de fijado de imágenes.

2) No reiniciar contenedores durante el mantenimiento del host

Esto es mantenimiento y orquestación: drenar nodos, escalonar reinicios, configurar el comportamiento de actualización de systemd/docker y evitar automatizaciones “útiles” que reinicien todo con las actualizaciones de paquetes. El fijado ayuda, pero no evita los reinicios.

3) No desplegar nuevas versiones de la aplicación sin un cambio explícito

Esto es ingeniería de releases: desacoplar compilación de despliegue, requerir un cambio de configuración (commit en Git / solicitud de cambio) para desplegar un nuevo digest, y hacer que los rollbacks sean aburridos.

Este artículo se centra en (1) y (3), con suficiente realidad operacional para que (2) no te sorprenda.

Hechos e historia breve: por qué esto sigue ocurriendo

Un poco de contexto ayuda, porque este problema no es un error del usuario; es una propiedad de cómo funcionan las imágenes, las etiquetas y el pull.

  • Hecho 1: Las etiquetas de imagen Docker son punteros mutables. El registro puede mover :latest (o :1.2) a un nuevo digest en cualquier momento.
  • Hecho 2: El único identificador inmutable en el que puedes confiar para «mismos bytes» es el digest de contenido (p. ej., @sha256:…).
  • Hecho 3: La UX original de Docker hizo que las etiquetas parecieran versiones, lo que entrenó a toda una industria a tratar cadenas mutables como hechos inmutables.
  • Hecho 4: La API del Docker Registry v2 (mediados de 2010s) formalizó los manifests y digests, por eso hoy podemos fijar por digest sin trucos.
  • Hecho 5: «Latest» no es una versión; es un término de marketing. Los registros nunca prometieron semántica para ello, y cumplieron esa promesa.
  • Hecho 6: Las imágenes multi-arquitectura complicaron aún más el «mismo tag»: la misma etiqueta puede mapear a manifests distintos por arquitectura.
  • Hecho 7: El comportamiento de pull cambió con el tiempo entre versiones de Docker Engine, Compose y orquestadores—así que «antes no pasaba» puede ser verdad.
  • Hecho 8: Las características modernas de la cadena de suministro (SBOMs, atestaciones de procedencia, verificación de firmas) asumen que puedes nombrar un artefacto inmutable. Los digests son ese nombre.
  • Hecho 9: Las «builds reproducibles» siguen siendo raras en el mundo de contenedores. Incluso si tu Dockerfile no cambia, recompilar con frecuencia produce un digest distinto.

Si buscas un villano único, no es Docker. Es la combinación de punteros mutables más automatización que asume que los punteros son estables.

Modos de fallo: cómo los contenedores «se actualizan» solos

Pull-on-recreate: la trampa más común

Un contenedor no se transforma silenciosamente en una nueva imagen mientras se ejecuta. Lo que ocurre es más banal: algo lo recrea (Compose up, actualización de Swarm,
rollout de Kubernetes, reinicio de nodo), y el runtime descarga lo que la etiqueta apunta en ese momento.

Si usaste image: vendor/app:1.4 y el proveedor retagueó 1.4 para incluir un fix de seguridad, tu próxima recreación descargará nuevos bytes.
Eso puede ser bueno. Puede ser catastrófico. Definitivamente no está controlado por tu gestión de cambios.

Actualizaciones del host que reinician el daemon de Docker

Las actualizaciones de paquetes pueden reiniciar dockerd. Dependiendo de tus políticas de reinicio, los contenedores vuelven. Si tu orquestador los recrea, las etiquetas se resuelven de nuevo.
Si has fijado por digest y el nodo aún tiene la imagen localmente, obtendrás los mismos bytes. Si dependes de etiquetas y permites pulls, obtendrás ruleta.

Tareas de «limpieza» que eliminan imágenes

Podar imágenes es sano hasta que no lo es. Si un nodo poda imágenes sin usar y después necesita recrear un contenedor, debe descargar de nuevo. Ahí es cuando las etiquetas cambian bajo tus pies.

Retagging en el lado del registro y cultura de force-push

Algunas organizaciones tratan las etiquetas como ramas y las reescriben. Si tu despliegue referencia etiquetas, aceptaste «lo que haya en esa rama ahora mismo», aunque no fuera tu intención.

Broma #1: Usar :latest en producción es como nombrar tu único archivo de copia de seguridad final_final_really_final.zip. Es una vibra, no un plan.

Valores por defecto del orquestador que fomentan el pull

Kubernetes con imagePullPolicy: Always siempre hablará con el registro aunque el digest esté presente. Compose tiene sus propios predeterminados de pull según el comando.
Los mandos existen, pero los valores por defecto están optimizados para conveniencia, no para análisis postmortem de incidentes.

Opciones de fijado: tags, digests y referencias firmadas

Opción A: Fijar por digest (recomendado para “no cambiar los bits”)

Usa repo/name@sha256:…. Ese digest identifica un manifest (a menudo un índice multi-arquitectura), que a su vez apunta a las capas de la imagen. Es inmutable por definición.
Si haces pull por digest, obtendrás los mismos bytes mañana, la próxima semana y durante ese rollback a las 3 a.m.

La desventaja es la ergonomía humana. Los digests son largos, feos y no significan nada en una revisión de código. Por eso el fijado responsable usa ambos:
una etiqueta legible por humanos para la intención y un digest para la inmutabilidad.

Opción B: Fijar por “etiquetas inmutables” (aceptable si las haces cumplir)

Si tu registro y la política de tu organización garantizan que etiquetas como 1.2.3 nunca se reescriben, entonces fijar por tag semver puede funcionar.
Pero entiende en qué confías: un contrato social, no criptografía.

Opción C: Fijar por digest y verificar firmas (lo mejor cuando puedes permitírtelo)

Los digests resuelven “mismos bytes”. No resuelven “bytes en los que deberíamos confiar”. Ahí entran la firma y la procedencia: puedes requerir que
un digest esté firmado por la identidad de tu CI y acompañado de un SBOM/declaración de procedencia.

No necesitas hacerlo todo de golpe. Empieza con fijado por digest. Añade verificación de firmas cuando lo básico ya no te deje dormir.

Qué significa «responsablemente»

Fijar responsablemente no es «nunca actualizar». Es «las actualizaciones ocurren solo cuando lo decidimos, y podemos explicar qué bits están en ejecución.»
Quieres:

  • Referencias inmutables en producción (digests).
  • Un flujo de promoción que avance artefactos probados.
  • Rollback que reutilice digests conocidos buenos.
  • Políticas de retención en el registro que no borren lo que usa prod.

Docker Compose y Swarm: evita pulls sorpresa

Compose: haz la recreación determinista

Compose se usa con frecuencia como orquestador pero se comporta como una herramienta de conveniencia. Eso está bien—hasta que lo ejecutas bajo automatización en un horario.
Entonces aparecen las sorpresas de “¿por qué hizo pull?”.

Para Compose, la estrategia práctica es:

  • Referencia imágenes por digest en compose.yaml para producción.
  • Usa docker compose pull explícito durante ventanas de mantenimiento o despliegues impulsados por CI.
  • Evita el comportamiento de “siempre descargar” a menos que realmente lo quieras.

Swarm: entiende las actualizaciones de servicio y los digests

Los servicios de Swarm pueden seguir etiquetas, pero también registran digests resueltos. Aun así necesitas ser explícito sobre las actualizaciones para controlar cuándo se adopta un nuevo digest.

Enfoque Kubernetes: mismo problema, distintos controles

Los usuarios de Kubernetes suelen actuar como si el drama de las etiquetas Docker fuera de equipos pequeños. Luego un Deployment referencia myapp:stable y un nodo se drena,
y de repente “stable” significa “sorpresa”.

Los controles en Kubernetes son:

  • Fijar por digest en el Pod spec: image: repo/myapp@sha256:…
  • Establecer imagePullPolicy intencionalmente. Para digests, IfNotPresent suele estar bien, pero cuidado con la deriva de cache en nodos.
  • Usar políticas de admisión para rechazar etiquetas mutables en ciertos namespaces.

Si fijas por digest, una reprogramación no cambia los bytes. Si fijas por tag, las reprogramaciones se convierten en despliegues.

Registros, retención y la trampa del digest

Fijar por digest introduce un nuevo modo de fallo: tu registro podría recolectar basura de manifests no etiquetados. Muchos sistemas de retención tratan lo “no etiquetado” como “seguro de eliminar.”
Pero cuando despliegas por digest, puedes dejar de referenciar la etiqueta y el registro piensa que el artefacto está muerto.

El patrón responsable es: mantener una etiqueta que siga al artefacto promovido (aunque la producción use el digest), o configurar la retención del registro
para preservar manifests referenciados por digests en uso. Esto es menos glamuroso que firmar, pero más propenso a prevenir tu próximo outage.

Además: si usas un digest de índice multi-arquitectura, borrar el manifest de una arquitectura puede romper pulls en esa arquitectura, incluso si “funciona en amd64.”
Al registro no le importan tus sentimientos.

Tareas prácticas: comandos, salidas y decisiones (12+)

Estas son cosas reales que puedes hacer en un host hoy. Cada tarea incluye: un comando, qué significa la salida y la decisión que tomas a partir de ello.

Task 1: Ver qué imagen está usando realmente un contenedor en ejecución

cr0x@server:~$ docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.ID}}\t{{.Status}}'
NAMES          IMAGE                 ID           STATUS
api            myco/api:1.9          7c2d1a9f01b2   Up 3 days
worker         myco/worker:1.9       1a0b9c3f9e44   Up 3 days

Significado: Estás viendo la referencia configurada (a menudo una etiqueta), no necesariamente el digest al que se resolvió.
Decisión: Si esto muestra etiquetas, no asumas inmutabilidad. Pasa a la inspección por digest a continuación.

Task 2: Inspeccionar el Image ID y RepoDigests de un contenedor

cr0x@server:~$ docker inspect api --format 'ImageID={{.Image}} RepoDigests={{json .RepoDigests}}'
ImageID=sha256:5c0c0b9d2f2d8e0e5a11c8b3c9fb4f0d0a4c3cc0f55b9c8e0d1f0f2a3b4c5d6e RepoDigests=["myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a"]

Significado: .Image es el content ID local de la imagen; RepoDigests muestra el mapeo del digest del registro.
Decisión: Si RepoDigests está presente, regístralo y fíjalo en la configuración. Si no está, puede que ejecutes una imagen sin digest de registro (construida localmente o descargada sin metadata de digest).

Task 3: Resolver una etiqueta a un digest sin descargar capas (inspect de manifest)

cr0x@server:~$ docker buildx imagetools inspect myco/api:1.9
Name:      myco/api:1.9
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a

Manifests:
  Name:      myco/api:1.9@sha256:4f3c2b1a...
  Platform:  linux/amd64
  Name:      myco/api:1.9@sha256:7a6b5c4d...
  Platform:  linux/arm64

Significado: La etiqueta apunta actualmente a un digest de índice; los manifests por arquitectura difieren.
Decisión: Fija al digest de índice si ejecutas arquitecturas mezcladas; fija al digest específico de la arquitectura si requieres determinismo absoluto por plataforma.

Task 4: Confirmar si Compose recreará contenedores (y por tanto podría hacer pull)

cr0x@server:~$ docker compose up -d --no-build
[+] Running 2/2
 ✔ Container stack-api-1     Started
 ✔ Container stack-worker-1  Started

Significado: «Started» sugiere que los contenedores existentes fueron iniciados; «Recreated» significaría que se crearon contenedores nuevos.
Decisión: Si ves «Recreated» inesperadamente, averigua por qué (drift de configuración, cambio en variables de entorno, cambio en bind mount). La recreación es donde la deriva de tags muerde.

Task 5: Forzar a Compose a usar solo imágenes caché locales (detectar deriva de tags de forma segura)

cr0x@server:~$ docker compose up -d --pull never
[+] Running 2/2
 ✔ Container stack-api-1     Started
 ✔ Container stack-worker-1  Started

Significado: Compose se negó a hacer pull. Si necesitara una imagen faltante, fallaría.
Decisión: Usa esto en la automatización de producción para prevenir “oh, descargó algo nuevo porque la caché fue podada.”

Task 6: Detectar si una etiqueta cambió desde la última vez (comparar digests)

cr0x@server:~$ docker buildx imagetools inspect myco/api:1.9 --format '{{.Digest}}'
sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a

Significado: Este es el digest actual para la etiqueta.
Decisión: Guárdalo en Git (o en tu sistema de despliegue). Si cambia sin un release planeado, trátalo como un evento de cambio upstream.

Task 7: Fijar una imagen por digest en Compose

cr0x@server:~$ cat compose.yaml
services:
  api:
    image: myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
    restart: unless-stopped

Significado: El despliegue ahora nombra un artefacto inmutable.
Decisión: Cambiaste “actualizaciones cómodas” por “actualizaciones controladas.” Ese es el punto. Construye un pipeline de promoción para no quedarte estancado para siempre.

Task 8: Verificar qué digest está en ejecución tras el despliegue

cr0x@server:~$ docker compose ps --format json | jq -r '.[].Name + " " + .Image'
stack-api-1 myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a

Significado: Tu referencia de runtime ahora incluye el digest.
Decisión: Si todavía ves etiquetas aquí, tu ruta de despliegue está reescribiendo la referencia. Corrige eso antes de declarar victoria.

Task 9: Encontrar pulls de imagen recientes y correlacionarlos con incidentes

cr0x@server:~$ journalctl -u docker --since "24 hours ago" | grep -E "Pulling|Downloaded|Digest"
Jan 03 01:12:44 server dockerd[1123]: Pulling image "myco/api:1.9"
Jan 03 01:12:49 server dockerd[1123]: Downloaded newer image for myco/api:1.9
Jan 03 01:12:49 server dockerd[1123]: Digest: sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a

Significado: Los logs del demonio muestran actividad de pull de tags y el digest resuelto.
Decisión: Si un incidente comenzó justo después de un pull, asume deriva de imágenes hasta que se demuestre lo contrario. Captura el digest para el postmortem y rollback.

Task 10: Ver qué imágenes están presentes y si la poda podría perjudicarte

cr0x@server:~$ docker images --digests --format 'table {{.Repository}}\t{{.Tag}}\t{{.Digest}}\t{{.ID}}\t{{.Size}}' | head
REPOSITORY   TAG   DIGEST                                                                    ID            SIZE
myco/api     1.9   sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a   5c0c0b9d2f2d  312MB
myco/worker  1.9   sha256:1a2b3c4d5e6f...                                                    9aa0bb11cc22  188MB

Significado: Los digests mostrados aquí son lo que tu runtime puede usar si no necesita descargar.
Decisión: Si tus nodos podan rutinariamente, trátalo como “volveremos a descargar”, y fija por digest para prevenir la deriva de tags.

Task 11: Hacer un pull de prueba para ver si la etiqueta cambiaría (sin desplegar)

cr0x@server:~$ docker pull myco/api:1.9
1.9: Pulling from myco/api
Digest: sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a
Status: Image is up to date for myco/api:1.9
docker.io/myco/api:1.9

Significado: “Up to date” significa que la etiqueta se resuelve actualmente a lo que ya tienes localmente.
Decisión: Si el digest cambia aquí, detente y trátalo como un nuevo release. No permitas que un reinicio de mantenimiento se convierta en un despliegue no revisado.

Task 12: Probarte a ti mismo que las etiquetas son mutables (la demo incómoda)

cr0x@server:~$ docker image inspect myco/api:1.9 --format '{{index .RepoDigests 0}}'
myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a

Significado: Esto es lo que 1.9 apunta ahora mismo. No es una garantía sobre la próxima semana.
Decisión: Si te importa que los reinicios sean deterministas, deja de desplegar etiquetas en producción. Punto.

Task 13: Identificar contenedores que probablemente cambien en el próximo reinicio (usando tags)

cr0x@server:~$ docker ps --format '{{.Names}} {{.Image}}' | grep -v '@sha256:' | head
api myco/api:1.9
nginx nginx:latest

Significado: Cualquier cosa sin @sha256: se basa en tags y por tanto es mutable.
Decisión: Ponlos en una lista de remediación. Empieza por servicios expuestos a Internet y rutas de autenticación. Ya sabes, los que arruinan fines de semana.

Task 14: Comprobar políticas de reinicio que amplificarán reinicios del demonio

cr0x@server:~$ docker inspect api --format 'Name={{.Name}} RestartPolicy={{.HostConfig.RestartPolicy.Name}}'
Name=/api RestartPolicy=unless-stopped

Significado: Las políticas de reinicio determinan qué vuelve cuando el demonio se reinicia.
Decisión: Mantén políticas de reinicio, pero combínalas con fijado por digest. “Always restart” + “tag mutable” es cómo obtienes redeploys sorpresa.

Task 15: Validar riesgo de retención en el registro (listar digests locales en uso)

cr0x@server:~$ docker ps -q | xargs -n1 docker inspect --format '{{.Name}} {{json .RepoDigests}}' | head -n 3
/api ["myco/api@sha256:9d3a0c2e4d8b2b8a9f3f2c1d0e9b8a7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a"]
/worker ["myco/worker@sha256:1a2b3c4d5e6f..."]
/metrics ["prom/prometheus@sha256:7e6d5c4b3a2f..."]

Significado: Este es el conjunto real de artefactos de los que depende la producción.
Decisión: Asegura que tu registro mantiene estos digests. Si tu política de retención borra lo “no etiquetado”, asegúrate de que estos digests queden etiquetados o exentos.

Task 16: Detectar automatizaciones “útiles” como Watchtower (o equivalentes)

cr0x@server:~$ docker ps --format '{{.Names}} {{.Image}}' | grep -i watchtower
watchtower containrrr/watchtower:1.7.1

Significado: Un actualizador automático está ejecutándose en el host.
Decisión: O bien elimínalo en producción, delimítalo estrictamente, o haz que sea consciente de digests con una puerta de aprobación. Las actualizaciones no controladas son lo opuesto a SRE.

Guion rápido de diagnóstico

Cuando sospeches “los contenedores cambiaron después de actualizaciones”, no empieces discutiendo sobre etiquetas en un hilo de chat. Verifica los hechos rápido e identifica el cuello de botella:
¿es deriva de imágenes, deriva de configuración o presión de recursos?

Primero: ¿cambió el digest en ejecución?

  • Revisa los RepoDigests del contenedor actual vía docker inspect.
  • Compáralo con tu digest conocido bueno (desde Git, changelogs o notas de incidentes).
  • Si los digests difieren, trátalo como un despliegue. Inicia rollback o corrección hacia adelante.

Segundo: ¿qué disparó el reinicio/recreación?

  • Revisa journalctl -u docker por reinicios del demonio y pulls.
  • Revisa eventos de Compose/Swarm/Kubernetes por reprogramaciones y recreaciones.
  • Busca logs de actualización de paquetes que reiniciaron docker/containerd.

Tercero: si el digest no cambió, ¿qué más cambió?

  • Deriva de configuración: vars de entorno, archivos montados, secretos, DNS, certificados.
  • Cambios en kernel/host: comportamiento de cgroups, iptables/nft, MTU, cambios en el driver de almacenamiento.
  • Restricciones de recursos: disco lleno, agotamiento de inodos, throttling de CPU.

Cuarto: decide si congelar o avanzar

  • Si ejecutas tags: congela cambiando a digest, luego investiga el movimiento de la etiqueta upstream de forma segura.
  • Si ya estás fijado: tu incidente probablemente no es “actualización misteriosa”. Trabaja donde importa.

Errores comunes: síntoma → causa raíz → solución

1) «Nada cambió» pero el comportamiento cambió tras un reboot

Síntoma: Tras reiniciar el nodo, los contenedores se comportan distinto; las cabeceras de versión no coinciden con lo esperado.

Causa raíz: Los contenedores se recrearon y descargaron un digest distinto porque la etiqueta se movió, o la imagen local fue podada.

Solución: Fija por digest en las configuraciones de producción. Usa docker compose up --pull never en automatización. Deja de podar sin entender las consecuencias de redeploy.

2) El rollback no hizo rollback

Síntoma: Hiciste “rollback” a :stable pero el bug persiste o cambia de forma.

Causa raíz: Hiciste rollback a una etiqueta que ya avanzó; las etiquetas no son snapshots.

Solución: Haz rollback al digest registrado. Guarda digests por release en Git o en tu herramienta de despliegue.

3) Solo los nodos ARM están rotos

Síntoma: amd64 está bien; nodos arm64 entran en crashloop tras reprogramación.

Causa raíz: El índice multi-arquitectura cambió o el manifest de una arquitectura fue borrado por retención/GC.

Solución: Fija al digest de índice multi-arquitectura y protégelo del GC; verifica que exista el manifest de cada arquitectura antes de promocionar.

4) «Fijamos» pero aún hubo deriva

Síntoma: El archivo Compose muestra un digest, sin embargo los nodos descargan otra cosa.

Causa raíz: Un script wrapper o un sistema de plantillas reescribió la referencia de imagen a una etiqueta en runtime, o se usó otro archivo Compose.

Solución: Imprime la configuración efectiva en CI/CD y asegura la presencia de @sha256:. Trata la «config renderizada» como el artefacto de despliegue.

5) Producción ya no puede descargar el digest fijado

Síntoma: Un nodo nuevo falla con “manifest unknown” para un digest que desplegaste el mes pasado.

Causa raíz: La retención del registro borró el manifest porque estaba no etiquetado, o se limpió el repo agresivamente.

Solución: Mantén «release tags» apuntando a digests promovidos, o configura exenciones de retención para digests usados en producción. Audita la retención regularmente.

6) El equipo de seguridad odia el fijado

Síntoma: «Fijaste imágenes, así que nunca parchearás.»

Causa raíz: El fijado se adoptó como congelamiento, no como un flujo de promoción controlado.

Solución: Fija en producción, pero actualiza mediante promoción impulsada por CI. Añade reconstrucciones programadas, puertas de escaneo y despliegues explícitos.

Tres mini-historias corporativas desde el campo

Mini-historia 1: Un incidente causado por una suposición equivocada

Una compañía B2B SaaS mediana ejecutaba Docker Compose en unas pocas VMs potentes. Usaban myco/api:2.3 en todas partes y asumían “2.3 significa 2.3.”
El equipo proveedor de la imagen tenía otra suposición: trataban 2.3 como una línea menor y la reconstruían regularmente con imágenes base parcheadas.

Un martes, el equipo de infraestructura desplegó actualizaciones de seguridad del host. Docker se reinició. Los contenedores volvieron. La API volvió—en su mayoría.
El síntoma fue extraño: un subconjunto de clientes recibía 401s, luego los reintentos funcionaban, luego fallaban otra vez. Parecía rate limiting o un problema de caché de tokens.

El on-call hizo lo habitual: revisar logs de la app, revisar Redis, revisar el balanceador. Los gráficos sugerían latencia elevada, no fallos totales.
Mientras, los clientes escalaron porque “problemas intermitentes de autenticación” es el tipo de bug que derrite la confianza.

El descubrimiento fue vergonzosamente simple. Alguien comparó el digest en ejecución en un host con otro y descubrió que eran diferentes.
La mitad de la flota había descargado una reconstrucción nueva de 2.3; la otra mitad aún tenía cacheado el digest anterior y no necesitó descargar.
El clúster era ahora un canario mismatched que nadie pidió.

Arreglarlo llevó menos tiempo que explicarlo: fijaron producción a un digest, hicieron roll al toda la flota a un artefacto conocido y estabilizaron.
La solución a largo plazo fue cultural: “tag equivale a versión” se volvió una suposición prohibida a menos que el registro hiciera cumplir inmutabilidad.

Mini-historia 2: Una optimización que se volvió en contra

Otra compañía tenía un objetivo sensato: reducir el uso de disco en nodos. Sus contenedores producían mucho churn de imágenes y el equipo de almacenamiento se quejaba
de volúmenes raíz hinchados y backups lentos. Así que añadieron una limpieza agresiva nocturna: docker system prune -af en cada nodo.

Al principio funcionó. Las alertas de disco desaparecieron. Todos celebraron y volvieron a ignorar la planificación de capacidad. Luego una dependencia silente se actualizó:
un nodo se reinició tras un parche de kernel y algunos servicios se recrearon. Esos servicios estaban basados en tags y ahora todas las imágenes tuvieron que descargarse de nuevo.

El registro tenía un límite de tasa pequeño, y la ruta de red hacia él no era tan ancha como la gente pensaba. Los reinicios en rolling se volvieron reinicios lentos.
Más importante, una etiqueta upstream se movió a un digest que incluía un cambio de librería con distintos valores por defecto TLS. El servicio no crasheó; simplemente dejó de hablar con un upstream que usaba cifrados antiguos.

El incidente no fue “la limpieza de disco rompió prod.” El incidente fue “la limpieza de disco convirtió cada reinicio en un redeploy.”
La solución fue separar responsabilidades: mantener la limpieza, pero fijar imágenes por digest y conservar una pequeña caché de digests de producción en cada nodo.
También dejaron de fingir que los registros son infinitamente disponibles a velocidad infinita.

Broma #2: docker system prune -af en cron es el equivalente operacional de afeitarse con una motosierra. Es rápido—hasta que no lo es.

Mini-historia 3: Una práctica aburrida pero correcta que salvó el día

Una empresa regulada ejecutaba cientos de contenedores, y su control de cambios era… llamémoslo “entusiasta.” Los ingenieros se quejaban del papeleo,
pero el proceso de release tenía una característica subestimada: cada artefacto de despliegue incluía los digests resueltos, guardados junto con la configuración.

Una mañana, un servicio crítico empezó a lanzar fallos de segmentación tras una falla de nodo que desencadenó reprogramación. Todos sospecharon
“hardware malo” o “regresión de kernel.” La firma del crash estaba en una dependencia nativa, así que las acusaciones comenzaron.

El SRE on-call hizo algo profundamente poco sexy: comparó los digests en el registro de despliegue con los digests en ejecución en el nuevo nodo.
No coincidían. El nodo había descargado una etiqueta que avanzó durante la noche porque una pipeline de builds republicó “stable” tras pruebas.

Debido a que la organización tenía el digest registrado, el rollback fue inmediato y preciso: desplegar el digest conocido anterior. Sin conjeturas. Sin “probar la etiqueta de la semana pasada.”
El servicio se recuperó rápidamente y el incidente se degradó de “pánico infraestrutural” a “bug en control de releases.”

El postmortem no fue glamuroso tampoco: imponer fijado por digest, dejar de usar “stable” en producción y requerir aprobaciones para retagging.
Las prácticas aburridas no llenan conferencias, pero te devuelven el sueño.

Listas de verificación / plan paso a paso

Paso a paso: mover un despliegue Compose de tags a digests (sin caos)

  1. Inventario de lo que está en ejecución. Reúne nombres de contenedores, imágenes actuales y repo digests actuales.
  2. Resolver tags a digests. Para cada etiqueta usada en producción, registra el digest al que actualmente mapea.
  3. Actualizar archivos Compose. Sustituye repo:tag por repo@sha256:….
  4. Desplegar con pulls deshabilitados. Usa docker compose up -d --pull never para evitar adoptar accidentalmente una etiqueta movida.
  5. Verificar la referencia en ejecución. Confirma que @sha256: aparece en docker compose ps y docker inspect.
  6. Registrar digests en Git/changelogs. Facilita responder “¿qué está en ejecución?” sin SSH a prod.
  7. Arreglar tu política de retención. Asegura que los digests promovidos no sean recolectados como basura.
  8. Construir un flujo de promoción. CI construye una vez, taggea de forma inmutable y “promociona” moviendo una etiqueta de release o actualizando la configuración con un digest.

Checklist de producción: antes de parchear hosts

  • Confirma que producción usa digests, no tags.
  • Asegura que los nodos tienen suficiente disco para no verse forzados a una poda de emergencia.
  • Confirma la accesibilidad al registro y las credenciales desde los nodos.
  • Deshabilita auto-actualizadores en nodos de producción (o resérvalos para stacks no críticos).
  • Planifica el reinicio del demonio: conoce qué contenedores se reiniciarán y en qué orden.

Checklist de release: antes de mover una nueva imagen a producción

  • Promociona un digest específico que pasó pruebas; no promociones una etiqueta que pueda moverse.
  • Verifica que el digest exista para las arquitecturas necesarias.
  • Escanea la imagen y registra SBOM/procedencia si tu organización lo requiere.
  • Mantén un digest de rollback explícito listo (y probado).

Principio operativo que vale la pena imprimir

parafraseada de Gene Kim: hacer cambios más pequeños y controlados reduce el riesgo y acelera la recuperación.
Fijar por digest hace que los reinicios sean aburridos; la promoción controlada hace que las actualizaciones sean deliberadas.

Preguntas frecuentes

1) Si los contenedores no cambian mientras se ejecutan, ¿por qué mi app cambió «sin despliegue»?

Porque algo recreó el contenedor (reinicio del demonio, reinicio del nodo, reprogramación del orquestador), y tu configuración referenciaba una etiqueta mutable.
Esa recreación implícitamente realizó un despliegue.

2) ¿Fijar por digest es suficiente para ser seguro?

Es suficiente para ser determinista. La seguridad es aparte: aún necesitas reconstrucciones a tiempo, escaneo y un camino de promoción controlado.
Los digests ayudan a la seguridad al dejar claro exactamente qué artefacto escaneaste y aprobaste.

3) ¿Puedo seguir usando tags en desarrollo?

Sí. En desarrollo la conveniencia importa. Usa etiquetas como :latest si acelera la iteración. Pero haz límite de promoción estricto:
producción debe tomar digests (o tags inmutables forzados).

4) ¿Qué tiene de malo usar etiquetas :1.2.3?

Nada—si tu registro y la política organizacional hacen cumplir inmutabilidad y lo auditas. Si no está hecho cumplir, confías en que nadie retaggee,
haga force-push o “reconstruya 1.2.3 con un arreglo rápido.”

5) ¿Fijar por digest romperá el parchado automático de imágenes base?

Lo cambia de “silencioso” a “explícito.” Deberías reconstruir y promocionar nuevos digests con un calendario. Eso es más sano que despertarte con una sorpresa.

6) ¿Cómo hago rollback de forma segura?

Haz rollback desplegando el digest conocido bueno anterior, no “revirtiendo” al nombre de una etiqueta. Guarda digests por release para que el rollback sea un cambio, no una búsqueda.

7) ¿Compose siempre hace pull en up?

No siempre. El comportamiento depende del comando y flags. Si quieres impedir pulls en producción, usa --pull never.
Si quieres refrescar deliberadamente, ejecuta docker compose pull como paso separado.

8) ¿Y Kubernetes—debería usar siempre digests allí también?

Para producción, sí, a menos que tengas inmutabilidad estricta de tags y control de admisión. Fijar por digest previene que “reprogramar = redeploy.”
Si usas tags, trata cada evento de nodo como un posible rollout.

9) ¿Por qué mi digest fijado falló al descargar en un nodo nuevo meses después?

La retención o garbage collection del registro probablemente borró el manifest (a menudo porque estaba sin tag). Arregla políticas de retención y mantiene tags de release
apuntando a digests promovidos.

10) ¿Fijo el digest del índice o el digest específico de la arquitectura?

Si ejecutas arquitecturas mezcladas, fija el digest de índice para que cada nodo obtenga el manifest de plataforma correcto.
Si ejecutas una sola arquitectura y quieres máximo determinismo, fija el digest específico de la plataforma.

Conclusión: siguientes pasos que puedes hacer esta semana

Si recuerdas una cosa: las etiquetas son nombres convenientes, no versiones inmutables. Si tu configuración de producción referencia etiquetas, tu próxima actualización de host puede convertirse en un despliegue.
Eso no es “DevOps.” Es apostar con mejor marca.

Pasos prácticos:

  • Haz inventario de contenedores en producción que aún ejecutan imágenes basadas en tags (sin @sha256:).
  • Elige un servicio crítico y fíjalo por digest en su configuración de despliegue.
  • Actualiza tu automatización para desplegar con pull deshabilitado por defecto (--pull never) y para promover nuevos digests explícitamente.
  • Audita la retención del registro para que los digests fijados sigan siendo descargables por meses, no días.
  • Registra digests por release para que el rollback sea un cambio, no una cacería.

No necesitas herramientas de cadena de suministro perfectas para detener las actualizaciones sorpresa. Necesitas determinismo, disciplina y la decisión de no tratar punteros mutables como contratos.

← Anterior
Email “550 rejected”: qué significa realmente y cómo desbloquearlo
Siguiente →
Docker “No se puede conectar con el daemon de Docker»: soluciones que realmente funcionan

Deja un comentario