Dockerfile “failed to solve”: errores que puedes solucionar al instante

¿Te fue útil?

No hay nada que diga “pipeline de entrega calmado y sano” como una compilación Docker que explota con failed to solve cinco minutos antes de una release. Tu registro de CI se convierte en una novela de detectives escrita por un compilador. Alguien sugiere “simplemente ejecútalo de nuevo”, como si los sistemas de producción funcionaran con optimismo.

Esta guía es la versión pragmática: qué significa realmente failed to solve (BuildKit se está quejando, no Docker), cómo encontrar la falla real rápidamente y las soluciones que funcionan de forma confiable bajo restricciones reales: redes inestables, runners cerrados, repos gigantes y controles “de seguridad” que rompen builds.

Qué significa realmente “failed to solve”

Cuando ves failed to solve, casi siempre estás viendo un error de BuildKit que se propaga. BuildKit es el motor de construcción moderno de Docker: construye un grafo de dependencias (capas, orígenes, montajes) y luego lo “resuelve” como un plan de build. Si algo se rompe en cualquier punto de ese grafo—descargar una imagen base, leer un archivo en el contexto de build, ejecutar un RUN, calcular una suma de verificación, desempaquetar un archivo—BuildKit informa la falla como “failed to solve”. No intenta ser misterioso. Simplemente tampoco intenta ser útil.

Hay tres implicaciones clave:

  • El error real casi siempre está unas líneas más arriba. “failed to solve” es el cierre, no la película.
  • El paso que falla es un nodo en un grafo. El número de paso en los logs puede ser engañoso si los pasos se ejecutan en paralelo o están en caché.
  • El contexto importa más que la pureza del Dockerfile. Muchas fallas no son “errores del Dockerfile”; son problemas de entorno, red, permisos o tamaño del contexto.

Si quieres un modelo mental útil operativamente: Docker build es una canalización de entradas (archivos del contexto, imágenes base, secretos, red) hacia transformaciones deterministas (capas). “Failed to solve” significa que una entrada falta, no se puede leer, no es confiable o es lo suficientemente lenta como para considerarse muerta.

Una cita, porque sigue siendo dolorosamente cierta en los sistemas de build y en todo lo demás: “La esperanza no es una estrategia.” — Gene Kranz

Guía rápida de diagnóstico

Este es el orden que ahorra más tiempo en la mayoría de entornos. No improvises. Sigue los pasos hasta encontrar el cuello de botella.

1) Localiza el primer error real, no el último envoltorio

  • Desplázate hacia arriba hasta la primera línea de fallo no ruidosa. BuildKit a menudo imprime múltiples errores en cascada.
  • Identifica a qué categoría pertenece: contexto, sintaxis, red, auth, permisos, platform, caché o runtime.

2) Confirma el builder y sus ajustes

  • ¿Está habilitado BuildKit? ¿Estás usando buildx? ¿Qué driver (docker, docker-container, kubernetes)?
  • ¿La falla ocurre en un builder remoto donde el sistema de archivos/red difiere de tu portátil?

3) Revisa primero la salud del contexto de build

  • La mayoría de las “soluciones instantáneas” son problemas de contexto: archivos faltantes, .dockerignore incorrecto, contexto gigantesco, permisos.
  • Verifica las rutas de archivos referenciadas en COPY/ADD y confirma que estén dentro del contexto de build.

4) Si es red/auth, reproduce con una sola descarga

  • Intenta hacer pull de la imagen base con las mismas credenciales y configuración de red.
  • Intenta hacer curl al endpoint del repositorio de paquetes desde el runner (no desde tu estación de trabajo).

5) Si es un paso RUN, redúcelo

  • Haz visible el comando RUN que falla (no uses mega-líneas con “&& … && …” para depurar).
  • Temporalmente elimina flags de paralelismo y añade salida verbosa.

6) Si es relacionado con caché, pruébalo

  • Vuelve a ejecutar con --no-cache o limpia cachés para confirmar que no persigues un estado obsoleto.
  • Después corrige las claves de caché, no los síntomas.

Broma #1: Trata “failed to solve” como una rabieta de niño—algo real pasó antes, y ahora todos solo gritan por ello.

Categorías de solución instantánea (y por qué ocurren)

Categoria A: errores de sintaxis de Dockerfile y del parser

Estos son agradables. Fallan rápido y de forma consistente. Verás errores como:

  • failed to parse Dockerfile
  • unknown instruction
  • invalid reference format

La solución casi siempre es un typo, una \ faltante, comillas incorrectas o usar una característica que tu builder no soporta. Si copias fragmentos de blogs, asume que están mal hasta probarlos.

Categoria B: problemas del contexto de build (archivos faltantes, rutas incorrectas, .dockerignore)

Esta es la “solución instantánea” más común y la más fácil de diagnosticar mal porque la gente confunde la estructura del repositorio con el contexto de build. El contexto es el directorio que pasas a docker build. BuildKit solo puede ver archivos bajo ese directorio (menos los patrones ignorados). Si tu Dockerfile dice:

  • COPY ../secrets /secrets (no)
  • COPY build/output/app /app (tal vez, si existe en tiempo de build)
  • COPY . /src (funciona, pero a menudo es una mala idea)

Entonces la corrección del contexto decide tu destino. Un pequeño error en .dockerignore puede convertirse en “checksum of ref … not found”, “failed to compute cache key” o el clásico “failed to walk … no such file or directory”.

Categoria C: fallos de permisos y propiedad

BuildKit ejecuta pasos con usuarios específicos, montajes y comportamientos readonly. Entre Docker rootless, runners de CI endurecidos y directorios NFS corporativos, puedes encontrar:

  • permission denied al leer archivos del contexto
  • operation not permitted al hacer chmod/chown
  • failed to create shim task cuando colisionan namespaces de usuario

Las soluciones van desde lo mundano (chmod a un archivo) hasta lo estructural (dejar de intentar chown 200k archivos durante el build).

Categoria D: red, DNS, proxies y MITM corporativo

Las builds de Docker descargan imágenes base y paquetes. Eso implica DNS, TLS, proxies y timeouts. BuildKit además hace fetches en paralelo, lo que puede empeorar una red inestable. Verás:

  • failed to fetch oauth token
  • i/o timeout
  • x509: certificate signed by unknown authority
  • temporary failure in name resolution

Esto se arregla a menudo importando el CA corporativo, configurando correctamente las variables de proxy o usando un mirror de registry que sea accesible desde el runner.

Categoria E: autenticación en registries y límites de tasa

Los pulls no autenticados alcanzan límites de tasa. Credenciales incorrectas provocan 401/403. Registries privados con tokens expirados te golpean en el peor momento. BuildKit a veces envuelve esto en un “failed to solve” genérico, pero el error subyacente suele ser explícito si haces scroll.

Categoria F: incompatibilidad de plataforma y arquitectura

Las builds multi-arch son geniales hasta que no lo son. Si tu imagen base no soporta la plataforma objetivo, verás errores como:

  • no match for platform in manifest
  • exec format error

La solución: especifica --platform, elige una base multi-arch o deja de construir imágenes amd64 en un runner arm64 sin emulación configurada.

Categoria G: rarezas de caché/solver (checksum, clave de caché, invalidación)

BuildKit es agresivo con el caching, lo cual es bueno hasta que tu build depende de algo que no debería cachearse. Los errores pueden incluir:

  • failed to compute cache key
  • failed to calculate checksum of ref
  • rpc error: code = Unknown desc = …

Esto suele significar “el archivo que BuildKit espera no está en el contexto” o “tu estado de caché está corrupto” o “tu builder remoto perdió un montaje”. No es místico; es solo estado.

Categoria H: secretos, montajes SSH y credenciales en tiempo de build

Los builds modernos usan RUN --mount=type=secret y RUN --mount=type=ssh. Genial. Pero si CI no pasa el secreto/agent SSH, BuildKit puede fallar con:

  • secret not found
  • ssh agent requested but no SSH_AUTH_SOCK

Solución: pasa el secreto o deja de pretender que tu build tiene acceso a cosas privadas sin conectarlas correctamente.

Tareas prácticas: comandos, salidas, decisiones

Estos son chequeos reales que puedes ejecutar en una máquina de desarrollador o en un runner de CI. Cada uno incluye: el comando, qué significa la salida y qué decisión tomar a continuación. Ejecútalos en el orden que corresponda a tu categoría de fallo.

Task 1: Confirma si BuildKit está en juego

cr0x@server:~$ docker build --help | head -n 5
Usage:  docker build [OPTIONS] PATH | URL | -

Build an image from a Dockerfile

Qué significa: La salida de help por sí sola no te dirá BuildKit, pero confirma que usas la entrada CLI clásica.

Decisión: A continuación, verifica explícitamente el entorno y el estado del builder.

cr0x@server:~$ echo $DOCKER_BUILDKIT

Qué significa: Vacío usualmente significa “default”. En muchas instalaciones modernas, el valor por defecto es BuildKit igualmente.

Decisión: Revisa buildx builders para ver qué backend estás usando realmente.

Task 2: Inspecciona buildx y el builder activo

cr0x@server:~$ docker buildx ls
NAME/NODE       DRIVER/ENDPOINT             STATUS    BUILDKIT   PLATFORMS
default         docker                       running   v0.12.5    linux/amd64,linux/arm64
ci-builder *    docker-container            running   v0.12.5    linux/amd64

Qué significa: Estás usando un builder en contenedor (docker-container) llamado ci-builder. Ese builder tiene su propia red, DNS y almacenamiento de caché.

Decisión: Si las fallas parecen DNS/timeouts o archivos faltantes, reproduce usando el mismo builder, no el default.

Task 3: Re-ejecuta con progreso plano para encontrar la línea que falla

cr0x@server:~$ docker buildx build --progress=plain -t testimg:debug .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.27kB done
#2 [internal] load metadata for docker.io/library/alpine:3.19
#2 ERROR: failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/3.19": dial tcp: lookup registry-1.docker.io: temporary failure in name resolution
------
 > [internal] load metadata for docker.io/library/alpine:3.19:
------
failed to solve: failed to do request: Head "https://registry-1.docker.io/v2/library/alpine/manifests/3.19": dial tcp: lookup registry-1.docker.io: temporary failure in name resolution

Qué significa: No es un problema del Dockerfile. El DNS desde el builder está roto.

Decisión: Pasa a chequeos de red/DNS. No toques el Dockerfile todavía; solo crearás nuevos bugs.

Task 4: Verifica el tamaño del contexto de build (contextos grandes causan “failed to solve” lentos)

cr0x@server:~$ docker buildx build --progress=plain --no-cache -t testimg:ctx .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.10kB done
#2 [internal] load .dockerignore
#2 transferring context: 2B done
#3 [internal] load build context
#3 transferring context: 1.48GB 34.2s done

Qué significa: Estás enviando 1.48GB de contexto al builder. Eso no es “un poco grande”. Eso es “¿por qué tu repo git también es un servidor de archivos?”

Decisión: Arregla .dockerignore y reduce el alcance de COPY. Contextos grandes causan timeouts, thrash de caché y CI lento.

Task 5: Muestra qué se está ignorando por .dockerignore (chequeo práctico)

cr0x@server:~$ sed -n '1,120p' .dockerignore
.git
node_modules
dist
*.log

Qué significa: Parece razonable, pero tal vez olvidaste build/, target/, .venv/ o datos de prueba grandes.

Decisión: Añade ignores para artefactos generados y caches locales; asegúrate de que el build aún tenga lo que necesita.

Task 6: Detecta “COPY failed: file not found” antes de perder tiempo

cr0x@server:~$ grep -nE '^(COPY|ADD) ' Dockerfile
12:COPY build/output/app /app/app
13:COPY configs/prod.yaml /app/config.yaml

Qué significa: El Dockerfile espera que build/output/app exista en el contexto de build en tiempo de build.

Decisión: Si CI no genera esa ruta antes de docker build, la build fallará. O bien genera los artefactos en un paso previo de CI o haz el build dentro del Dockerfile (multi-stage).

Task 7: Confirma que los archivos referenciados existen en el contexto

cr0x@server:~$ ls -la build/output/app configs/prod.yaml
ls: cannot access 'build/output/app': No such file or directory
-rw-r--r-- 1 cr0x cr0x  842 Jan  2 09:41 configs/prod.yaml

Qué significa: Un COPY fallará inmediatamente.

Decisión: Decide si (a) producir el artefacto antes del docker build, o (b) convertirlo a un build multi-stage que compile dentro de la imagen.

Task 8: Valida pull de imagen base y autenticación (separado del build)

cr0x@server:~$ docker pull alpine:3.19
3.19: Pulling from library/alpine
Digest: sha256:de0eb0b3f2a47ba1b5d1c61c87a1f1d9b5a4f34b3a4a1d7f3d6c2c6a7f1d2d3c
Status: Image is up to date for alpine:3.19
docker.io/library/alpine:3.19

Qué significa: El acceso al registry está OK desde el host Docker. Si buildx usa un builder remoto, ese builder aún puede estar roto—no declares victoria todavía.

Decisión: Si el build sigue fallando al descargar la misma imagen, inspecciona la red o la configuración de proxy del builder.

Task 9: Revisa DNS y conectividad saliente desde el contenedor del builder

cr0x@server:~$ docker ps --filter name=buildx_buildkit --format '{{.ID}} {{.Names}}'
a1b2c3d4e5f6 buildx_buildkit_ci-builder0
cr0x@server:~$ docker exec -it buildx_buildkit_ci-builder0 sh -lc 'cat /etc/resolv.conf && nslookup registry-1.docker.io 2>/dev/null | head -n 5'
nameserver 127.0.0.11
options ndots:0

Server:    127.0.0.11
Address:   127.0.0.11:53

Qué significa: El builder usa el DNS embebido de Docker (127.0.0.11). Si nslookup cuelga o falla, probablemente tienes un problema de configuración del daemon DNS o el puerto 53 está bloqueado en salida UDP/TCP.

Decisión: Arregla la configuración DNS del daemon de Docker o proporciona servidores DNS explícitos para el entorno del builder.

Task 10: Identifica desajuste de proxy (modo clásico corporativo)

cr0x@server:~$ env | grep -iE 'http_proxy|https_proxy|no_proxy'
HTTP_PROXY=http://proxy.corp:8080
HTTPS_PROXY=http://proxy.corp:8080
NO_PROXY=localhost,127.0.0.1,.corp

Qué significa: El proxy está definido en tu shell. El contenedor del builder puede no heredarlo automáticamente dependiendo de cómo se creó.

Decisión: Pasa proxy build args o configura el builder/daemon para usar el proxy de forma consistente. Si TLS está interceptado, también necesitarás el CA corporativo dentro de la imagen de build.

Task 11: Detecta desajuste de plataforma rápidamente

cr0x@server:~$ docker buildx imagetools inspect alpine:3.19 | sed -n '1,40p'
Name:      docker.io/library/alpine:3.19
MediaType: application/vnd.oci.image.index.v1+json
Digest:    sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa

Manifests:
  Name:      docker.io/library/alpine:3.19@sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
  Platform:  linux/amd64
  Name:      docker.io/library/alpine:3.19@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc
  Platform:  linux/arm64

Qué significa: Esta imagen base es multi-arch. Bien. Si tu imagen base muestra solo una plataforma y tú estás construyendo para otra, has encontrado la causa raíz.

Decisión: Elige una imagen base con la plataforma objetivo o establece --platform explícitamente y asegúrate de que la emulación esté configurada si es necesaria.

Task 12: Forza un build limpio para descartar corrupción de caché

cr0x@server:~$ docker buildx build --no-cache --progress=plain -t testimg:nocache .
#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 1.27kB done
#2 [internal] load metadata for docker.io/library/alpine:3.19
#2 DONE 0.9s
#3 [internal] load build context
#3 transferring context: 52.4MB 1.1s done
#4 [1/6] FROM docker.io/library/alpine:3.19@sha256:bbbb...
#4 DONE 0.0s
#5 [2/6] RUN apk add --no-cache ca-certificates
#5 DONE 2.8s

Qué significa: Si un build limpio tiene éxito pero las builds cacheadas fallan intermitentemente, tienes problemas de caché/estado (a menudo presión de disco en el builder remoto o registros de caché corruptos).

Decisión: Limpia el almacenamiento del builder y ajusta la estrategia de export/import de caché en lugar de “simplemente deshabilitar caché para siempre”.

Task 13: Revisa la presión de disco del builder (el asesino silencioso)

cr0x@server:~$ docker exec -it buildx_buildkit_ci-builder0 sh -lc 'df -h /var/lib/buildkit | tail -n 1'
overlay          20G   19G  1.0G  95% /

Qué significa: 95% lleno en el filesystem del builder. Espera errores raros: fallos de desempaquetado, “no space left”, errores aleatorios de caché y ralentizaciones espectaculares.

Decisión: Prune del cache de build, expande el almacenamiento o deja de exportar caches enormes que nunca reutilizas.

Task 14: Prunea la caché de BuildKit de forma segura (e interpreta la salida)

cr0x@server:~$ docker buildx prune -f --verbose
ID                                              RECLAIMABLE     SIZE        LAST ACCESSED
v1:9n8m7l6k5j4h3g2f1d0s                          true            1.2GB       2 days ago
v1:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa          true            650MB       7 days ago
Total: 1.85GB reclaimed

Qué significa: Recuperaste 1.85GB. Si tu builder estaba al 95%, esto puede convertir una build fallida en una build que funciona al instante.

Decisión: Si el prune lo arregla, implementa políticas automáticas de retención de caché y monitoriza el uso de disco del builder. Si no, sigue investigando.

Task 15: Diagnostica “secret not found” y arregla el cableado

cr0x@server:~$ docker buildx build --progress=plain --secret id=npmrc,src=$HOME/.npmrc -t testimg:secrets .
#7 [4/6] RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
#7 DONE 18.9s

Qué significa: Tu montaje de secreto está presente y el paso tuvo éxito. Si omites --secret, BuildKit falla el paso en tiempo de ejecución.

Decisión: En CI, asegúrate de que el secreto exista, tenga el scope correcto y sea pasado por el comando de build. No lo incorpores en la imagen. Nunca.

Task 16: Haz que un RUN fallido sea depurable

cr0x@server:~$ docker buildx build --progress=plain --build-arg DEBUG=1 -t testimg:debugrun .
#9 [5/6] RUN set -eux; apk add --no-cache git; git --version
+ apk add --no-cache git
(1/4) Installing ca-certificates (20241121-r0)
(2/4) Installing libcurl (8.6.0-r0)
(3/4) Installing pcre2 (10.42-r2)
(4/4) Installing git (2.45.2-r0)
OK: 26 MiB in 24 packages
+ git --version
git version 2.45.2

Qué significa: set -eux muestra el comando exacto que falla y se detiene en el primer error. Así dejas de adivinar.

Decisión: Mantén el patrón de “modo debug” en tu Dockerfile usando un build arg para poder activarlo cuando CI falle.

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

1) “failed to compute cache key” cerca de un COPY/ADD

Síntomas: La build falla en una línea COPY con lenguaje de cache-key/checksum; a veces menciona “not found”.

Causa raíz: La ruta de origen no existe en el contexto de build (o está ignorada por .dockerignore), así que BuildKit no puede hashearla para calcular keys de caché.

Solución: Asegura que el archivo exista antes del build, ajusta el directorio de contexto o actualiza .dockerignore. Prefiere copiar solo los archivos necesarios, no COPY . ..

2) “COPY failed: stat … no such file or directory”

Síntomas: Error directo de archivo faltante.

Causa raíz: Ruta relativa incorrecta, contexto equivocado o CI hace build desde un working directory diferente al que asumiste.

Solución: Usa claridad absoluta: ejecuta docker build -f path/to/Dockerfile path/to/context. En CI, imprime pwd y ls antes de construir. Haz las rutas aburridas.

3) “failed to do request: Head … temporary failure in name resolution”

Síntomas: Fallo al obtener metadata de la imagen base.

Causa raíz: DNS roto dentro del builder o salida bloqueada.

Solución: Configura el DNS del daemon, arregla la política de red del runner o ejecuta el builder con servidores DNS conocidos. Valida desde dentro del contenedor del builder.

4) “x509: certificate signed by unknown authority” durante instalación de paquetes o pull

Síntomas: Fallos TLS al golpear registries o mirrors de paquetes, especialmente en entornos corporativos.

Causa raíz: Intercepción TLS / CA corporativa no confiada en la imagen base o en el builder.

Solución: Instala la CA corporativa en la etapa de build (y preferiblemente en una imagen base compartida). No desactives la verificación TLS como “solución” a menos que disfrutes de incidentes.

5) “failed to fetch oauth token” o 401/403 al descargar imágenes base

Síntomas: Errores de auth en registries, a menudo intermitentes cuando tokens expiran.

Causa raíz: Falta de docker login en CI, helper de credenciales equivocado, tokens expirados o pull a un registry privado sin pasar credenciales al builder remoto.

Solución: Haz login antes del build; para builders remotos buildx, asegúrate de que las credenciales estén disponibles en ese contexto. Confirma con un docker pull directo desde el mismo entorno.

6) “no match for platform in manifest”

Síntomas: El build falla temprano al resolver la imagen base.

Causa raíz: La imagen base no publica para tu arquitectura objetivo.

Solución: Usa una imagen base multi-arch, fija la plataforma correcta o ajusta tus runners. Si construyes en arm64 pero distribuyes amd64, sé explícito.

7) “exec format error” durante RUN

Síntomas: La imagen base se descarga, pero ejecutar binarios falla.

Causa raíz: Desajuste de arquitectura en tiempo de ejecución (p. ej., binario amd64 en imagen arm64) o QEMU/emulación no configurada.

Solución: Alinea imagen base + binarios + plataforma. Si usas emulación, asegúrate de que binfmt/qemu esté configurado en el host que ejecuta el builder.

8) “no space left on device” (a veces disfrazado de errores de unpack)

Síntomas: Fallos aleatorios al desempaquetar capas, escribir caché o exportar imágenes.

Causa raíz: Almacenamiento del builder lleno (común con driver docker-container de buildx), agotamiento de inodos o límites de overlayfs.

Solución: Prunea cachés, amplia disco y deja de copiar directorios gigantes temprano en el Dockerfile (se multiplican en capas almacenadas).

9) “failed to create LLB definition” o errores RPC raros

Síntomas: BuildKit reporta errores gRPC/rpc con “unknown desc.”

Causa raíz: Inestabilidad del builder, desajuste de versiones, estado corrupto o presión de recursos (disco, memoria).

Solución: Reinicia el builder, actualiza BuildKit/buildx, verifica recursos y reproduce con --progress=plain. Si desaparece después de prunar, era presión de estado.

10) “secret not found” / fallos en montajes SSH

Síntomas: Un paso RUN que espera un secreto/agent SSH falla inmediatamente.

Causa raíz: El comando de build no pasó --secret o --ssh, o CI no expuso el secreto.

Solución: Conéctalo correctamente. Si no puedes, cambia el diseño: descarga dependencias en otro paso o véndelas. No cometas secretos por frustración.

Tres mini-historias corporativas desde el terreno

Mini-historia 1: El incidente causado por una asunción equivocada

Un equipo migró sus builds de un runner GitLab self-hosted a una flota de runners gestionados. Mismo repo, mismo Dockerfile, mismos comandos. La primera build falló con failed to solve en un paso COPY. Los desarrolladores se encogieron de hombros: “El archivo está en el repo; no puede faltar.”

El archivo se generaba. Localmente, todos lo tenían porque su flujo de trabajo de desarrollo ejecutaba una herramienta que producía build/output/app. El runner antiguo también tenía un paso previo que ejecutaba la misma herramienta—pero vivía en una plantilla compartida que nadie recordaba usar. La nueva pipeline de CI fue “limpiada” para ser “más simple”, que es jerga corporativa para “borramos las partes aburridas que hacían que funcionara”.

La asunción equivocada fue sutil: asumieron que Docker build podía ver todo el estado del repo, incluidos elementos que existirían “cuando se ejecute el build”. Pero el contexto de Docker build es una instantánea del filesystem en el momento que invocas el build. Si el artefacto no está ahí, no importa cuán presente se sienta espiritualmente.

La solución fue formalizar la creación del artefacto: o un paso de CI que produzca el artefacto antes de construir la imagen, o un build multi-stage que compile dentro de una etapa builder. Eligieron multi-stage. Fue más lento al principio, luego más rápido tras ajustar la caché y lo más importante: determinista.

Lección del postmortem: cuando los builds fallan tras cambios de runner, asume que tu dependencia de entorno estaba sin documentar. Usualmente lo estaba.

Mini-historia 2: La “optimización” que salió mal

Otra organización tenía el objetivo de reducir tiempo de CI. Alguien notó que los builds Docker eran “lentos porque envían mucho contexto” y decidió ser ingenioso. Añadieron patrones agresivos a .dockerignore para excluir básicamente todo excepto el Dockerfile y un par de directorios. CI se volvió más rápido. Aplaudieron.

Luego un candidato a release falló con failed to compute cache key: failed to calculate checksum apuntando a un archivo en una línea COPY—uno que existía, pero ahora estaba ignorado. El equipo lo arregló quitando la exclusión de ese archivo. La siguiente build falló por otro archivo ignorado. Otro ajuste. Más fallos. Se convirtió en un juego del gato y el ratón porque las reglas de ignore se escribieron sin mapearlas al grafo real de dependencias del Dockerfile.

Lo que salió mal no fueron solo los fallos. Fue el tiempo gastado en redescubrir lo que la imagen realmente requería y el riesgo sutil de seguridad: los desarrolladores comenzaron a “arreglar” temporalmente copiando . para pasar las builds, enviando credenciales y basura de desarrollo a las imágenes. Así es como terminas con un .env en contenedores de producción y un equipo de cumplimiento en tu calendario.

La resolución final fue aburrida y correcta: reconstruyeron .dockerignore desde primeros principios. Listaron cada archivo copiado a la imagen, añadieron solo esos directorios al contexto y ajustaron el Dockerfile para copiar rutas específicas en un orden estable. El contexto de build se redujo sin romper dependencias.

Lección: optimizar el contexto de build sin entender las dependencias de COPY es como quitar pernos para aligerar un puente.

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

Un equipo de plataforma operaba un cluster dedicado de buildx. Nada glamuroso: driver container, almacenamiento persistente y una política estricta. Cada nodo builder exportaba métricas de uso de disco, uso de inodos y tamaño de caché. Además prunearon caches en un horario con una ventana de retención ajustada a su carga.

Un viernes, el build de un equipo de producto empezó a fallar con “failed to solve” esporádicos: a veces durante desempaquetado de capas, a veces durante export de caché. Los desarrolladores sospecharon “Docker es inestable” (una teoría atemporal) y empezaron a reejecutar jobs hasta conseguir un green build.

El equipo de plataforma miró los dashboards. Los discos del builder estaban acercándose al límite. No por un proyecto, sino porque varios equipos habían activado exportes de caché y nadie fijó límites. Cuando la presión de disco subió, BuildKit empezó a fallar de formas desordenadas—porque las fallas de almacenamiento rara vez son corteses.

Aumentaron el tamaño del volumen del builder y ajustaron la política de prune. Los builds se estabilizaron de inmediato. Sin heroísmos dramáticos, solo alguien que se preocupa por señales “aburridas” como uso de disco. El equipo de producto hizo el release. Fue tranquilo. Eso es lo mejor.

Lección: el disco del builder es infraestructura de producción. Trátalo como infraestructura de producción, o te tratará como hobby.

Listas de verificación / plan paso a paso

Paso a paso: de CI roja a una causa raíz confirmada

  1. Vuelve a ejecutar una vez con --progress=plain. Tu objetivo es claridad, no esperanza.
  2. Identifica la fase que falla: internal/context, metadata fetch, COPY/ADD, RUN, export.
  3. Revisa el contexto de build: verifica que los archivos referenciados existan; revisa .dockerignore; mide el tamaño del contexto.
  4. Comprueba la resolución de la imagen base: haz pull manual; inspecciona plataformas; valida la auth.
  5. Revisa el entorno del builder: qué buildx builder, tipo de driver, DNS/proxy, presión de disco.
  6. Reduce el comando RUN que falla: añade set -eux, elimina comandos encadenados, reintenta.
  7. Descarta corrupción de caché: ejecuta con --no-cache; si lo arregla, repara la estrategia de caché.
  8. Aplica la solución más pequeña que lo haga determinista. Determinista vence a ingenioso.

Lista: cómo reducir la probabilidad de fallos de Dockerfile la próxima semana

  • Mantén .dockerignore pequeño, explícito y revisado junto con cambios del Dockerfile.
  • Prefiere builds multi-stage para que la creación de artefactos sea parte del grafo de build, no una suposición externa.
  • Fija imágenes base a tags que controles (y considera digests para pipelines críticos).
  • Deja de usar COPY . . como estilo de vida. Copia directorios específicos.
  • Divide pasos RUN riesgosos. Un paso que descarga paquetes, otro que compila, otro que limpia.
  • Monitoriza disco del builder. Establece políticas de prune. Documenta la configuración del builder como si fuera una base de datos (porque se comporta como tal).
  • Maneja proxies y CAs corporativas explícitamente. Si los necesitas, codifícalos.
  • Usa montajes de secretos para credenciales; verifica que CI los pase; nunca hornees secretos en capas.

Broma #2: “Vamos a desactivar la caché para arreglar el build” es el equivalente en CI a desconectar el detector de humo para poder dormir.

Datos interesantes y contexto histórico

  • BuildKit empezó como un proyecto separado y luego se convirtió en el builder por defecto en muchas instalaciones de Docker porque el builder clásico no escalaba caching y concurrencia igual de bien.
  • El concepto “LLB” (Low-Level Build) es el formato interno de grafo de BuildKit; “solve” se refiere a calcular y ejecutar ese grafo.
  • El builder original de Docker ejecutaba pasos del Dockerfile secuencialmente; BuildKit introdujo más paralelismo y caching inteligente, lo que también cambia cómo se muestran las fallas.
  • .dockerignore existe porque los builds tempranos de Docker eran dolorosamente lentos cuando los usuarios enviaban sin querer repos completos (incluido .git) como contexto.
  • Los límites de tasa de registries se volvieron un problema operativo real a medida que el uso de CI explotó; muchas organizaciones lo descubrieron solo después de que las pipelines empezaron a fallar en horas pico.
  • Las imágenes multi-arquitectura se volvieron comunes cuando los servidores ARM y Apple Silicon crecieron; los errores de mismatch de plataforma aumentaron en consecuencia.
  • Docker rootless y runners endurecidos mejoraron la seguridad pero hicieron que las asunciones de permisos en Dockerfiles antiguos fallaran más frecuentemente.
  • El soporte de secretos en build mejoró significativamente con montajes de BuildKit, reduciendo la necesidad de hacks con ARG que filtran secretos en capas.
  • Los builds reproducibles se volvieron una expectativa más fuerte a medida que crecieron las preocupaciones sobre la cadena de suministro; “funciona si lo vuelves a ejecutar” dejó de ser aceptable.

FAQ

¿Por qué Docker dice “failed to solve” en lugar del error real?

Porque BuildKit reporta una falla de alto nivel para el grafo de build (“solve failed”) e incluye el error subyacente arriba. Usa --progress=plain para hacer visible el error real.

¿Es “failed to solve” siempre un problema del Dockerfile?

No. Con frecuencia es red/DNS, auth de registry, contexto de build o presión de disco del builder. Trata al Dockerfile como culpable solo después de haber demostrado que el entorno está sano.

¿Por qué funciona en mi portátil pero falla en CI?

Los runners de CI tienen políticas de red diferentes, proxies, credenciales, permisos de filesystem y a menudo una ruta de contexto distinta. Además, CI puede usar un builder buildx remoto con su propio entorno.

¿Cómo veo rápido qué paso falló realmente?

Ejecuta el build con docker buildx build --progress=plain y desplázate hasta el primer bloque ERROR. Ignora la línea final de envoltorio hasta que hayas leído el error real.

¿Cuál es la forma más rápida de detectar problemas de contexto?

Revisa .dockerignore, haz grep de las líneas COPY/ADD y verifica que esas rutas fuente existan bajo el directorio de contexto. Si tu transferencia de contexto es de cientos de MB, arréglalo también.

¿Debo fijar imágenes base por digest?

Si te importa la repetibilidad y el control de la cadena de suministro, sí—especialmente en pipelines de producción. Si necesitas actualizaciones de seguridad regulares, usa un proceso controlado para actualizar digests en lugar de tags flotantes.

¿Cómo arreglo proxies y CA corporativos durante builds?

Pasa la configuración de proxy de forma consistente al builder y a los pasos de build, e instala la CA corporativa en la imagen (y a veces en el entorno del builder). No desactives la verificación TLS como solución.

¿Cuál es la forma correcta de usar secretos durante docker build?

Usa montajes de secretos de BuildKit (--secret y RUN --mount=type=secret). Verifica que CI proporcione el secreto. Evita ARG/ENV para secretos porque se filtran en el historial y las capas.

¿Por qué los errores relacionados con caché se ven tan extraños?

Porque las cachés de BuildKit están dirigidas por contenido y ligadas al grafo de build. Cuando el contenido referenciado no está disponible (archivo ignorado, montaje faltante, caché corrupta), obtienes errores de checksum y cache-key.

¿Cuándo debo usar --no-cache?

Como herramienta diagnóstica. Si lo arregla, has probado que es un problema de estado/caché. Luego arregla la estrategia de caché o la salud del builder—no renuncies permanentemente al cache salvo que disfrutes de CI más lento.

Conclusión: pasos siguientes que realmente reducen el dolor

“Failed to solve” no es un mensaje. Es una etiqueta de categoría. Tu trabajo es desgranarla hasta el primer error concreto y decidir si estás lidiando con contexto, red, auth, plataforma, permisos, caché o runtime.

Haz estas tres cosas y tu yo del futuro te agradecerá (en silencio, por no despertarte a las 2 a.m.):

  1. Estandariza --progress=plain en los logs de CI (al menos en fallos) para que el error real sea visible.
  2. Haz el contexto determinista: rutas COPY explícitas, .dockerignore disciplinado y builds multi-stage para artefactos generados.
  3. Opera tus builders: monitoriza disco, prunea inteligentemente y trata la infraestructura de build como producción—porque está aguas arriba de producción.

La mayoría de los errores Dockerfile “failed to solve” se arreglan al instante una vez que dejas de mirar la última línea y empiezas a tratar los builds como sistemas: entradas, estado y dominios de fallo.

← Anterior
Estilos de formularios que sobreviven en producción: inputs, selects, checkboxes, radios, switches
Siguiente →
WireGuard vs IPsec en oficinas: qué es más fácil de mantener y trampas comunes

Deja un comentario