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 CACHED en la salida.
  • No: vuelve a ejecutar esta línea y todas las siguientes dejan de usar la caché.
InstrucciónCriterio
RUNSi la cadena de la instrucción es idéntica (no comprueba si los paquetes disponibles en la red se han actualizado)
COPY / ADDSi el contenido de los archivos copiados es idéntico
OtrasSi la instrucción y sus argumentos son idénticos
Nueva compilación tras modificar el código: si instalas primero los paquetes, npm ci usa la caché; si copias primero el código fuente, npm ci se vuelve a ejecutar
Cuando cambia una capa, hay que volver a compilar todas las capas posteriores: coloca primero los pasos que cambian con menos frecuencia

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:

LenguajeCopia primeroInstala después
Node.jspackage.json, package-lock.jsonnpm ci
Pythonrequirements.txt o pyproject.tomlpip install
Gogo.mod, go.sumgo mod download
Java (Maven)pom.xmlmvn dependency:go-offline
PHPcomposer.json, composer.lockcomposer 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 baseEspacio en disco
python:3.141.63 GB
python:3.14-slim180 MB
python:3.14-alpine82.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 RUN para que no queden en la imagen.
  • RUN --mount=type=cache permite 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