La cache de build no es una copia opaca de una imagen anterior. BuildKit representa la construcción como un grafo de operaciones y reutiliza resultados cuando los inputs relevantes de una operación coinciden con un resultado disponible.
Texto
Dockerfile + context + args + mounts + base images
→ grafo de operaciones
→ claves de cache
→ resultados reutilizados o ejecutados
→ image final
La cache debe mejorar velocidad sin cambiar la corrección. Un build limpio y uno con cache deberían producir un artefacto funcional equivalente para los mismos inputs permitidos.
Construir una aplicación puede incluir operaciones costosas:
descargar imágenes base;
instalar paquetes del sistema;
descargar dependencias;
compilar TypeScript o binarios;
ejecutar tests;
generar assets;
empaquetar artefactos para varias plataformas.
Repetir todo ante cualquier cambio hace lentos CI, desarrollo y releases. La cache intenta reutilizar únicamente la parte del grafo cuyos inputs no cambiaron.
El reto no consiste en “activar cache”; consiste en diseñar instrucciones con fronteras de inputs correctas.
BuildKit es el builder moderno utilizado por Docker para ejecutar construcciones. Entre sus capacidades se encuentran:
resolver operaciones como un grafo;
ejecutar ramas independientes en paralelo;
transferir únicamente archivos necesarios;
usar cache local y remota;
cache mounts;
secret y SSH mounts;
multi-stage eficiente;
outputs y plataformas múltiples;
provenance y attestations según configuración.
La directiva:
docker
# syntax=docker/dockerfile:1
indica el frontend Dockerfile que interpreta la sintaxis. Fijar una variante puede ayudar a controlar capacidades; usar un frontend móvil puede introducir cambios. La política depende del entorno y debe probarse.
No existe una regla única resumida en “Docker compara la línea”. Según la operación, pueden influir:
instrucción normalizada;
contenido y metadata de archivos copiados;
imagen base y digest;
valores de build args utilizados;
environment efectivo;
mounts y opciones;
plataforma;
resultados de dependencias anteriores;
frontend y solver.
Para RUN, BuildKit no puede saber automáticamente que una URL externa o un repository de paquetes cambió si los inputs declarados permanecen iguales.
docker
RUN apt-get update && apt-get install -y curl
La red puede ofrecer paquetes nuevos, pero un resultado cacheado puede reutilizarse. La cache refleja inputs conocidos, no el estado completo de internet.
COPY package.json package-lock.json ./
RUN npm ci
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
Cada operación declara inputs más precisos.
No fragmentes por dogma. Cientos de instrucciones pequeñas pueden volver difícil leer y mantener el Dockerfile. El objetivo es alinear fronteras de cache con fronteras reales de cambio.
Los valores de secret mounts no deberían persistirse en la cache como contenido de layers. Sin embargo, el resultado del comando puede quedar cacheado según sus inputs y comportamiento.
docker
RUN --mount=type=secret,id=npmrc,target=/root/.npmrc npm ci
Cambiar el valor del secreto no necesariamente invalida el paso por sí solo. Si el secreto cambia qué dependencias puede descargar, puede necesitarse un input de invalidación no sensible, por ejemplo una versión de credencial o argumento de cache bust controlado.
Nunca escribas el secreto en el resultado para forzar invalidación.
ARG CACHE_BUST=1
RUN echo "$CACHE_BUST" >/dev/null && some-command
Puede forzar invalidación, pero debe usarse de forma específica. Cambiarlo en cada build destruye el valor de cache y puede ocultar una dependencia mal declarada.
En un multi-stage build, BuildKit puede omitir etapas que no participan en el target solicitado.
docker
FROM base AS lint
RUN npm run lint
FROM base AS test
RUN npm test
FROM base AS build
RUN npm run build
FROM runtime
COPY --from=build /app/dist ./dist
Al construir el target final, una etapa independiente de lint puede no ejecutarse si no forma parte del grafo. Si CI exige lint y tests, debe solicitarlos como targets o conectarlos explícitamente al workflow. No asumas que aparecer en el Dockerfile garantiza ejecución.
# syntax=docker/dockerfile:1
FROM node:22-slim AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci
FROM deps AS build
COPY tsconfig.json ./
COPY src ./src
RUN npm run build
FROM node:22-slim AS runtime-deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm,sharing=locked \
npm ci --omit=dev
FROM node:22-slim AS runtime
ENV NODE_ENV=production
WORKDIR /app
COPY --from=runtime-deps /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]
Binarios nativos para amd64 no deben reutilizarse como si fueran arm64. Incluye plataforma en la frontera o usa caches separadas cuando la herramienta no lo hace correctamente.