Contenido
Tutorial de caché de compilación en Docker: optimiza tus imágenes
Con el mismo Dockerfile, el tiempo de compilación puede variar desde menos de 1 segundo hasta varios minutos, según cómo esté escrito; el tamaño de la imagen también puede cambiar mucho. En esta sección verás, con experimentos, las reglas de la caché y cómo reducir el tamaño de las imágenes.
Cómo funciona la caché
docker build procesa las instrucciones en orden. Para cada línea, Docker comprueba: ¿Esta instrucción y todas las capas anteriores son iguales que en la compilación anterior?
- Sí: usa directamente la caché y muestra
CACHEDen la salida. - No: vuelve a ejecutar esta línea y todas las siguientes dejan de usar la caché.
| Instrucción | Criterio |
|---|---|
RUN | Si la cadena de la instrucción es idéntica (no comprueba si los paquetes disponibles en la red se han actualizado) |
COPY / ADD | Si el contenido de los archivos copiados es idéntico |
| Otras | Si la instrucción y sus argumentos son idénticos |
Experimento: el efecto del orden de las instrucciones
Usa un proyecto pequeño con Express (el paquete express está en package.json) para comparar dos Dockerfiles.
Orden incorrecto (Dockerfile.bad):
FROM node:24-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
Orden correcto (Dockerfile.good):
FROM node:24-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
CMD ["node", "server.js"]
Orden incorrecto: cambia una línea de código
Primero, compila una vez. Luego cambia una sola palabra en server.js y vuelve a compilar:
$ docker build -f Dockerfile.bad -t cache-bad .
[+] Building 6.9s (9/9) FINISHED docker:default
=> [internal] load build definition from Dockerfile.bad 0.0s
=> => transferring dockerfile: 125B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.2s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/4] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 280B 0.0s
=> CACHED [2/4] WORKDIR /app 0.0s
=> [3/4] COPY . . 0.1s
=> [4/4] RUN npm install 5.4s
=> exporting to image 1.0s
=> => exporting layers 0.5s
=> => exporting manifest sha256:d27475f082a53018e303541bc1c0691ac25526e87761300e2aa8ec76d73e 0.0s
=> => exporting config sha256:19f2024df4329a5f2af23e50d8c18826f073d425e18133e9571c393999d913 0.0s
=> => exporting attestation manifest sha256:58b4730e9566f2ec7e8222cd41c14ea971d87272b6b0c190 0.0s
=> => exporting manifest list sha256:58b8fc79124f2fe21270801d5e40f09839c06ba5203304423125270 0.0s
=> => naming to docker.io/library/cache-bad:latest 0.0s
=> => unpacking to docker.io/library/cache-bad:latest 0.4s
[3/4] COPY . . vuelve a ejecutarse porque cambió server.js. También se vuelve a ejecutar [4/4] RUN npm install, la instrucción siguiente, y tarda 5.4 segundos en descargar e instalar de nuevo todos los paquetes.
El orden correcto: cambia una línea de código
Primera compilación (se ejecuta npm install):
$ docker build -f Dockerfile.good -t cache-good .
[+] Building 5.7s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.good 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 95B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> [3/5] COPY package.json ./ 0.0s
=> [4/5] RUN npm install 4.4s
=> [5/5] COPY . . 0.1s
=> exporting to image 0.9s
=> => exporting layers 0.5s
=> => exporting manifest sha256:4b36d92a774b02cc1a188193e178a445b90f330b19f2dcc741d631e246e8 0.0s
=> => exporting config sha256:4a597aa667d9ba52eab36c4483f7507bbcae158b1b22af5ca6e4ac5b7020fa 0.0s
=> => exporting attestation manifest sha256:efea2ef65e2672718391b6419b9f5f2be50396163032a29e 0.0s
=> => exporting manifest list sha256:236918100fd2092d0388da5c03b32dfa3abaa2e62304abb3e3dd335 0.0s
=> => naming to docker.io/library/cache-good:latest 0.0s
=> => unpacking to docker.io/library/cache-good:latest 0.4s
Vuelve a construir después de modificar server.js:
$ docker build -f Dockerfile.good -t cache-good .
[+] Building 0.4s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.good 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.1s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 280B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> CACHED [3/5] COPY package.json ./ 0.0s
=> CACHED [4/5] RUN npm install 0.0s
=> [5/5] COPY . . 0.0s
=> exporting to image 0.1s
=> => exporting layers 0.0s
=> => exporting manifest sha256:6cd8c2671a295aab85724aefe72f8dc749f4d332224f5d534fff5e4e57fa 0.0s
=> => exporting config sha256:7df3d574ae91f98ea3634c4dcede63e64ce8018c902643c2db08c2b4dee4c3 0.0s
=> => exporting attestation manifest sha256:fc9ff7e57f4187c04b74446f5dd96e46ef9c7f0ea88372a6 0.0s
=> => exporting manifest list sha256:79e8a6d76ff2382f02bcadc183a44670959d1d0a30467a27b9d619e 0.0s
=> => naming to docker.io/library/cache-good:latest 0.0s
=> => unpacking to docker.io/library/cache-good:latest 0.0s
La compilación completa solo tardó 0.4 segundos: COPY package.json y RUN npm install aparecen como CACHED; solo se vuelve a ejecutar el último COPY . .. Este proyecto tiene un solo paquete; en proyectos reales suele haber cientos, y npm install puede tardar varios minutos, así que la diferencia es mucho mayor.
La misma idea se aplica a todos los lenguajes:
| Lenguaje | Copia primero | Instala después |
|---|---|---|
| Node.js | package.json, package-lock.json | npm ci |
| Python | requirements.txt o pyproject.toml | pip install |
| Go | go.mod, go.sum | go mod download |
| Java (Maven) | pom.xml | mvn dependency:go-offline |
| PHP | composer.json, composer.lock | composer install |
Experimento: limpiar archivos temporales en la misma capa
Cada capa se añade sobre la anterior. Si eliminas archivos en un RUN posterior, el tamaño de la capa anterior no disminuye. Compara estas dos formas de instalar curl:
Dockerfile.separate:
FROM debian:trixie-slim
RUN apt-get update
RUN apt-get install -y --no-install-recommends curl
RUN rm -rf /var/lib/apt/lists/*
Dockerfile.same:
FROM debian:trixie-slim
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& rm -rf /var/lib/apt/lists/*
Compara los tamaños después de construir:
$ docker images --filter reference=apt-separate --filter reference=apt-same --filter reference=debian:trixie-slim
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
apt-same:latest 5357e9775c1e 136MB 34.9MB
apt-separate:latest ad6c0a37d9ea 174MB 51.7MB
El contenido es idéntico (ambas instalan curl y eliminan las listas de paquetes), pero apt-separate ocupa 38 MB más. Usa docker history para ver por qué:
$ docker history apt-separate
IMAGE CREATED CREATED BY SIZE COMMENT
ad6c0a37d9ea 11 seconds ago RUN /bin/sh -c rm -rf /var/lib/apt/lists/* #… 20.5kB buildkit.dockerfile.v0
<missing> 11 seconds ago RUN /bin/sh -c apt-get install -y --no-insta… 13.4MB buildkit.dockerfile.v0
<missing> 16 seconds ago RUN /bin/sh -c apt-get update # buildkit 21.6MB buildkit.dockerfile.v0
<missing> 8 days ago # debian.sh --arch 'amd64' out/ 'trixie' '@1… 87.7MB debuerreotype 0.17
$ docker history apt-same
IMAGE CREATED CREATED BY SIZE COMMENT
5357e9775c1e 1 second ago RUN /bin/sh -c apt-get update && apt-get… 13.4MB buildkit.dockerfile.v0
<missing> 8 days ago # debian.sh --arch 'amd64' out/ 'trixie' '@1… 87.7MB debuerreotype 0.17
La capa apt-get update de apt-separate ocupa 21.6 MB (las listas de paquetes descargadas). El rm -rf posterior solo las «marca para eliminar» en una capa nueva; los 21.6 MB originales siguen en la imagen. Si escribes todo en un mismo RUN, las listas de paquetes se eliminan antes de que termine esa capa y nunca se guardan en ninguna capa.
Formas habituales de «no dejar archivos temporales» en cada lenguaje:
RUN pip install --no-cache-dir -r requirements.txt
RUN npm ci && npm cache clean --force
RUN apk add --no-cache curl
.dockerignore
Al empezar una compilación, Docker envía todo el contexto de compilación (normalmente la carpeta actual) al motor de compilación. Si no excluyes node_modules, el ejemplo de Introducción a Dockerfile envía 30 MB; al excluirlo, solo envía 159 B. Ejemplo de .dockerignore:
.git
node_modules
dist
*.log
.env
.env.*
Dockerfile*
.dockerignore
Además de acelerar la compilación, esto evita que los cambios en esos archivos invaliden la caché de COPY . . e impide que .env y las claves privadas se incluyan en la imagen.
Elige una imagen base ligera
Tamaños medidos para las imágenes de Python 3.14 en la sección de Imágenes:
| Imagen base | Espacio en disco |
|---|---|
python:3.14 | 1.63 GB |
python:3.14-slim | 180 MB |
python:3.14-alpine | 82.6 MB |
Compilaciones multietapa
Las herramientas de compilación y los paquetes de desarrollo solo se necesitan durante la compilación; no deberían quedarse en la imagen final. Las compilaciones multietapa son la forma más eficaz de reducir su tamaño.
Montajes de caché: acelera la descarga de paquetes
BuildKit admite montajes de caché para conservar la caché de descargas de los gestores de paquetes entre compilaciones. Aunque la capa RUN npm ci deje de usar la caché, no tendrás que volver a descargar todos los paquetes de la red:
# syntax=docker/dockerfile:1
FROM node:24-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
RUN --mount=type=cache,target=/root/.cache/pip \
pip install -r requirements.txt
El contenido de los montajes de caché no se incluye en la imagen, así que no aumenta su tamaño.
Forzar una compilación sin caché
Cuando necesites obtener la versión más reciente de un paquete o sospeches que hay un problema con la caché:
$ docker build --no-cache -f Dockerfile.good -t cache-good .
[+] Building 6.6s (10/10) FINISHED docker:default
=> [internal] load build definition from Dockerfile.good 0.0s
=> => transferring dockerfile: 147B 0.0s
=> [internal] load metadata for docker.io/library/node:24-alpine 0.3s
=> [internal] load .dockerignore 0.0s
=> => transferring context: 65B 0.0s
=> [1/5] FROM docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> => resolve docker.io/library/node:24-alpine@sha256:ebfe2f90462722a7a4de65e91990e97fe0d401 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 280B 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> [3/5] COPY package.json ./ 0.0s
=> [4/5] RUN npm install 5.1s
=> [5/5] COPY . . 0.1s
=> exporting to image 1.0s
=> => exporting layers 0.4s
=> => exporting manifest sha256:30688d5c22069d97e9db0bd32d5939e8e99fda0c6a0c4af1f32fb5baacc9 0.0s
=> => exporting config sha256:e8e75108925e8f145e7367b8e36350a8175c3776154f560fb47a9fb287fe21 0.0s
=> => exporting attestation manifest sha256:adbbb2fc6683c9a35c67bb9237cf3afcfd24c39dd9aa4b55 0.0s
=> => exporting manifest list sha256:6b92b5704d4fa7a3f11cfda5ffde00e6516bdbf358e23dbece712a2 0.0s
=> => naming to docker.io/library/cache-good:latest 0.0s
=> => unpacking to docker.io/library/cache-good:latest 0.5s
--no-cache vuelve a ejecutar cada paso (no aparece ningún CACHED). Si añades --pull, también vuelve a comprobar si hay una versión nueva de la imagen base.
Consultar y borrar la caché de compilación
La caché de compilación se acumula con el tiempo:
$ docker builder du
ID RECLAIMABLE SIZE LAST ACCESSED
mr6xd3ylof28pr8obxiet3fo8 true 403B 3 minutes ago
l34l5occgcgh8bscivg2iusrn true 626B 3 minutes ago
... (salida omitida)
Shared: 488.2MB
Private: 118.5MB
Reclaimable: 606.7MB
Total: 606.7MB
$ docker builder prune -f
ID RECLAIMABLE SIZE LAST ACCESSED
xqb9jtg0kmq0mdzb0vrmod68j true 20.67kB 3 minutes ago
u1pbndrekyh1q91qbb4n9e3fy* true 8.192kB 5 minutes ago
... (salida omitida)
Total: 118.5MB
docker builder prune solo borra la caché de compilación; no afecta a las imágenes ni a los contenedores. Consulta Liberar espacio en disco para saber cómo borrar todos los datos de Docker.
Resumen
- Cuando cambia una capa, hay que reconstruir todas las capas posteriores.
- Coloca primero los pasos que cambian poco (instalar paquetes) y después los que cambian con frecuencia (copiar código).
- Descarga, instala y elimina los archivos temporales en el mismo
RUNpara que no queden en la imagen. RUN --mount=type=cachepermite compartir las descargas de paquetes entre varias compilaciones.
Ejercicios
Ejercicio 1: ¿Cuál de estos dos fragmentos de Dockerfile genera una imagen más pequeña? ¿Por qué?
# A
RUN apt-get update
RUN apt-get install -y curl
RUN rm -rf /var/lib/apt/lists/*
# B
RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/*
Respuesta
B genera una imagen más pequeña. En A, rm se ejecuta en una capa nueva, así que las listas de paquetes siguen en las capas anteriores. En B se descargan y se eliminan en la misma capa, por lo que no se guardan.
Ejercicio 2: Compila el mismo proyecto una vez con docker build --no-cache y otra sin opciones. Compara el tiempo total que muestra [+] Building.
Pista
Sin opciones, la mayoría de los pasos muestran CACHED y suelen tardar unas décimas de segundo. Con --no-cache, se vuelve a ejecutar cada paso; el tiempo depende de la velocidad de instalación de los paquetes.
Preguntas frecuentes
¿Qué hago si RUN apt-get update usa la caché e instala paquetes antiguos?
La caché solo compara las cadenas de las instrucciones; no comprueba si los paquetes disponibles en la red se han actualizado. Escribe apt-get update y apt-get install en el mismo RUN y, cuando sea necesario, fuerza una nueva compilación con docker build --no-cache.
¿La caché de compilación ocupa mucho espacio en disco?
Usa docker builder du para consultarlo y docker builder prune para limpiarlo. Consulta la prueba en [Liberar espacio en disco](/es/docker/cleanup/).
¿Cada compilación desde cero en CI tarda demasiado?
Usa una caché externa, como cache-from: type=gha en GitHub Actions. Consulta [Compilación automática con GitHub Actions](/es/docker/ci-cd/).
Actualizado 14 de octubre de 2026