# Optimizar el uso del caché en las compilaciones


Al compilar con Docker, se reutiliza una capa del caché de compilación si la
instrucción y los archivos de los que depende no han cambiado desde que se compiló
anteriormente. Reutilizar capas del caché acelera el proceso de compilación porque Docker
no tiene que volver a compilar la capa de nuevo.

Aquí tienes algunas técnicas que puedes utilizar para optimizar el almacenamiento en caché de las compilaciones y acelerar
el proceso de compilación:

- [Ordenar las capas](#ordenar-las-capas): Poner los comandos en tu
  Dockerfile en un orden lógico puede ayudarte a evitar la invalidación innecesaria
  del caché.
- [Mantener el contexto pequeño](#mantener-el-contexto-pequeño): El contexto es el conjunto de
  archivos y directorios que se envían al builder para procesar una instrucción de
  compilación. Mantener el contexto lo más pequeño posible reduce la cantidad de datos que
  deben enviarse al builder y reduce la probabilidad de invalidación del
  caché.
- [Usar montajes bind](#usar-montajes-bind): Los montajes bind te permiten montar un archivo o
  directorio desde la máquina host en el contenedor de compilación. El uso de montajes bind
  puede ayudarte a evitar capas innecesarias en la imagen, las cuales pueden ralentizar el
  proceso de compilación.
- [Usar montajes de caché](#usar-montajes-de-caché): Los montajes de caché te permiten especificar un
  caché de paquetes persistente para utilizarlo durante las compilaciones. El caché persistente ayuda a
  acelerar los pasos de compilación, especialmente los pasos que implican la instalación de paquetes utilizando
  un gestor de paquetes. Tener un caché persistente para los paquetes significa que, incluso si
  vuelves a compilar una capa, solo descargarás los paquetes nuevos o modificados.
- [Usar un caché externo](#usar-un-caché-externo): Un caché externo te permite
  almacenar el caché de compilación en una ubicación remota. La imagen del caché externo se puede
  compartir entre varias compilaciones y en diferentes entornos.

## Ordenar las capas

Poner los comandos de tu Dockerfile en un orden lógico es un excelente lugar
para comenzar. Dado que un cambio provoca la recompilación de los pasos siguientes, intenta hacer
que los pasos costosos aparezcan cerca del principio del Dockerfile. Los pasos que cambian
con frecuencia deben aparecer cerca del final del Dockerfile, para evitar activar
recompilaciones de capas que no han cambiado.

Considera el siguiente ejemplo. Un fragmento de Dockerfile que ejecuta una compilación de JavaScript
a partir de los archivos de origen en el directorio actual:

```dockerfile
# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY . .          # Copia todos los archivos en el directorio actual
RUN npm install   # Instala dependencias
RUN npm build     # Ejecuta la compilación
```

Este Dockerfile es bastante ineficiente. Actualizar cualquier archivo provoca una reinstalación de
todas las dependencias cada vez que compilas la imagen de Docker, incluso si las dependencias
no han cambiado desde la última vez.

En su lugar, el comando `COPY` se puede dividir en dos. Primero, copia los archivos de gestión de
paquetes (en este caso, `package.json` y `yarn.lock`). Luego, instala
las dependencias. Finalmente, copia el código fuente del proyecto, el cual está sujeto
a cambios frecuentes.

```dockerfile
# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY package.json yarn.lock .    # Copia archivos de gestión de paquetes
RUN npm install                  # Instala dependencias
COPY . .                         # Copia los archivos del proyecto
RUN npm build                    # Ejecuta la compilación
```

Al instalar las dependencias en las capas iniciales del Dockerfile, no hay
necesidad de volver a compilar esas capas cuando un archivo del proyecto ha cambiado.

## Mantener el contexto pequeño

La forma más sencilla de asegurarse de que tu contexto no incluya archivos innecesarios es
crear un archivo `.dockerignore` en la raíz de tu contexto de compilación. El
archivo `.dockerignore` funciona de manera similar a los archivos `.gitignore` y te permite
excluir archivos y directorios del contexto de compilación.

A continuación, se muestra un ejemplo de archivo `.dockerignore` que excluye el directorio `node_modules`
y todos los archivos y directorios que comiencen con `tmp`:

```plaintext {title=".dockerignore"}
node_modules
tmp*
```

Las reglas de exclusión especificadas en el archivo `.dockerignore` se aplican a todo el contexto
de compilación, incluidos los subdirectorios. Esto significa que es un mecanismo bastante general,
pero es una buena forma de excluir archivos y directorios que sabes
que no necesitas en el contexto de compilación, como archivos temporales, archivos de registro (logs) y
artefactos de compilación.

## Usar montajes bind

Es posible que estés familiarizado con los montajes bind al ejecutar contenedores con `docker
run` o Docker Compose. Los montajes bind te permiten montar un archivo o directorio desde la
máquina host en un contenedor.

```bash
# montaje bind utilizando la opción -v
docker run -v $(pwd):/path/in/container image-name
# montaje bind utilizando la opción --mount
docker run --mount=type=bind,src=.,dst=/path/in/container image-name
```

Para usar montajes bind en una compilación, puedes usar la opción `--mount` con la instrucción
`RUN` en tu Dockerfile:

```dockerfile
FROM golang:latest
WORKDIR /build
RUN --mount=type=bind,target=. go build -o /app/hello
```

En este ejemplo, el directorio actual se monta en el contenedor de compilación en
`/build` antes de que se ejecute el comando `go build`. La salida de la compilación se
escribe en `/app/hello`, que está fuera del punto de montaje. Esta distinción es
importante: la salida de la compilación debe escribirse fuera del destino del montaje bind,
ya que el montaje es de solo lectura por defecto. El código fuente está disponible en el
contenedor de compilación durante la ejecución de esa instrucción `RUN`. Cuando la
instrucción termina de ejecutarse, los archivos montados no persisten en la imagen final,
ni en el caché de compilación. Solo permanece la salida del comando `go build`.

Las instrucciones `COPY` y `ADD` en un Dockerfile te permiten copiar archivos desde el
contexto de compilación al contenedor de compilación. El uso de montajes bind es beneficioso para
la optimización del caché de compilación porque no estás añadiendo capas innecesarias al
caché. Si tienes un contexto de compilación que es más bien grande y solo se utiliza
para generar un artefacto, es mejor usar montajes bind para montar temporalmente
el código fuente requerido para generar el artefacto en la compilación. Si utilizas
`COPY` para añadir los archivos al contenedor de compilación, BuildKit incluirá todos
esos archivos en el caché, incluso si los archivos no se utilizan en la imagen final.

Hay algunas cosas a tener en cuenta al usar montajes bind en una compilación:

- Los montajes bind son de solo lectura por defecto. Si necesitas escribir en el directorio
  montado, debes especificar la opción `rw`. Sin embargo, incluso con la opción `rw`,
  los cambios no persisten en la imagen final ni en el caché de compilación.
  Las escrituras de archivos se mantienen durante la ejecución de la instrucción `RUN` y
  se descartan una vez que la instrucción termina.
- Los archivos montados no persisten en la imagen final. Solo la salida de la
  instrucción `RUN` persiste en la imagen final. Si necesitas incluir
  archivos del contexto de compilación en la imagen final, debes usar las instrucciones
  `COPY` o `ADD`.
- Si el directorio de destino no está vacío, los contenidos del directorio de destino
  quedan ocultos por los archivos montados. Los contenidos originales se restauran después de que
  termina la instrucción `RUN`.

  **Ejemplo**



Por ejemplo, dado un contexto de compilación que contiene únicamente un `Dockerfile`:

```plaintext
.
└── Dockerfile
```

Y un Dockerfile que monta el directorio actual en el contenedor de compilación:

```dockerfile
FROM alpine:latest
WORKDIR /work
RUN touch foo.txt
RUN --mount=type=bind,target=. ls
RUN ls
```

El primer comando `ls` con el montaje bind muestra el contenido del directorio
montado. El segundo `ls` muestra el contenido del contexto de compilación original.

```plaintext {title="Registro de compilación"}
#8 [stage-0 3/5] RUN touch foo.txt
#8 DONE 0.1s

#9 [stage-0 4/5] RUN --mount=target=. ls -1
#9 0.040 Dockerfile
#9 DONE 0.0s

#10 [stage-0 5/5] RUN ls -1
#10 0.046 foo.txt
#10 DONE 0.1s
```




## Usar montajes de caché

Las capas de caché normales en Docker corresponden a una coincidencia exacta de la instrucción
y los archivos de los que depende. Si la instrucción y los archivos de los que depende
han cambiado desde que se compiló la capa, la capa se invalida y el proceso de compilación
debe volver a compilar la capa.

Los montajes de caché son una forma de especificar una ubicación de caché persistente para ser utilizada durante
las compilaciones. El caché es acumulativo entre compilaciones, por lo que puedes leer y escribir en él
varias veces. Este almacenamiento en caché persistente significa que, incluso si necesitas
volver a compilar una capa, solo descargarás los paquetes nuevos o modificados. Cualquier paquete
que no haya cambiado se reutilizará desde el montaje de caché.

Para usar montajes de caché en una compilación, puedes usar la opción `--mount` con la instrucción
`RUN` en tu Dockerfile:

```dockerfile
FROM node:latest
WORKDIR /app
RUN --mount=type=cache,target=/root/.npm npm install
```

En este ejemplo, el comando `npm install` utiliza un montaje de caché para el
directorio `/root/.npm`, la ubicación predeterminada para el caché de npm. El montaje de caché
persiste entre compilaciones, por lo que incluso si terminas volviendo a compilar la capa, solo
descargarás los paquetes nuevos o modificados. Cualquier cambio en el caché persiste
entre compilaciones, y el caché se comparte entre múltiples compilaciones.

La forma de especificar los montajes de caché depende de la herramienta de compilación que estés utilizando. Si
no estás seguro de cómo especificar los montajes de caché, consulta la documentación de la herramienta de
compilación que estés utilizando. Aquí tienes algunos ejemplos:

**Go**



```dockerfile
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -o /app/hello
```

**Apt**



```dockerfile
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
  --mount=type=cache,target=/var/lib/apt,sharing=locked \
  apt update && apt-get --no-install-recommends install -y gcc
```

**Python**



```dockerfile
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
```

**Ruby**



```dockerfile
RUN --mount=type=cache,target=/root/.gem \
    bundle install
```

**Rust**



```dockerfile
RUN --mount=type=cache,target=/app/target/ \
    --mount=type=cache,target=/usr/local/cargo/git/db \
    --mount=type=cache,target=/usr/local/cargo/registry/ \
    cargo build
```

**.NET**



```dockerfile
RUN --mount=type=cache,target=/root/.nuget/packages \
    dotnet restore
```

**PHP**

  

```dockerfile
RUN --mount=type=cache,target=/tmp/cache \
    composer install
```



Es importante que leas la documentación de la herramienta de compilación que utilizas
para asegurarte de estar utilizando las opciones correctas de montaje de caché. Los gestores de paquetes
tienen diferentes requisitos sobre cómo utilizan el caché, y el uso de las opciones incorrectas
puede provocar comportamientos inesperados. Por ejemplo, Apt necesita acceso exclusivo
a sus datos, por lo que los cachés utilizan la opción `sharing=locked` para garantizar que las
compilaciones paralelas que utilizan el mismo montaje de caché se esperen entre sí y no accedan
a los mismos archivos de caché al mismo tiempo.

## Usar un caché externo

El almacenamiento de caché predeterminado para las compilaciones es interno al builder (instancia de
BuildKit) que estés utilizando. Cada builder utiliza su propio almacenamiento de caché. Cuando
cambias entre diferentes builders, el caché no se comparte entre ellos. El uso de
un caché externo te permite definir una ubicación remota para subir (push) y descargar (pull)
los datos del caché.

Los cachés externos son especialmente útiles para los pipelines de CI/CD, donde los builders
suelen ser efímeros y los minutos de compilación son valiosos. Reutilizar el caché entre
compilaciones puede acelerar drásticamente el proceso de compilación y reducir costos. Incluso puedes
hacer uso del mismo caché en tu entorno de desarrollo local.

Para usar un caché externo, especificas las opciones `--cache-to` y `--cache-from`
con el comando `docker buildx build`.

- `--cache-to` exporta el caché de compilación a la ubicación especificada.
- `--cache-from` especifica cachés remotos para que los utilice la compilación.

El siguiente ejemplo muestra cómo configurar un workflow de GitHub Actions utilizando
`docker/build-push-action`, y subir las capas de caché de compilación a una imagen de registro
OCI:

```yaml {title=".github/workflows/ci.yml"}
name: ci

on:
  push:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Iniciar sesión en Docker Hub
        uses: docker/login-action@v4
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Configurar Docker Buildx
        uses: docker/setup-buildx-action@v4

      - name: Compilar y subir (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
```

Esta configuración le indica a BuildKit que busque el caché en la imagen `user/app:buildcache`.
Y cuando termina la compilación, el nuevo caché de compilación se sube a la misma imagen,
sobrescribiendo el caché anterior.

Este caché también se puede utilizar localmente. Para descargar el caché en una compilación local,
puedes usar la opción `--cache-from` con el comando `docker buildx build`:

```console
$ docker buildx build --cache-from type=registry,ref=user/app:buildcache .
```

## Resumen

Optimizar el uso del caché en las compilaciones puede acelerar significativamente el proceso de compilación.
Mantener el contexto de compilación pequeño, utilizar montajes bind, montajes de caché y cachés
externos son técnicas que puedes utilizar para aprovechar al máximo el caché de compilación y
acelerar el proceso de compilación.

Para obtener más información sobre los conceptos tratados en esta guía, consulta:

- [Archivos .dockerignore](/build/concepts/context/#dockerignore-files)
- [Invalidación de caché](/build/cache/invalidation/)
- [Montajes de caché](/reference/dockerfile/#run---mounttypecache)
- [Tipos de backend de caché](/build/cache/backends/)
- [Mejores prácticas de compilación](/build/building/best-practices/)


