Guía10 min

Cache de build Docker para un agente IA: capas bien ordenadas antes de cache externo

Resumen

El build cache de Docker reutiliza capas cuando la instrucción y sus archivos no cambian. Orden de capas de menos a más frecuente, COPY del lockfile antes del código, cache mounts para pnpm/pip/go, docker builder prune y --cache-to/--cache-from para CI. Docs Docker build cache, 6 sep 2026.

Docker
Capas de build Docker ordenadas de menos a más frecuentes con un cache mount de paquetes

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.

El build cache de Docker guarda el resultado de cada instrucción del Dockerfile y lo reutiliza solo si la instrucción y los archivos que toca no cambiaron desde el último build. Para un agente IA que se despliega en contenedor —multi-stage, pnpm, un runtime en Fly o VPS—, el cache es la diferencia entre un push que builda en dos minutos y uno que reinstala todo el mundo de dependencias en cada corrida. Fuentes oficiales Docker consultadas el 6 de septiembre de 2026.

La regla base es cruel y simple: cuando una capa se invalida, todas las capas siguientes se re-construyen, aunque su resultado no cambiaría en absoluto. Un Dockerfile mal ordenado convierte un cambio de una línea en el código en una reinstalación completa de dependencias.

Cómo invalida el cache el builder

El builder revisa la imagen base y luego compara cada instrucción contra las capas cacheadas:

  • ADD, COPY y RUN --mount=type=bind calculan un checksum de los archivos involucrados. Si cambia el contenido (o los metadatos que sí cuentan), la capa se invalida.
  • RUN sin bind mount solo compara el string del comando. RUN pnpm install es cache hit eterno aunque el registry haya publicado diez releases nuevas: el cache no mira dentro del contenedor.
  • El mtime de los archivos copiados no invalida el cache. Copiar los mismos archivos con otra fecha es un hit.
  • Los build secrets no participan del checksum de contenido: cambiar el valor de un secret no invalida nada por sí solo (los IDs y rutas de mount del secret sí).
  • Cambiar SOURCE_DATE_EPOCH entre builds invalida WORKDIR y todo lo que sigue. Setearlo al timestamp del commit rompe el cache con cada commit.

La consecuencia práctica: RUN apk add curl congelado en cache no significa curl actualizado. Rebuild la semana siguiente y sigue el mismo paquete. Para forzar re-ejecución: cambiar algo en una capa anterior, docker builder prune, o --no-cache / --no-cache-filter <stage> para invalidar solo un stage concreto.

Orden de capas: de menos frecuente a más frecuente

Es la técnica de mayor retorno y no requiere nada extra. Los pasos caros y estables van arriba; lo que cambia seguido, abajo.

Dockerfile con el lockfile copiado antes del código fuente: el install sobrevive los cambios de código

El anti-patrón canónico:

FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

Cambiar una coma en el código invalida el COPY . ., y con él npm install vuelve a descargar todo. La versión cache-friendly divide el COPY:

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build

El pnpm install solo se re-corre cuando cambian el manifest o el lockfile. Para un agente con dependencias pesadas esto ahorra minutos por build.

Mantén el contexto pequeño

El build context es lo que el builder envía a procesar cada instrucción. Un contexto gordo hace builds lentos y aumenta la probabilidad de invalidaciones. El .dockerignore es la herramienta: excluye node_modules, tmp*, logs, builds locales y —sobre todo— el .env, que no va en la imagen ni en el contexto que el builder checksuma (detalle en dockerignore).

Cache mounts: el cache que sobrevive al rebuild

Las capas normales son exact-match: si el layer se invalida, su trabajo se tira. Un cache mount es una ubicación persistente que sobrevive al rebuild de la capa: aunque el install se re-ejecute, solo descarga los paquetes nuevos o cambiados.

Un cache mount persiste entre builds aunque la capa que lo usa se reconstruya

Para pnpm, el store es lo que se cacha:

# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
    corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build

El patrón es el mismo para otros runtimes que docs documenta: Go (/go/pkg/mod + /root/.cache/go-build), pip (/root/.cache/pip), Cargo (/app/target + /var/cache/cargo con CARGO_HOME para coordinar locks) y Composer (/tmp/cache). Apt necesita acceso exclusivo a sus datos: usa sharing=locked en los mounts de /var/cache/apt y /var/lib/apt para que builds paralelos se esperen en vez de corromper el cache.

Y una nota de límites: los cache mounts no se exportan con --cache-to en la mayoría de backends —en GitHub Actions en particular, BuildKit no los preserva—. El workaround documentado es reproducible-containers/buildkit-cache-dance. Úsalo solo si el cache mount te ahorra minutos reales; para la mayoría de agentes el cache de capas + lockfile primero es suficiente.

Cache externo para CI: --cache-to / --cache-from

En CI el builder es efímero: cada job arranca con cache vacío y build minutes caros. El cache interno de BuildKit no se comparte entre builders. La solución es exportar a una ubicación remota con docker buildx build:

  • --cache-to type=registry,ref=registry/app:buildcache exporta el cache como imagen aparte en el registry.
  • --cache-from type=registry,ref=registry/app:buildcache lo importa al inicio del build.
  • mode=max cachea todas las capas, incluidas las intermedias; el default mode=min solo las que llegan a la imagen final. max da más hits a costa de storage.
  • El backend type=gha guarda el cache en el GitHub Actions cache, pero solo funciona dentro de un workflow (los atributos url y token solo se pueblan en ese contexto) y requiere Buildx ≥ 0.21 / BuildKit ≥ 0.20 desde el apagón de la API v1 de abril 2025.
  • inline incrusta el cache en la imagen de salida: solo mode=min y solo con el exporter de imagen.

En GitHub Actions con docker/build-push-action el patrón registry queda así:

- name: Build and push
  uses: docker/build-push-action@v7
  with:
    push: true
    tags: user/app:latest
    cache-from: type=registry,ref=user/app:buildcache
    cache-to: type=registry,ref=user/app:buildcache,mode=max

Y un warning de docs que muerde: ninguna ubicación de cache se puede escribir dos veces sin sobrescribir. Cache por rama = refs distintos (:buildcache-<branch>). El patrón multi-cache —--cache-from de la rama actual y otro --cache-from de main— da hits cuando la rama es nueva.

El backend type=local con actions/cache sigue vivo como alternativa; pasa reset=true (Buildx ≥ 0.35) en --cache-to porque el directorio crece con cada corrida dejando blobs huérfanos.

Comparativa rápida

TécnicaQué cacheaSobrevive rebuild de capaDónde brillaTrampa
Orden de capasEl layer exactoNoTodo proyectoLockfile copiado tarde = reinstall eterno
.dockerignoreNo cachea: recorta el contextoN/AContextos gordosSin él, el .env entra al contexto
Cache mountsPaquetes del package managerInstalls pesados, Go/RustNo se exportan a GHA; apt necesita sharing=locked
--cache-to/from registryTodas las capas (mode=max)Sí, vía importCI efímeroEscribir 2× la misma ref sobrescribe
type=ghaTodas las capasSí, vía Actions cacheWorkflows GHRequiere Buildx ≥ 0.21; solo dentro de GH
docker builder pruneLimpia el cache internoDisco lleno, forzar refreshBorra todo el cache local

Checklist antes de culpar al cache

  • El primer COPY lleva manifest + lockfile, y el install corre antes de copiar el código.
  • .dockerignore excluye node_modules, builds locales, logs y .env.
  • El install pesado usa --mount=type=cache apuntando al store real del package manager (pnpm store, pip cache, GOMODCACHE).
  • apt con cache mount usa sharing=locked en /var/cache/apt y /var/lib/apt.
  • En CI, --cache-to y --cache-from apuntan a la misma ref del registry (o type=gha dentro de Actions). Cache por rama usa refs distintas con fallback al cache de main.
  • RUN apk add / apt install "actualizado" es una ilusión de cache: los paquetes se congelan hasta invalidar la capa a propósito.
  • Ningún secret viaja por ARG o COPY —los secrets no invalidan cache, pero COPY los incrusta en la capa para siempre.

Errores típicos

El install corre cada build pese al cache. Casi siempre es el COPY . . antes del install: cualquier cambio de código invalida el install. Divide el COPY: manifest primero, código después.

El cache mount de pnpm no acelera nada. El target debe ser el store real de pnpm; un target genérico /cache que el package manager nunca lee no cachea nada. Verifica con pnpm store path dentro del contenedor.

El cache de CI nunca da hits. Los builders de CI son efímeros: sin export/import explícito no hay cache que sobreviva. Revisa que --cache-from esté en el build actual, no solo --cache-to en el anterior. Si el workflow falla con el error de "legacy service", tus tools son viejos para la API v2 del cache de GitHub: Buildx ≥ 0.21, BuildKit ≥ 0.20, o usa setup-buildx-action.

Cambié el secret y el build sigue igual. Correcto: el contenido de secrets no participa del checksum. Invalida con un ARG CACHEBUST cuyo valor cambies junto con el secret, o con --no-cache-filter sobre el stage que lo usa.

El build es más lento con cache externo. mode=max en un monorepo gigante puede hacer el export más caro que los hits que trae. Prueba mode=min o invalida por stage con --no-cache-filter.

Relacionados

Siguiente paso: si el build ya es rápido y prod sigue lento, revisa límites de CPU y cold starts. Si el disco del builder se llena, docker builder prune es la válvula —con la regla de que borra el cache local completo.