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

Pilotes de stockage

Pour utiliser efficacement les pilotes de stockage, il est important de savoir comment Docker construit et stocke les images, et comment ces images sont utilisées par les conteneurs. Vous pouvez utiliser ces informations pour faire des choix éclairés sur la meilleure façon de persister les données de vos applications et éviter les problèmes de performance en cours de route.

Pilotes de stockage versus volumes Docker

Docker utilise des pilotes de stockage pour stocker les couches d'image, et pour stocker des données dans la couche modifiable d'un conteneur. La couche modifiable du conteneur ne persiste pas après la suppression du conteneur, mais convient pour stocker des données éphémères qui sont générées à l'exécution. Les pilotes de stockage sont optimisés pour l'efficacité de l'espace, mais (selon le pilote de stockage) les vitesses d'écriture sont inférieures aux performances du système de fichiers natif, en particulier pour les pilotes de stockage qui utilisent un système de fichiers copy-on-write. Les applications intensives en écriture, comme le stockage de base de données, sont impactées par une surcharge de performance, en particulier si des données préexistantes existent dans la couche en lecture seule.

Utilisez les volumes Docker pour les données intensives en écriture, les données qui doivent persister au-delà de la durée de vie du conteneur, et les données qui doivent être partagées entre conteneurs. Consultez la section volumes pour apprendre comment utiliser les volumes pour persister les données et améliorer les performances.

Images et couches

Une image Docker est construite à partir d'une série de couches. Chaque couche représente une instruction dans le Dockerfile de l'image. Chaque couche sauf la toute dernière est en lecture seule. Considérez le Dockerfile suivant :

# syntax=docker/dockerfile:1

FROM ubuntu:22.04
LABEL org.opencontainers.image.authors="[email protected]"
COPY . /app
RUN make /app
RUN rm -r $HOME/.cache
CMD python /app/app.py

Ce Dockerfile contient quatre commandes. Les commandes qui modifient le système de fichiers créent une nouvelle couche. L'instruction FROM commence par créer une couche à partir de l'image ubuntu:22.04. La commande LABEL ne modifie que les métadonnées de l'image, et ne produit pas de nouvelle couche. La commande COPY ajoute quelques fichiers du répertoire actuel de votre client Docker. La première commande RUN construit votre application en utilisant la commande make, et écrit le résultat dans une nouvelle couche. La seconde commande RUN supprime un répertoire de cache, et écrit le résultat dans une nouvelle couche. Enfin, l'instruction CMD spécifie quelle commande exécuter dans le conteneur, ce qui ne modifie que les métadonnées de l'image, ce qui ne produit pas de couche d'image.

Chaque couche n'est qu'un ensemble de différences par rapport à la couche précédente. Notez qu'à la fois ajouter et supprimer des fichiers résultera en une nouvelle couche. Dans l'exemple ci-dessus, le répertoire $HOME/.cache est supprimé, mais sera toujours disponible dans la couche précédente et s'ajoutera à la taille totale de l'image. Consultez les Meilleures pratiques pour écrire des Dockerfiles et utiliser les constructions multi-étapes sections pour apprendre comment optimiser vos Dockerfiles pour des images efficaces.

Les couches sont empilées les unes sur les autres. Lorsque vous créez un nouveau conteneur, vous ajoutez une nouvelle couche modifiable au-dessus des couches sous-jacentes. Cette couche est souvent appelée la "couche de conteneur". Tous les changements apportés au conteneur en cours d'exécution, comme l'écriture de nouveaux fichiers, la modification de fichiers existants et la suppression de fichiers, sont écrits dans cette fine couche de conteneur modifiable. Le diagramme ci-dessous montre un conteneur basé sur une image ubuntu:15.04.

Couches d'un conteneur basé sur l'image Ubuntu

Un pilote de stockage gère les détails sur la façon dont ces couches interagissent les unes avec les autres. Différents pilotes de stockage sont disponibles, qui ont des avantages et des inconvénients dans différentes situations.

Conteneur et couches

La différence majeure entre un conteneur et une image est la couche modifiable supérieure. Toutes les écritures dans le conteneur qui ajoutent de nouvelles données ou modifient des données existantes sont stockées dans cette couche modifiable. Lorsque le conteneur est supprimé, la couche modifiable est également supprimée. L'image sous-jacente reste inchangée.

Parce que chaque conteneur a sa propre couche de conteneur modifiable, et que tous les changements sont stockés dans cette couche de conteneur, plusieurs conteneurs peuvent partager l'accès à la même image sous-jacente et pourtant avoir leur propre état de données. Le diagramme ci-dessous montre plusieurs conteneurs partageant la même image Ubuntu 15.04.

Conteneurs partageant la même image

Docker utilise des pilotes de stockage pour gérer le contenu des couches d'image et de la couche de conteneur modifiable. Chaque pilote de stockage gère l'implémentation différemment, mais tous les pilotes utilisent des couches d'image empilables et la stratégie copy-on-write (CoW).

Note

Utilisez les volumes Docker si vous avez besoin que plusieurs conteneurs aient un accès partagé aux mêmes données exactes. Consultez la section volumes pour en savoir plus sur les volumes.

Taille du conteneur sur disque

Pour voir la taille approximative d'un conteneur en cours d'exécution, vous pouvez utiliser la commande docker ps -s. Deux colonnes différentes se rapportent à la taille.

  • size : la quantité de données (sur disque) qui est utilisée pour la couche modifiable de chaque conteneur.
  • virtual size : la quantité de données utilisée pour les données d'image en lecture seule utilisées par le conteneur plus la size de la couche modifiable du conteneur. Plusieurs conteneurs peuvent partager une partie ou toutes les données d'image en lecture seule. Deux conteneurs démarrés à partir de la même image partagent 100% des données en lecture seule, tandis que deux conteneurs avec des images différentes qui ont des couches en commun partagent ces couches communes. Par conséquent, vous ne pouvez pas simplement totaliser les tailles virtuelles. Cela surestime l'utilisation totale du disque d'une quantité potentiellement non négligeable.

L'espace disque total utilisé par tous les conteneurs en cours d'exécution sur disque est une combinaison des valeurs size et virtual size de chaque conteneur. Si plusieurs conteneurs démarrés à partir de la même image exacte, la taille totale sur disque pour ces conteneurs serait SOMME (size des conteneurs) plus une taille d'image (virtual size - size).

Cela ne compte pas non plus les façons supplémentaires suivantes dont un conteneur peut occuper de l'espace disque :

  • Espace disque utilisé pour les fichiers de log stockés par le logging-driver. Cela peut être non négligeable si votre conteneur génère une grande quantité de données de journalisation et que la rotation des logs n'est pas configurée.
  • Volumes et montages bind utilisés par le conteneur.
  • Espace disque utilisé pour les fichiers de configuration du conteneur, qui sont généralement petits.
  • Mémoire écrite sur disque (si le swap est activé).
  • Points de contrôle, si vous utilisez la fonctionnalité expérimentale checkpoint/restore.

La stratégie copy-on-write (CoW)

Copy-on-write est une stratégie de partage et de copie de fichiers pour une efficacité maximale. Si un fichier ou répertoire existe dans une couche inférieure de l'image, et qu'une autre couche (y compris la couche modifiable) a besoin d'un accès en lecture, elle utilise simplement le fichier existant. La première fois qu'une autre couche a besoin de modifier le fichier (lors de la construction de l'image ou de l'exécution du conteneur), le fichier est copié dans cette couche et modifié. Cela minimise les E/S et la taille de chacune des couches suivantes. Ces avantages sont expliqués plus en détail ci-dessous.

Le partage favorise des images plus petites

Lorsque vous utilisez docker pull pour télécharger une image depuis un dépôt, ou lorsque vous créez un conteneur à partir d'une image qui n'existe pas encore localement, chaque couche est téléchargée séparément, et stockée dans la zone de stockage locale de Docker, qui est généralement /var/lib/docker/ sur les hôtes Linux. Vous pouvez voir ces couches être téléchargées dans cet exemple :

$ docker pull ubuntu:22.04
22.04: Pulling from library/ubuntu
f476d66f5408: Pull complete
8882c27f669e: Pull complete
d9af21273955: Pull complete
f5029279ec12: Pull complete
Digest: sha256:6120be6a2b7ce665d0cbddc3ce6eae60fe94637c6a66985312d1f02f63cc0bcd
Status: Downloaded newer image for ubuntu:22.04
docker.io/library/ubuntu:22.04

Chacune de ces couches est stockée dans son propre répertoire à l'intérieur de la zone de stockage locale de l'hôte Docker. Pour examiner les couches sur le système de fichiers, listez le contenu de /var/lib/docker/<storage-driver>. Cet exemple utilise le pilote de stockage overlay2 :

$ ls /var/lib/docker/overlay2
16802227a96c24dcbeab5b37821e2b67a9f921749cd9a2e386d5a6d5bc6fc6d3
377d73dbb466e0bc7c9ee23166771b35ebdbe02ef17753d79fd3571d4ce659d7
3f02d96212b03e3383160d31d7c6aeca750d2d8a1879965b89fe8146594c453d
ec1ec45792908e90484f7e629330666e7eee599f08729c93890a7205a6ba35f5
l

Les noms de répertoire ne correspondent pas aux ID de couche.

Now imagine that you have two different Dockerfiles. You use the first one to create an image called acme/my-base-image:1.0.

# syntax=docker/dockerfile:1
FROM alpine
RUN apk add --no-cache bash

The second one is based on acme/my-base-image:1.0, but has some additional layers:

# syntax=docker/dockerfile:1
FROM acme/my-base-image:1.0
COPY . /app
RUN chmod +x /app/hello.sh
CMD /app/hello.sh

The second image contains all the layers from the first image, plus new layers created by the COPY and RUN instructions, and a read-write container layer. Docker already has all the layers from the first image, so it doesn't need to pull them again. The two images share any layers they have in common.

If you build images from the two Dockerfiles, you can use docker image ls and docker image history commands to verify that the cryptographic IDs of the shared layers are the same.

  1. Make a new directory cow-test/ and change into it.

  2. Within cow-test/, create a new file called hello.sh with the following contents.

    #!/usr/bin/env bash
    echo "Hello world"
  3. Copy the contents of the first Dockerfile above into a new file called Dockerfile.base.

  4. Copy the contents of the second Dockerfile above into a new file called Dockerfile.

  5. Within the cow-test/ directory, build the first image. Don't forget to include the final . in the command. That sets the PATH, which tells Docker where to look for any files that need to be added to the image.

    $ docker build -t acme/my-base-image:1.0 -f Dockerfile.base .
    [+] Building 6.0s (11/11) FINISHED
    => [internal] load build definition from Dockerfile.base                                      0.4s
    => => transferring dockerfile: 116B                                                           0.0s
    => [internal] load .dockerignore                                                              0.3s
    => => transferring context: 2B                                                                0.0s
    => resolve image config for docker.io/docker/dockerfile:1                                     1.5s
    => [auth] docker/dockerfile:pull token for registry-1.docker.io                               0.0s
    => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671... 0.0s
    => [internal] load .dockerignore                                                              0.0s
    => [internal] load build definition from Dockerfile.base                                      0.0s
    => [internal] load metadata for docker.io/library/alpine:latest                               0.0s
    => CACHED [1/2] FROM docker.io/library/alpine                                                 0.0s
    => [2/2] RUN apk add --no-cache bash                                                          3.1s
    => exporting to image                                                                         0.2s
    => => exporting layers                                                                        0.2s
    => => writing image sha256:da3cf8df55ee9777ddcd5afc40fffc3ead816bda99430bad2257de4459625eaa   0.0s
    => => naming to docker.io/acme/my-base-image:1.0                                              0.0s
    
  6. Build the second image.

    $ docker build -t acme/my-final-image:1.0 -f Dockerfile .
    
    [+] Building 3.6s (12/12) FINISHED
    => [internal] load build definition from Dockerfile                                            0.1s
    => => transferring dockerfile: 156B                                                            0.0s
    => [internal] load .dockerignore                                                               0.1s
    => => transferring context: 2B                                                                 0.0s
    => resolve image config for docker.io/docker/dockerfile:1                                      0.5s
    => CACHED docker-image://docker.io/docker/dockerfile:1@sha256:9e2c9eca7367393aecc68795c671...  0.0s
    => [internal] load .dockerignore                                                               0.0s
    => [internal] load build definition from Dockerfile                                            0.0s
    => [internal] load metadata for docker.io/acme/my-base-image:1.0                               0.0s
    => [internal] load build context                                                               0.2s
    => => transferring context: 340B                                                               0.0s
    => [1/3] FROM docker.io/acme/my-base-image:1.0                                                 0.2s
    => [2/3] COPY . /app                                                                           0.1s
    => [3/3] RUN chmod +x /app/hello.sh                                                            0.4s
    => exporting to image                                                                          0.1s
    => => exporting layers                                                                         0.1s
    => => writing image sha256:8bd85c42fa7ff6b33902ada7dcefaaae112bf5673873a089d73583b0074313dd    0.0s
    => => naming to docker.io/acme/my-final-image:1.0                                              0.0s
    
  7. Check out the sizes of the images.

    $ docker image ls
    
    REPOSITORY             TAG     IMAGE ID         CREATED               SIZE
    acme/my-final-image    1.0     8bd85c42fa7f     About a minute ago    7.75MB
    acme/my-base-image     1.0     da3cf8df55ee     2 minutes ago         7.75MB
    
  8. Check out the history of each image.

    $ docker image history acme/my-base-image:1.0
    
    IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
    da3cf8df55ee   5 minutes ago   RUN /bin/sh -c apk add --no-cache bash # bui…   2.15MB    buildkit.dockerfile.v0
    <missing>      7 weeks ago     /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
    <missing>      7 weeks ago     /bin/sh -c #(nop) ADD file:f278386b0cef68136…   5.6MB
    

    Some steps don't have a size (0B), and are metadata-only changes, which do not produce an image layer and don't take up any size, other than the metadata itself. The output above shows that this image consists of 2 image layers.

    $ docker image history  acme/my-final-image:1.0
    
    IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
    8bd85c42fa7f   3 minutes ago   CMD ["/bin/sh" "-c" "/app/hello.sh"]            0B        buildkit.dockerfile.v0
    <missing>      3 minutes ago   RUN /bin/sh -c chmod +x /app/hello.sh # buil…   39B       buildkit.dockerfile.v0
    <missing>      3 minutes ago   COPY . /app # buildkit                          222B      buildkit.dockerfile.v0
    <missing>      4 minutes ago   RUN /bin/sh -c apk add --no-cache bash # bui…   2.15MB    buildkit.dockerfile.v0
    <missing>      7 weeks ago     /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
    <missing>      7 weeks ago     /bin/sh -c #(nop) ADD file:f278386b0cef68136…   5.6MB
    

    Notice that all steps of the first image are also included in the final image. The final image includes the two layers from the first image, and two layers that were added in the second image.

    The <missing> lines in the docker history output indicate that those steps were either built on another system and part of the alpine image that was pulled from Docker Hub, or were built with BuildKit as builder. Before BuildKit, the "classic" builder would produce a new "intermediate" image for each step for caching purposes, and the IMAGE column would show the ID of that image.

    BuildKit uses its own caching mechanism, and no longer requires intermediate images for caching. Refer to BuildKit to learn more about other enhancements made in BuildKit.

  9. Check out the layers for each image

    Use the docker image inspect command to view the cryptographic IDs of the layers in each image:

    $ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-base-image:1.0
    [
      "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf",
      "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a"
    ]
    
    $ docker image inspect --format "{{json .RootFS.Layers}}" acme/my-final-image:1.0
    [
      "sha256:72e830a4dff5f0d5225cdc0a320e85ab1ce06ea5673acfe8d83a7645cbd0e9cf",
      "sha256:07b4a9068b6af337e8b8f1f1dae3dd14185b2c0003a9a1f0a6fd2587495b204a",
      "sha256:cc644054967e516db4689b5282ee98e4bc4b11ea2255c9630309f559ab96562e",
      "sha256:e84fb818852626e89a09f5143dbc31fe7f0e0a6a24cd8d2eb68062b904337af4"
    ]
    

    Notice that the first two layers are identical in both images. The second image adds two additional layers. Shared image layers are only stored once in /var/lib/docker/ and are also shared when pushing and pulling an image to an image registry. Shared image layers can therefore reduce network bandwidth and storage.

    Tip

    Format output of Docker commands with the --format option.

    The examples above use the docker image inspect command with the --format option to view the layer IDs, formatted as a JSON array. The --format option on Docker commands can be a powerful feature that allows you to extract and format specific information from the output, without requiring additional tools such as awk or sed. To learn more about formatting the output of docker commands using the --format flag, refer to the format command and log output section. We also pretty-printed the JSON output using the jq utility for readability.

Copying makes containers efficient

When you start a container, a thin writable container layer is added on top of the other layers. Any changes the container makes to the filesystem are stored here. Any files the container doesn't change don't get copied to this writable layer. This means that the writable layer is as small as possible.

When an existing file in a container is modified, the storage driver performs a copy-on-write operation. The specific steps involved depend on the specific storage driver. For the overlay2 driver, the copy-on-write operation follows this rough sequence:

  • Search through the image layers for the file to update. The process starts at the newest layer and works down to the base layer one layer at a time. When results are found, they're added to a cache to speed future operations.
  • Perform a copy_up operation on the first copy of the file that's found, to copy the file to the container's writable layer.
  • Any modifications are made to this copy of the file, and the container can't see the read-only copy of the file that exists in the lower layer.

Btrfs, ZFS, and other drivers handle the copy-on-write differently. You can read more about the methods of these drivers later in their detailed descriptions.

Containers that write a lot of data consume more space than containers that don't. This is because most write operations consume new space in the container's thin writable top layer. Note that changing the metadata of files, for example, changing file permissions or ownership of a file, can also result in a copy_up operation, therefore duplicating the file to the writable layer.

Tip

Use volumes for write-heavy applications.

Don't store the data in the container for write-heavy applications. Such applications, for example write-intensive databases, are known to be problematic particularly when pre-existing data exists in the read-only layer.

Instead, use Docker volumes, which are independent of the running container, and designed to be efficient for I/O. In addition, volumes can be shared among containers and don't increase the size of your container's writable layer. Refer to the use volumes section to learn about volumes.

A copy_up operation can incur a noticeable performance overhead. This overhead is different depending on which storage driver is in use. Large files, lots of layers, and deep directory trees can make the impact more noticeable. This is mitigated by the fact that each copy_up operation only occurs the first time a given file is modified.

To verify the way that copy-on-write works, the following procedure spins up 5 containers based on the acme/my-final-image:1.0 image we built earlier and examines how much room they take up.

  1. From a terminal on your Docker host, run the following docker run commands. The strings at the end are the IDs of each container.

    $ docker run -dit --name my_container_1 acme/my-final-image:1.0 bash \
      && docker run -dit --name my_container_2 acme/my-final-image:1.0 bash \
      && docker run -dit --name my_container_3 acme/my-final-image:1.0 bash \
      && docker run -dit --name my_container_4 acme/my-final-image:1.0 bash \
      && docker run -dit --name my_container_5 acme/my-final-image:1.0 bash
    
    40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c
    a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107
    3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc
    939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39
    cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
    
  2. Run the docker ps command with the --size option to verify the 5 containers are running, and to see each container's size.

    $ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}"
    
    CONTAINER ID   IMAGE                     NAMES            SIZE
    cddae31c314f   acme/my-final-image:1.0   my_container_5   0B (virtual 7.75MB)
    939b3bf9e7ec   acme/my-final-image:1.0   my_container_4   0B (virtual 7.75MB)
    3ed3c1a10430   acme/my-final-image:1.0   my_container_3   0B (virtual 7.75MB)
    a5ff32e2b551   acme/my-final-image:1.0   my_container_2   0B (virtual 7.75MB)
    40ebdd763416   acme/my-final-image:1.0   my_container_1   0B (virtual 7.75MB)
    

    The output above shows that all containers share the image's read-only layers (7.75MB), but no data was written to the container's filesystem, so no additional storage is used for the containers.

    Note

    This step requires a Linux machine, and doesn't work on Docker Desktop, as it requires access to the Docker Daemon's file storage.

    While the output of docker ps provides you information about disk space consumed by a container's writable layer, it doesn't include information about metadata and log-files stored for each container.

    More details can be obtained by exploring the Docker Daemon's storage location (/var/lib/docker by default).

    $ sudo du -sh /var/lib/docker/containers/*
    
    36K  /var/lib/docker/containers/3ed3c1a10430e09f253704116965b01ca920202d52f3bf381fbb833b8ae356bc
    36K  /var/lib/docker/containers/40ebdd7634162eb42bdb1ba76a395095527e9c0aa40348e6c325bd0aa289423c
    36K  /var/lib/docker/containers/939b3bf9e7ece24bcffec57d974c939da2bdcc6a5077b5459c897c1e2fa37a39
    36K  /var/lib/docker/containers/a5ff32e2b551168b9498870faf16c9cd0af820edf8a5c157f7b80da59d01a107
    36K  /var/lib/docker/containers/cddae31c314fbab3f7eabeb9b26733838187abc9a2ed53f97bd5b04cd7984a5a
    

    Each of these containers only takes up 36k of space on the filesystem.

  3. Per-container storage

    To demonstrate this, run the following command to write the word 'hello' to a file on the container's writable layer in containers my_container_1, my_container_2, and my_container_3:

    $ for i in {1..3}; do docker exec my_container_$i sh -c 'printf hello > /out.txt'; done
    

    Running the docker ps command again afterward shows that those containers now consume 5 bytes each. This data is unique to each container, and not shared. The read-only layers of the containers aren't affected, and are still shared by all containers.

    $ docker ps --size --format "table {{.ID}}\t{{.Image}}\t{{.Names}}\t{{.Size}}"
    
    CONTAINER ID   IMAGE                     NAMES            SIZE
    cddae31c314f   acme/my-final-image:1.0   my_container_5   0B (virtual 7.75MB)
    939b3bf9e7ec   acme/my-final-image:1.0   my_container_4   0B (virtual 7.75MB)
    3ed3c1a10430   acme/my-final-image:1.0   my_container_3   5B (virtual 7.75MB)
    a5ff32e2b551   acme/my-final-image:1.0   my_container_2   5B (virtual 7.75MB)
    40ebdd763416   acme/my-final-image:1.0   my_container_1   5B (virtual 7.75MB)
    

The previous examples illustrate how copy-on-write filesystems help make containers efficient. Not only does copy-on-write save space, but it also reduces container start-up time. When you create a container (or multiple containers from the same image), Docker only needs to create the thin writable container layer.

If Docker had to make an entire copy of the underlying image stack each time it created a new container, container creation times and disk space used would be significantly increased. This would be similar to the way that virtual machines work, with one or more virtual disks per virtual machine. The vfs storage doesn't provide a CoW filesystem or other optimizations. When using this storage driver, a full copy of the image's data is created for each container.