Acelerar builds de Docker: guía práctica para ir 10 veces más rápido con caché

Corregiste un typo, volviste a lanzar docker build y — npm install otra vez. Pasaron 10 minutos, revisaste 20 publicaciones en redes, te tomaste dos cafés, y la barra de progreso seguía girando.
Quien ha desarrollado con contenedores conoce esa frustración.
Investigué el mecanismo de caché de Docker y bajé el tiempo de build de 10 minutos a 30 segundos.
En este artículo veremos 3 trucos que puedes aplicar ya: configurar .dockerignore, entender la caché por capas y optimizar el orden de instrucciones del Dockerfile. Al final, un extra con la caché montada de BuildKit. ¿Tu build va lento? Sigue leyendo.
¿Por qué tu build de Docker es tan lento?
El contexto de build es demasiado grande
Empecemos por un error muy común: el contexto de build (Build Context).
Cuando ejecutas docker build ., lo primero que hace Docker no es ejecutar el Dockerfile, sino empaquetar todos los archivos del directorio . y enviarlos al daemon de Docker. Sí, todos: node_modules, la carpeta .git, cientos de MB de datos de prueba que hayas descargado.
Lo más extremo que he visto: un proyecto frontend con un contexto de 800 MB. Solo transferir esos archivos lleva 2-3 minutos. Y la imagen en realidad solo necesita menos de 10 MB de código fuente.
Es como enviar un libro por mensajería y empaquetar también la estantería.
Efecto dominó al invalidar la caché por capas
Segundo problema: no entender la caché por capas de Docker.
Las imágenes Docker son por capas. Cada instrucción del Dockerfile — FROM, RUN, COPY — crea una capa. Al construir, Docker comprueba si hay caché en cada capa. Si la instrucción es idéntica y los archivos dependientes no cambiaron, reutiliza la caché sin volver a ejecutar.
Suena bien, ¿verdad?
El problema: cuando una capa pierde la caché, todas las capas posteriores deben reconstruirse. Como fichas de dominó: cae la primera y el resto también.
Muchos Dockerfiles se escriben así:
FROM node:18
COPY . /app
WORKDIR /app
RUN npm install
¿Parece correcto? En realidad es un problema grave.
La línea COPY . /app copia todo el proyecto. Cambias cualquier archivo — aunque sea un typo en README.md — y esa capa pierde la caché. ¿Y después? npm install también se vuelve a ejecutar.
Por eso, al cambiar una línea de código, reinstalas todo el árbol de dependencias.
Orden de instrucciones poco adecuado
Tercer error: no saber cómo ordenar las instrucciones.
La estrategia de caché de Docker es simple: revisa de arriba a abajo y deja de usar caché en cuanto falla una capa. Eso significa poner instrucciones que cambian poco al principio y las que cambian mucho al final.
En la práctica muchos Dockerfiles hacen lo contrario: primero copian el código (cambia a menudo) y luego instalan dependencias (cambian poco). Resultado: cada cambio de código invalida la caché de dependencias.
En resumen: no distinguir qué cambia a menudo y qué es relativamente estable.
Truco 1: configurar .dockerignore para reducir el contexto de build
Bien, problemas expuestos. Empecemos por la optimización más simple e inmediata: .dockerignore.
¿Qué es?
¿Has usado .gitignore? .dockerignore hace lo mismo: le dice a Docker qué archivos no empaquetar en el contexto de build.
Es muy fácil: en la raíz del proyecto (junto al Dockerfile) crea un archivo .dockerignore y escribe las reglas.
¿Cómo configurarlo en un proyecto Node.js?
Aquí va la configuración que uso yo:
# Directorios de dependencias
**/node_modules/
**/npm-debug.log
**/.npm
# Git
.git/
.gitignore
.gitattributes
# Pruebas y documentación
**/test/
**/tests/
**/docs/
**/*.md
!README.md
# IDE y editores
.vscode/
.idea/
*.swp
*.swo
.DS_Store
# Variables de entorno y configuración
.env
.env.*
*.local
# Artefactos de build
dist/
build/
coverage/
Puntos clave:
- Excluye siempre node_modules. Puede pesar cientos de MB y en la imagen se reinstala; no hace falta copiarlo del host.
- Usa el prefijo
**/para directorios anidados. Por ejemplo**/node_modules/coincide con./node_modules/y./packages/lib/node_modules/. - Añade barra final a los directorios.
node_modules/es un directorio;node_moduleses un archivo. Docker distingue.
¿Qué tan visible es el efecto?
Lo probé en un proyecto Next.js:
- Antes: contexto de 520 MB, transferencia 2 min 15 s
- Después: contexto de 4,8 MB, transferencia 3 s
Sí, 3 segundos. Ahorras 2 minutos de golpe.
Y además la imagen pesa menos — sin empaquetar .git ni node_modules, la imagen final pasó de 1,2 GB a 680 MB.
Trampas habituales
Trampa 1: .dockerignore solo aplica en la raíz del contexto de build. Si construyes con docker build -f subfolder/Dockerfile ., el .dockerignore va en la raíz del proyecto, no en subfolder.
Trampa 2: escribir node_modules (sin barra) puede no funcionar. Usa node_modules/.
Trampa 3: olvidar excluir .git. Esa carpeta suele pesar cientos de MB y nunca se usa en la imagen.
Truco 2: entender y aprovechar la caché por capas de Docker
.dockerignore mejora la transferencia; el núcleo sigue siendo entender cómo funciona la caché.
¿Qué es la caché por capas?
Una imagen Docker es como un pastel de capas: cada capa es el resultado de una instrucción del Dockerfile.
Por ejemplo:
FROM node:18 # Capa 1
RUN apt-get update # Capa 2
COPY package.json . # Capa 3
RUN npm install # Capa 4
COPY . . # Capa 5
Al construir, Docker revisa capa a capa:
- Capa 1: instrucción FROM; ¿hay imagen node:18 local? Sí → caché.
- Capa 2: instrucción RUN; ¿el texto del comando es igual? Sí → caché.
- Capa 3: instrucción COPY; calcula el checksum de package.json. ¿El archivo no cambió? Caché.
- Capa 4: instrucción RUN; sigue comprobando.
- Capa 5: igual.
Clave: cuando una capa pierde la caché, todas las siguientes se reconstruyen.
Ese es el efecto dominó. Si cambias package.json en la capa 3, npm install de la capa 4 y la copia de código de la capa 5 se vuelven a ejecutar.
¿Cómo saber si la caché se aplicó?
Mira la salida del build:
Step 3/5 : COPY package.json .
---> Using cache
---> 3a8f29e7c5b1
Si ves Using cache, se usó la caché. Si no, se está reconstruyendo.
También puedes usar docker history <image-id> para ver el historial de capas; el SIZE y la fecha de cada capa quedan claros.
¿Por qué COPY es especial?
RUN solo mira el texto del comando. RUN npm install — si el texto no cambia, Docker asume que puede usar caché.
COPY y ADD son distintos. Docker calcula el checksum del contenido copiado; aunque el nombre del archivo no cambie, si el contenido cambia, la caché falla.
Es un diseño acertado: si el archivo cambia, los pasos posteriores pueden verse afectados y no conviene reutilizar caché antigua.
Por eso COPY . . es peligroso: cualquier cambio en el proyecto (incluso en README.md) invalida esa capa.
Truco 3: optimizar el orden de instrucciones del Dockerfile
Entendida la caché, toca la práctica: ¿cómo escribir el Dockerfile para maximizarla?
Regla de oro: de lo estable a lo volátil
En una frase: instrucciones que cambian poco al principio, las que cambian mucho al final.
¿Por qué? Docker revisa la caché de arriba a abajo. Si las capas iniciales son estables, los cambios posteriores no las invalidan.
En concreto:
- Imagen base — casi nunca cambia
- Dependencias del sistema — cambian de vez en cuando
- Dependencias del proyecto — a veces cambian
- Código fuente — cambia a diario
Ordenando así, maximizas el uso de caché.
Ejemplo incorrecto: copiar código antes de instalar dependencias
Muchos empiezan así:
FROM node:18
WORKDIR /app
# Error: copiar todo el proyecto de golpe
COPY . .
# Luego instalar dependencias
RUN npm install
# Comando de arranque
CMD ["npm", "start"]
¿El problema? Cambias el código fuente, falla la caché de COPY . . y npm install también se vuelve a ejecutar.
Resultado: cambias una línea de JS y reinstalas cientos de paquetes npm. Otros 10 minutos perdidos.
Ejemplo correcto: dependencias primero, código después
Versión optimizada:
FROM node:18
WORKDIR /app
# Paso 1: copiar solo archivos de dependencias
COPY package.json package-lock.json ./
# Paso 2: instalar dependencias (esta capa se cachea)
RUN npm ci --only=production
# Paso 3: copiar código fuente
COPY . .
# Comando de arranque
CMD ["npm", "start"]
Ventajas:
- Mientras package.json no cambie, la capa de
npm ciusa caché. - Si cambias el código, solo falla
COPY . .; la instalación de dependencias sigue en caché. - En el segundo build saltas npm install y el build vuela.
En mis pruebas, este cambio bajó builds posteriores de 7-8 minutos a unos 30 segundos.
Lo mismo en otros lenguajes
Proyecto Python:
FROM python:3.11
WORKDIR /app
# Primero requirements.txt
COPY requirements.txt .
# Luego pip install
RUN pip install --no-cache-dir -r requirements.txt
# Por último el código
COPY . .
Proyecto Go:
FROM golang:1.21
WORKDIR /app
# Primero go.mod y go.sum
COPY go.mod go.sum ./
# Descargar dependencias
RUN go mod download
# Luego el código
COPY . .
# Compilar
RUN go build -o main .
La idea es la misma: separar archivos de gestión de dependencias y código fuente para reutilizar caché en la instalación.
Avanzado: COPY granular
Si la estructura es compleja, puedes ser más fino:
# Primero archivos de configuración que cambian poco
COPY .eslintrc.json .prettierrc ./
# Luego archivos de dependencias
COPY package*.json ./
RUN npm install
# Después librerías compartidas (si las hay)
COPY ./lib ./lib
# Por último código de negocio
COPY ./src ./src
Es menos habitual, pero en algunos escenarios (por ejemplo monorepos) ayuda mucho.
Avanzado: caché montada de BuildKit
Hasta aquí optimizaciones básicas. Ahora algo más avanzado: la caché montada de BuildKit.
¿Qué es BuildKit?
BuildKit es el motor de build nuevo que Docker introdujo en la 18.09; es más rápido que el antiguo y soporta funciones de caché más potentes.
Activarlo es sencillo:
# Activar temporalmente
export DOCKER_BUILDKIT=1
docker build .
# O directamente en el comando
DOCKER_BUILDKIT=1 docker build .
Si tu Docker es reciente (19.03+), BuildKit suele estar activo por defecto. Si no estás seguro, ejecuta docker version.
¿Qué es la caché montada?
La caché por capas tiene un límite: cuando una capa falla, hay que ejecutarla por completo.
Por ejemplo, cambias package.json y añades una dependencia: la capa de npm install pierde caché y todos los paquetes — incluso los ya descargados — se vuelven a bajar.
La caché montada resuelve eso: aunque falle la caché por capas, conserva la caché de descargas del gestor de paquetes.
En la práctica: un directorio de caché persistente que el gestor comparte entre builds.
¿Cómo usarla?
Ejemplo en Node.js:
FROM node:18
WORKDIR /app
COPY package*.json ./
# Aquí está la clave: montar el directorio de caché de npm
RUN --mount=type=cache,target=/root/.npm \
npm ci --only=production
COPY . .
CMD ["npm", "start"]
Lo importante es --mount=type=cache,target=/root/.npm:
type=cacheindica montaje de cachétarget=/root/.npmes el directorio de caché de npm
Con esto, aunque cambie package.json y falle la caché por capas, npm no descarga todo desde cero. Lee la caché en /root/.npm y solo baja paquetes nuevos o actualizados.
Otros gestores de paquetes
Yarn:
RUN --mount=type=cache,target=/root/.yarn \
yarn install --frozen-lockfile
pip (Python):
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
apt (paquetes del sistema):
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
apt-get update && apt-get install -y gcc
En apt, el parámetro sharing=locked es importante: apt necesita acceso exclusivo a su caché para evitar conflictos en builds concurrentes.
¿Qué resultados da?
Lo probé en un proyecto con más de 200 dependencias:
- Caché por capas fallida pero caché montada activa: instalación de 8 minutos a 1 min 30 s
- Arranque en frío (sin ninguna caché): sigue siendo 8 minutos
La caché montada es la segunda línea de defensa. Si la caché por capas sigue válida, es lo más rápido (se salta el paso). Si falla, la caché montada evita descargar todo de nuevo.
Notas
-
Retención por defecto limitada: BuildKit limpia cachés de más de 2 días y más de 512 MB. En CI/CD puede que necesites ajustar la política.
-
No siempre hace falta: si el proyecto tiene pocas dependencias (una docena de paquetes), la diferencia es pequeña.
-
Rutas correctas: cada gestor usa un directorio de caché distinto; consulta la documentación.
Conclusión
En resumen, tres cosas:
Primero, hazlo ya: crea .dockerignore en la raíz del proyecto y excluye node_modules, .git y archivos de prueba. No lleva 5 minutos y el contexto puede reducirse más del 90%.
Segundo, cámbialo hoy: reordena el Dockerfile. Primero COPY de dependencias, luego RUN de instalación, por último COPY del código. Ese ajuste puede bajar builds posteriores de 10 minutos a 30 segundos.
Tercero, cuando puedas: si hay muchas dependencias y cambian a menudo, prueba la caché montada de BuildKit; te salva cuando falla la caché por capas.
En mi proyecto, tras estos tres pasos el build pasó de 10 minutos a 30 segundos y la imagen de 1,2 GB a 680 MB. Es una de las optimizaciones con mejor retorno que he visto.
¿Tu build de Docker va lento? Prueba lo de este artículo. Cuando lo tengas, cuéntanos en los comentarios cuánto lo aceleraste; tengo curiosidad.
Flujo completo de optimización de builds de Docker
Pasa de 10 minutos a 30 segundos dominando caché por capas, .dockerignore y optimización del Dockerfile
Estimated time: PT30M
-
1
Step 1: Entender por qué el build es lento: contexto y caché por capas
Problema del contexto de build: -
2
Step 2: Truco 1: configurar .dockerignore para reducir el contexto
Hazlo ya: crea .dockerignore en la raíz y excluye node_modules, .git y archivos de prueba; no lleva 5 minutos y el contexto puede reducirse más del 90%. -
3
Step 3: Truco 2: optimizar el orden de instrucciones del Dockerfile
Cámbialo hoy: primero COPY de dependencias, luego RUN de instalación, por último COPY del código; los builds posteriores pueden pasar de 10 minutos a 30 segundos. -
4
Step 4: Truco 3: usar caché montada de BuildKit
Cuando puedas: si hay muchas dependencias y cambian a menudo, prueba la caché montada de BuildKit; te salva cuando falla la caché por capas.
FAQ
¿Por qué los builds de Docker son tan lentos?
• Cuando ejecutas docker build ., lo primero que hace Docker es empaquetar todos los archivos del directorio . y enviarlos al daemon de Docker
• Incluye node_modules, la carpeta .git, datos de prueba, etc.
• El contexto de build de un proyecto frontend puede llegar a 800 MB; solo transferir esos archivos lleva 2-3 minutos
• Y en realidad la imagen solo necesita menos de 10 MB de código fuente
• Es como enviar un libro por mensajería y empaquetar también la estantería
Efecto dominó al invalidar la caché por capas:
• Las imágenes Docker son por capas; cada instrucción del Dockerfile (FROM, RUN, COPY) crea una capa
• Al construir, Docker comprueba si hay caché disponible en cada capa; si la instrucción es idéntica y los archivos dependientes no cambiaron, reutiliza la caché
• El problema: cuando una capa pierde la caché, todas las capas posteriores deben reconstruirse, como fichas de dominó
Muchos Dockerfiles se escriben así: FROM node:18, COPY . /app, WORKDIR /app, RUN npm install; así, cada cambio de código obliga a volver a ejecutar npm install.
¿Cómo configurar .dockerignore para reducir el contexto de build?
Ejemplo de .dockerignore:
• node_modules (excluir dependencias)
• .git (excluir historial de Git)
• *.log (excluir logs)
• .env (excluir variables de entorno)
• dist (excluir artefactos de build)
• test (excluir archivos de prueba)
• *.md (excluir documentación)
Tras configurarlo, el contexto pasa de 800 MB a menos de 10 MB y la transferencia de 2-3 minutos a unos segundos.
¿Cómo optimizar el orden de instrucciones del Dockerfile?
Principio de optimización:
• Instrucciones que cambian poco al principio (FROM, dependencias del sistema, dependencias de la app)
• Instrucciones que cambian mucho al final (COPY del código fuente)
Antes:
• FROM node:18
• COPY . /app
• WORKDIR /app
• RUN npm install
• Cada cambio de código vuelve a ejecutar npm install
Después:
• FROM node:18
• WORKDIR /app
• COPY package*.json ./
• RUN npm install
• COPY . .
• npm install solo se vuelve a ejecutar cuando cambia package.json; los cambios de código no afectan la instalación de dependencias
¿Cómo usar la caché montada de BuildKit?
Caché montada de BuildKit:
• Usa --mount=type=cache para montar directorios de caché
• Guarda la caché de node_modules de npm install en el host
• En el siguiente build se reutiliza la caché; el build puede ser más de 10 veces más rápido
Ejemplos:
• npm: RUN --mount=type=cache,target=/root/.npm npm install
• yarn: RUN --mount=type=cache,target=/root/.yarn yarn install --frozen-lockfile
• pip: RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
• apt: RUN --mount=type=cache,target=/var/cache/apt,sharing=locked apt-get update && apt-get install -y gcc
En apt, el parámetro sharing=locked es importante porque apt necesita acceso exclusivo a su caché, para evitar conflictos en builds concurrentes.
¿Qué resultados da la optimización de builds de Docker?
• Tiempo de build de 10 minutos a 30 segundos (20 veces más rápido)
• Tamaño de imagen de 1,2 GB a 680 MB
• Contexto de build de 800 MB a menos de 10 MB
• Tiempo de transferencia de 2-3 minutos a unos segundos
En mi propio proyecto, tras estos tres pasos el build pasó de 10 minutos a 30 segundos y la imagen de 1,2 GB a 680 MB. Es una de las optimizaciones con mejor retorno que he visto.
Tres cosas clave:
• Primero, hazlo ya (crea .dockerignore en la raíz del proyecto)
• Segundo, cámbialo hoy (reordena las instrucciones del Dockerfile)
• Tercero, cuando puedas (si hay muchas dependencias y cambian a menudo, prueba la caché montada de BuildKit)
11 min de lectura · Publicado el: 17 dic 2025 · Actualizado el: 21 ago 2026
Guía práctica de Docker
Si llegaste desde búsqueda, lo más rápido es ir al artículo anterior o siguiente de esta misma serie.
Anterior
Docker multietapa en producción: de 1 GB a 10 MB
Domina Docker multietapa y reduce imágenes de producción de 1 GB a 10 MB. Plantillas para Go, Node.js y Python, comparativa Alpine vs Distroless y 5 errores habituales que debes evitar.
Parte 6 de 38
Siguiente
Orquestación multi-servicio con Docker Compose: entorno local en un solo comando
Orquesta Web, API, MySQL y Redis con Docker Compose y arranca tu entorno local con un comando. Olvídate de instalaciones manuales, conflictos de versiones y puertos ocupados: un nuevo miembro del equipo puede clonar el repo y empezar en 5 minutos; cambiar de proyecto lleva segundos.
Parte 8 de 38



Comentarios
Inicia sesión con GitHub para dejar un comentario