Qu’est ce que l’on entend par optimiser une image de conteneur avec Docker ?
Optimiser veut dire répondre à ces deux questions: Est ce que l’image a une taille honorable ? Est-ce que la création de l’image est rapide ?
Ces questions sont très importantes lorsque vous créez une image de conteneur. Cela vous permet de créer des images optimisées et une image optimisée vous fera gagner de la place pour le stockage de vos images, du temps pour notamment la création de l’image ou le déploiement de cette dernière.
Dans cet article, on va explorer 6 techniques et pratiques qui vous permettront d’optimiser une image avec Docker.
Optimisation #1 : Ignorer certains fichiers
Les fichiers « ignore » sont très présents dans le paysage des outils de développement. Par exemple le .gitignore
permet à un utilisateur de git d’ignorer certains fichiers qui ne seront jamais indexés et donc jamais commit.
Il existe la même chose pour docker, le .dockerignore
.
Quand on construit une image avec docker, on passe à la commande build
un chemin. Le contenu de ce chemin va être envoyé dans un « contexte ». Par la suite, docker ne pourra interagir qu’avec les fichiers présents dans ce contexte, par exemple lors de l’utilisation de la commande COPY
.
Or il existe peut-être des fichiers et des dossiers envoyés au contexte qui sont totalement inutiles à l’image. Leur présence dans le contexte n’implique pas forcément leur présence dans l’image, cela dépend des instructions du Dockerfile. Cependant, envoyer des fichiers inutiles dans le contexte, c’est quand même une petite perte de temps, surtout s’ils sont nombreux ou volumineux.
C’est donc à cela que sert le .dockerignore
, à dire à docker d’ignorer certains fichiers et dossiers qui ne seront pas envoyés au contexte. Vous allez donc peut-être gagner du temps ou éviter d’intégrer des fichiers inutiles dans votre image.
Optimisation #2 : Utiliser des petites images de base
La première ligne de votre Dockerfile commence probablement par l’instruction FROM
.
Dès cette première étape, vous pouvez optimiser votre image de conteneur en choisissant la bonne image de base. Il est possible de choisir parmi un énorme panel d’images de base, un large choix de systèmes d’exploitation est à votre disposition, peu importe le langage ou le type d’application.
La plupart du temps on vous indiquera d’utiliser comme image de base, des images peu volumineuses comme alpine
ou debian-slim
.
Dans la grande majorité des cas, ces images fonctionnent avec n’importe quel type application. Mais il se peut bien évidemment que des effets de bords se produisent. Restez attentif.
Optimisation #3 : Réduire le nombre de couches
Une image de conteneurs est composée de couches. Ces couches s’ajoutent quand vous utilisez des instructions telles que RUN
, COPY
ou ADD
. Plus vous avez de couches, plus la taille de l’image sera élevée.
Il faut donc tenter de réduire le nombre de ces couches au maximum. Il existe beaucoup de façons de réduire le nombre de couches, cela dépend notamment de votre application.
Une technique utilisable à chaque fois, c’est de ne pas utiliser une ligne RUN
pour chaque commande unix
que vous lancez. Au contraire, utilisez l’opérateur &&
pour mettre en chaine vos commandes sous une seule et même instruction RUN
.
Ci-dessous un petit exemple avant/après:
FROM alpine
RUN apk update
RUN apk add openssh
RUN touch file
FROM alpine
RUN apk update && apk add openssh && touch file
Optimisation #4 : Comprendre le cache
Docker possède un cache.
Chaque couche créée lors de la construction d’une image sera mise en cache. Les prochaines fois que cette couche interviendra dans la création d’une image, elle sera récupérée depuis le cache, accélérant ainsi l’achèvement de l’image.
Mais il y a une subtilité, une couche ne dépend pas uniquement de l’instruction à l’origine de cette dernière mais aussi du contenu des couches précédentes.
De façon pratique, pour maximiser l’utilisation du cache, on va préférer placer les instructions qui seront amenées à changer souvent en bas du Dockerfile
et ainsi réutiliser ce qui n’a pas bougé à chaque build
. Des fichiers qui changent souvent c’est par exemple le code de l’application, tout simplement.
Mais il faut quand même faire attention, le cache peut avoir des effets indésirés.
Pour les instructions ADD
ou COPY
, les fichiers ajoutés sont analysés pour savoir si le cache peut être réutilisé.
Cependant pour la commande RUN
seulement le contenu de l’instruction est pris en compte. Donc le cache peut etre utilisé lorsque ce n’est pas voulu par le développeur. Par exemple lors d’une mise à jour des paquets du système d’exploitation comme la commande apk update
pour alpine.
Optimisation #5 : Faire du multi-stage
Il est possible avec docker de construire des images en plusieurs étapes.
Concrètement, cela se représente par l’utilisation multiple de l’instruction FROM
. La principale fonctionnalité sera la possibilité de copier des fichiers entre les différentes étapes.
Maintenant, dans quels cas cette technique va-t-elle nous servir afin d’optimiser notre image ?
Le cas le plus répandu c’est lorsqu’on à faire à une application compilée. La première étape de la création de l’image correspondra à la création du binaire. L’image de base de cette étape aura toutes les dépendances nécessaires pour créer ce binaire.
Ensuite on passe à l’étape suivante qui consistera simplement à récupérer ce qui a été compilé à l’étape précédente. Et par opposition à l’étape précédente, l’image de base cette fois-ci aura ce qu’il faut pour lancer l’application.
C’est cette dernière étape qui formera l’image finale de l’application, une image beaucoup moins volumineuse que si l’on avait fait la même chose sans « multi-stage build ».
Voici un exemple avec une application en Golang :
FROM golang:1.11.18.3-alpine AS builder
WORKDIR /go/src/github.com/laupse/example/
COPY app.go ./
RUN go build -o app app.go
FROM alpine:3.16.0
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /go/src/github.com/laupse/example/ ./
CMD ["./app"]
On remarque que la première étape utilise l’image de Golang et est tagguée builder
. Première étape qui avec go build
construira un binaire.
Ensuite vient la deuxième étape, qui part d’une image Alpine. Ici, on récupère le binaire avec le paramètre --from=builder
de l’instruction COPY
.
Et quand j’évoquais l’avantage sur la taille de l’image, voici la différence entre ces deux images de base :
golang 1.18.3-alpine 328MB
alpine 3.16.0 5.53MB
Plutôt pas mal comme différence, non ?
Optimisation #6 : Limiter ce que vous mettez
Et pour terminer, ne mettez pas des ressources inutiles dans votre image. Cela fait un peu écho au .dockerignore
mais il existe d’autres situations ou ce dernier ne vous aidera pas.
Par exemple, des paquets que vous installez via la « package manager » de votre OS peuvent être des ressources qui sont juste recommandées. Vous pouvez ajouter des arguments du type --no-install-recommends
sur Ubuntu. Bien évidemment si cela casse quelque chose dans votre appli, il faudra peut être regarder les paquets de plus près.
Autre exemple, si votre application a besoin de données ou de configuration, ne les intégrez pas directement à l’image. Préférez utiliser des méthodes alternatives comme les volumes qui interviendront au moment de lancer votre conteneur, en plus d’alléger votre image vous allez obtenir de la flexibilité.
A votre tour maintenant
C’est terminé pour cet article. Ces six optimisations ne sont pas toutes équivalentes. Certaines apporteront plus que d’autres, mais il faut essayer des les avoir en tête lorsqu’on écrit des Dockerfile.
Je vous partage un répertoire Github qui vous donnera des exemples appliquant les optimisations ci-dessus https://github.com/inpulse-tv/docker-optimisation
Si vous avez optimisé vos images suite à cet article, n’hésitez pas à partager vos réussites à travers mes réseaux. Les liens sont juste en dessous !
0 commentaire