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
:
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'optionrw
, 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'instructionRUN
, 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 instructionsCOPY
ouADD
. -
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 commandels
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:
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: