⚠️ Traduction non officielle - Cette documentation est une traduction communautaire non officielle de Docker.

Optimiser l'utilisation du cache dans les constructions

Lors de la construction avec Docker, une couche est réutilisée depuis le cache de construction si l'instruction et les fichiers dont elle dépend n'ont pas changé depuis sa dernière construction. La réutilisation des couches du cache accélère le processus de construction car Docker n'a pas à reconstruire la couche.

Voici quelques techniques que vous pouvez utiliser pour optimiser la mise en cache de la construction et accélérer le processus de construction :

  • Ordonnez vos couches : Mettre les commandes de votre Dockerfile dans un ordre logique peut vous aider à éviter une invalidation de cache inutile.
  • Gardez le contexte petit : Le contexte est l'ensemble des fichiers et répertoires qui sont envoyés au constructeur pour traiter une instruction de construction. Garder le contexte aussi petit que possible réduit la quantité de données qui doit être envoyée au constructeur, et réduit la probabilité d'invalidation du cache.
  • Utilisez les montages de type bind : Les montages de type bind vous permettent de monter un fichier ou un répertoire de la machine hôte dans le conteneur de construction. L'utilisation de montages de type bind peut vous aider à éviter des couches inutiles dans l'image, ce qui peut ralentir le processus de construction.
  • Utilisez les montages de cache : Les montages de cache vous permettent de spécifier un cache de paquets persistant à utiliser lors des constructions. Le cache persistant aide à accélérer les étapes de construction, en particulier les étapes qui impliquent l'installation de paquets en utilisant un gestionnaire de paquets. Avoir un cache persistant pour les paquets signifie que même si vous reconstruisez une couche, vous ne téléchargez que les paquets nouveaux ou modifiés.
  • Utilisez un cache externe : Un cache externe vous permet de stocker le cache de construction à un emplacement distant. L'image de cache externe peut être partagée entre plusieurs constructions, et à travers différents environnements.

Ordonnez vos couches

Mettre les commandes de votre Dockerfile dans un ordre logique est un excellent point de départ. Parce qu'un changement provoque une reconstruction pour les étapes qui suivent, essayez de faire apparaître les étapes coûteuses près du début du Dockerfile. Les étapes qui changent souvent devraient apparaître près de la fin du Dockerfile, pour éviter de déclencher des reconstructions de couches qui n'ont pas changé.

Considérez l'exemple suivant. Un extrait de Dockerfile qui exécute une construction JavaScript à partir des fichiers source dans le répertoire courant :

# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY . .          # Copier tous les fichiers du répertoire courant
RUN npm install   # Installer les dépendances
RUN npm build     # Exécuter la construction

Ce Dockerfile est plutôt inefficace. La mise à jour de n'importe quel fichier provoque une réinstallation de toutes les dépendances à chaque fois que vous construisez l'image Docker même si les dépendances n'ont pas changé depuis la dernière fois.

Au lieu de cela, la commande COPY peut être divisée en deux. D'abord, copiez les fichiers de gestion de paquets (dans ce cas, package.json et yarn.lock). Ensuite, installez les dépendances. Enfin, copiez le code source du projet, qui est sujet à des changements fréquents.

# syntax=docker/dockerfile:1
FROM node
WORKDIR /app
COPY package.json yarn.lock .    # Copier les fichiers de gestion de paquets
RUN npm install                  # Installer les dépendances
COPY . .                         # Copier les fichiers du projet
RUN npm build                    # Exécuter la construction

En installant les dépendances dans les couches antérieures du Dockerfile, il n'est pas nécessaire de reconstruire ces couches lorsqu'un fichier de projet a changé.

Gardez le contexte petit

Le moyen le plus simple de s'assurer que votre contexte n'inclut pas de fichiers inutiles est de créer un fichier .dockerignore à la racine de votre contexte de construction. Le fichier .dockerignore fonctionne de manière similaire aux fichiers .gitignore, et vous permet d'exclure des fichiers et des répertoires du contexte de construction.

Voici un exemple de fichier .dockerignore qui exclut le répertoire node_modules, tous les fichiers et répertoires qui commencent par tmp :

.dockerignore
node_modules
tmp*

Les règles d'exclusion spécifiées dans le fichier .dockerignore s'appliquent à l'ensemble du contexte de construction, y compris les sous-répertoires. Cela signifie que c'est un mécanisme assez grossier, mais c'est un bon moyen d'exclure des fichiers et des répertoires que vous savez ne pas avoir besoin dans le contexte de construction, tels que les fichiers temporaires, les fichiers de log et les artefacts de construction.

Utilisez les montages de type bind

Vous êtes peut-être familier avec les montages de type bind lorsque vous exécutez des conteneurs avec docker run ou Docker Compose. Les montages de type bind vous permettent de monter un fichier ou un répertoire de la machine hôte dans un conteneur.

# montage de type bind en utilisant l'indicateur -v
docker run -v $(pwd):/path/in/container image-name
# montage de type bind en utilisant l'indicateur --mount
docker run --mount=type=bind,src=.,dst=/path/in/container image-name

Pour utiliser les montages de type bind dans une construction, vous pouvez utiliser l'indicateur --mount avec l'instruction RUN dans votre Dockerfile :

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

Dans cet exemple, le répertoire courant est monté dans le conteneur de construction avant l'exécution de la commande go build. Le code source est disponible dans le conteneur de construction pour la durée de cette instruction RUN. Lorsque l'instruction a fini de s'exécuter, les fichiers montés ne sont pas persistés dans l'image finale, ni dans le cache de construction. Seule la sortie de la commande go build reste.

Les instructions COPY et ADD dans un Dockerfile vous permettent de copier des fichiers du contexte de construction dans le conteneur de construction. L'utilisation de montages de type bind est bénéfique pour l'optimisation du cache de construction car vous n'ajoutez pas de couches inutiles au cache. Si vous avez un contexte de construction assez grand, et qu'il n'est utilisé que pour générer un artefact, il est préférable d'utiliser des montages de type bind pour monter temporairement le code source requis pour générer l'artefact dans la construction. Si vous utilisez COPY pour ajouter les fichiers au conteneur de construction, BuildKit inclura tous ces fichiers dans le cache, même si les fichiers ne sont pas utilisés dans l'image finale.

Il y a quelques points à connaître lors de l'utilisation de montages de type bind dans une construction :

  • Les montages de type bind sont en lecture seule par défaut. Si vous avez besoin d'écrire dans le répertoire monté, vous devez spécifier l'option rw. Cependant, même avec l'option rw, les modifications ne sont pas persistées dans l'image finale ou le cache de construction. Les écritures de fichiers sont maintenues pour la durée de l'instruction RUN, et sont supprimées une fois l'instruction terminée.

  • Les fichiers montés ne sont pas persistés dans l'image finale. Seule la sortie de l'instruction RUN est persistée dans l'image finale. Si vous avez besoin d'inclure des fichiers du contexte de construction dans l'image finale, vous devez utiliser les instructions COPY ou ADD.

  • Si le répertoire cible n'est pas vide, le contenu du répertoire cible est masqué par les fichiers montés. Le contenu original est restauré une fois l'instruction RUN terminée.

    Par exemple, étant donné un contexte de construction avec seulement un Dockerfile dedans :

    .
    └── Dockerfile

    Et un Dockerfile qui monte le répertoire courant dans le conteneur de construction :

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

    La première commande ls avec le montage de type bind montre le contenu du répertoire monté. La seconde commande ls liste le contenu du contexte de construction original.

    Journal de construction
    #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

Utilisez les montages de cache

Les couches de cache régulières dans Docker correspondent à une correspondance exacte de l'instruction et des fichiers dont elle dépend. Si l'instruction et les fichiers dont elle dépend ont changé depuis la construction de la couche, la couche est invalidée, et le processus de construction doit reconstruire la couche.

Les montages de cache sont un moyen de spécifier un emplacement de cache persistant à utiliser lors des constructions. Le cache est cumulatif à travers les constructions, vous pouvez donc lire et écrire dans le cache plusieurs fois. Cette mise en cache persistante signifie que même si vous avez besoin de reconstruire une couche, vous ne téléchargez que les paquets nouveaux ou modifiés.

To use cache mounts in a build, you can use the --mount flag with the RUN instruction in your Dockerfile:

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

In this example, the npm install command uses a cache mount for the /root/.npm directory, the default location for the npm cache. The cache mount is persisted across builds, so even if you end up rebuilding the layer, you only download new or changed packages. Any changes to the cache are persisted across builds, and the cache is shared between multiple builds.

How you specify cache mounts depends on the build tool you're using. If you're unsure how to specify cache mounts, refer to the documentation for the build tool you're using. Here are a few examples:

RUN --mount=type=cache,target=/go/pkg/mod \
    go build -o /app/hello
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
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt
RUN --mount=type=cache,target=/root/.gem \
    bundle install
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
RUN --mount=type=cache,target=/root/.nuget/packages \
    dotnet restore
RUN --mount=type=cache,target=/tmp/cache \
    composer install

It's important that you read the documentation for the build tool you're using to make sure you're using the correct cache mount options. Package managers have different requirements for how they use the cache, and using the wrong options can lead to unexpected behavior. For example, Apt needs exclusive access to its data, so the caches use the option sharing=locked to ensure parallel builds using the same cache mount wait for each other and not access the same cache files at the same time.

Use an external cache

The default cache storage for builds is internal to the builder (BuildKit instance) you're using. Each builder uses its own cache storage. When you switch between different builders, the cache is not shared between them. Using an external cache lets you define a remote location for pushing and pulling cache data.

External caches are especially useful for CI/CD pipelines, where the builders are often ephemeral, and build minutes are precious. Reusing the cache between builds can drastically speed up the build process and reduce cost. You can even make use of the same cache in your local development environment.

To use an external cache, you specify the --cache-to and --cache-from options with the docker buildx build command.

  • --cache-to exports the build cache to the specified location.
  • --cache-from specifies remote caches for the build to use.

The following example shows how to set up a GitHub Actions workflow using docker/build-push-action, and push the build cache layers to an OCI registry image:

.github/workflows/ci.yml
name: ci

on:
  push:

jobs:
  docker:
    runs-on: ubuntu-latest
    steps:
      - name: Login to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ vars.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build and push
        uses: docker/build-push-action@v6
        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

This setup tells BuildKit to look for cache in the user/app:buildcache image. And when the build is done, the new build cache is pushed to the same image, overwriting the old cache.

This cache can be used locally as well. To pull the cache in a local build, you can use the --cache-from option with the docker buildx build command:

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

Summary

Optimizing cache usage in builds can significantly speed up the build process. Keeping the build context small, using bind mounts, cache mounts, and external caches are all techniques you can use to make the most of the build cache and speed up the build process.

For more information about the concepts discussed in this guide, see: