Création d'images Docker efficaces : meilleures pratiques pour la performance
Créez des images Docker plus petites avec des images de base légères, .dockerignore, des Dockerfiles optimisés pour le cache et des constructions multi-étapes.
Création d'images Docker efficaces : bonnes pratiques pour la performance
Les images Docker efficaces accélèrent vos builds, allègent les déploiements et rendent les conteneurs de production plus faciles à sécuriser. Les images gonflées ralentissent l'IC, gaspillent l'espace de stockage du registre et contiennent souvent des outils dont votre application n'a pas besoin à l'exécution.
L'objectif n'est pas de créer l'image la plus petite possible à tout prix. L'objectif est de construire une image prévisible qui contient votre application, ses dépendances d'exécution, et peu d'autres choses.
Pourquoi l'efficacité des images est importante
Les images Docker optimisées offrent une cascade d'avantages tout au long du cycle de vie du développement logiciel :
- Builds plus rapides : Des contextes plus petits et moins d'opérations entraînent une création d'image plus rapide, accélérant vos pipelines CI/CD.
- Coûts de stockage réduits : Moins d'espace disque consommé sur les registres et les machines hôtes, réduisant les dépenses d'infrastructure.
- Déploiements plus rapides : Les images plus petites se transfèrent plus rapidement sur les réseaux, permettant un déploiement et une mise à l'échelle rapides dans les environnements de production.
- Performances améliorées : Moins de données à charger signifie que les conteneurs démarrent et s'exécutent plus efficacement.
- Sécurité renforcée : Une image plus petite avec moins de dépendances et d'outils présente une surface d'attaque réduite, car il y a moins de vulnérabilités potentielles à exploiter.
- Meilleure expérience développeur : Des boucles de rétroaction plus rapides et moins de temps d'attente contribuent à un environnement de développement plus productif.
Bonnes pratiques Dockerfile pour la performance
Votre Dockerfile est le plan de votre image. L'optimiser est la première et la plus importante étape vers l'efficacité.
1. Choisissez une image de base minimale
L'instruction FROM définit la base de votre image. Commencer avec une image de base plus petite réduit considérablement la taille finale de l'image.
- Alpine Linux : Très petite et utile pour les applications qui fonctionnent bien avec musl libc. Testez attentivement si votre application ou ses dépendances natives attendent un comportement glibc.
- Images Distroless : Fournies par Google, ces images contiennent uniquement votre application et ses dépendances d'exécution, supprimant le shell, les gestionnaires de paquets et autres utilitaires du système d'exploitation. Elles offrent une excellente sécurité et une taille minimale.
- Versions spécifiques de distribution : Évitez les tags génériques comme
ubuntu:latestounode:latest. Utilisez plutôt des versions spécifiques commeubuntu:22.04ounode:18-alpinepour garantir la reproductibilité et la stabilité.
# Mauvais : Image de base volumineuse, potentiellement incohérente
FROM ubuntu:latest
# Bon : Image de base plus petite et plus cohérente
FROM node:18-alpine
# Encore mieux pour les applications compilées (si applicable)
FROM gcr.io/distroless/static
2. Utilisez .dockerignore
Tout comme .gitignore, un fichier .dockerignore empêche les fichiers inutiles d'être copiés dans votre contexte de build. Cela accélère considérablement le processus docker build en réduisant les données que le démon Docker doit traiter.
Créez un fichier nommé .dockerignore à la racine de votre projet :
# Ignorer les fichiers liés à Git
.git
.gitignore
# Ignorer les dépendances Node.js (seront installées dans le conteneur)
node_modules
npm-debug.log
# Ignorer les fichiers de développement locaux
.env
*.log
*.DS_Store
# Ignorer les artefacts de build qui seront créés dans le conteneur
build
dist
3. Minimisez les couches en combinant les instructions RUN
Chaque instruction RUN dans un Dockerfile crée une nouvelle couche. Bien que les couches soient essentielles pour la mise en cache, un trop grand nombre peut gonfler l'image. Combinez les commandes associées en une seule instruction RUN, en utilisant && pour les enchaîner.
# Mauvais : Crée plusieurs couches
RUN apt-get update
RUN apt-get install -y --no-install-recommends git curl
RUN rm -rf /var/lib/apt/lists/*
# Bon : Crée une seule couche et nettoie en une seule fois
RUN apt-get update && \
apt-get install -y --no-install-recommends git curl && \
rm -rf /var/lib/apt/lists/*
Astuce : Incluez toujours les commandes de nettoyage comme rm -rf /var/lib/apt/lists/* pour Debian et Ubuntu dans la même instruction RUN qui installe les paquets. Pour Alpine, préférez apk add --no-cache plutôt que de nettoyer manuellement /var/cache/apk.
4. Ordonnez les instructions Dockerfile de manière optimale
Docker met en cache les couches en fonction de l'ordre des instructions. Placez les instructions les plus stables et les moins fréquemment modifiées en premier dans votre Dockerfile. Cela garantit que Docker peut réutiliser les couches mises en cache des builds précédents, accélérant considérablement les builds suivants.
Ordre général :
FROM(image de base)ARG(arguments de build)ENV(variables d'environnement)WORKDIR(répertoire de travail)COPYpour les dépendances (par exemple,package.json,pom.xml,requirements.txt)RUNpour installer les dépendances (par exemple,npm install,pip install)COPYpour le code source de l'applicationEXPOSE(ports)ENTRYPOINT/CMD(exécution de l'application)
FROM node:18-alpine
WORKDIR /app
# Ces fichiers changent moins fréquemment que le code source, placez-les donc en premier
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# Le code source de l'application change plus fréquemment
COPY . .
CMD ["node", "server.js"]
5. Utilisez des versions de paquets spécifiques
Épingler les versions des paquets installés via les commandes RUN (par exemple, apt-get install mypackage=1.2.3) garantit la reproductibilité et évite les problèmes inattendus ou les augmentations de taille dues aux nouvelles versions de paquets.
6. Évitez d'installer des outils inutiles
Installez uniquement ce qui est strictement nécessaire au fonctionnement de votre application. Les outils de développement, les débogueurs ou les éditeurs de texte n'ont pas leur place dans une image de production.
Utilisation des constructions multi-étapes
Les constructions multi-étapes sont une pierre angulaire de la création efficace d'images Docker. Elles vous permettent d'utiliser plusieurs instructions FROM dans un seul Dockerfile, où chaque FROM commence une nouvelle étape de construction. Vous pouvez ensuite copier sélectivement des artefacts d'une étape vers une étape finale et légère, laissant derrière vous toutes les dépendances de build, les fichiers intermédiaires et les outils.
Cela réduit considérablement la taille finale de l'image et améliore la sécurité en n'incluant que ce qui est nécessaire à l'exécution.
Comment fonctionnent les constructions multi-étapes
- Étape de construction : Cette étape contient tous les outils et dépendances nécessaires pour compiler votre application (par exemple, compilateurs, SDK, bibliothèques de développement). Elle produit l'exécutable ou les artefacts déployables.
- Étape d'exécution : Cette étape part d'une image de base minimale et ne copie que les artefacts nécessaires de l'étape de construction. Elle jette tout le reste de l'étape de construction, résultant en une image finale significativement plus petite.
Exemple de construction multi-étapes (application Go)
Considérons une application Go. La construire nécessite un compilateur Go, mais l'exécutable final n'a besoin que d'un environnement d'exécution.
# Étape 1 : Construction
FROM golang:1.20-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -a -ldflags '-s -w' -o myapp .
# Étape 2 : Exécution
FROM alpine:3.20
WORKDIR /root/
# Copier uniquement l'exécutable compilé depuis l'étape de construction
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]
Dans cet exemple :
- L'étape
builderutilisegolang:1.20-alpinepour compiler l'application Go. - L'étape
runnerpart d'une petite image Alpine et ne copie que l'exécutablemyappdepuis l'étapebuilder, supprimant le SDK Go et les dépendances de build.
Techniques d'optimisation avancées
1. Envisagez d'utiliser COPY --chown
Lors de la copie de fichiers, utilisez --chown pour définir le propriétaire et le groupe sur un utilisateur non root. C'est une bonne pratique de sécurité et peut éviter les problèmes de permissions.
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
# Copier les fichiers directement en tant qu'utilisateur non root
COPY --chown=appuser:appgroup ./app /app
2. N'ajoutez pas d'informations sensibles
Ne codez jamais en dur des secrets (clés API, mots de passe) directement dans votre Dockerfile ou votre image. Utilisez des variables d'environnement, Docker Secrets ou des systèmes de gestion de secrets externes. Les arguments de build (ARG) sont visibles dans l'historique de l'image, donc même les utiliser pour des secrets est risqué.
3. Utilisez les fonctionnalités de BuildKit (si disponibles)
Si votre build Docker utilise BuildKit, vous pouvez utiliser des fonctionnalités comme RUN --mount=type=cache pour les caches de dépendances ou RUN --mount=type=secret pour les secrets de build qui ne doivent pas être intégrés dans l'image.
# Exemple avec le cache BuildKit pour npm
FROM node:18-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
COPY . .
CMD ["node", "server.js"]
À retenir
La création d'images Docker efficaces commence par une habitude simple : faites en sorte que chaque fichier et paquet justifie sa place dans l'image finale. Utilisez une image de base légère, gardez le contexte de build petit, ordonnez les instructions pour la mise en cache et déplacez les compilateurs ou SDK dans une étape de construction.
Points clés à retenir :
- Commencez petit : Choisissez l'image de base la plus petite possible (
Alpine,Distroless). - Soyez intelligent avec les couches : Combinez les commandes
RUNet nettoyez efficacement. - Mettez en cache judicieusement : Ordonnez les instructions pour maximiser les hits de cache.
- Isolez les artefacts de build : Utilisez des constructions multi-étapes pour supprimer les dépendances de build.
- Gardez-le léger : N'incluez que ce qui est absolument nécessaire à l'exécution.
Surveillez en continu la taille de vos images et les temps de build. Des outils comme docker history peuvent vous aider à comprendre comment chaque instruction contribue à la taille finale de l'image. Révisez et refactorisez régulièrement vos Dockerfiles à mesure que votre application évolue pour maintenir une efficacité et des performances optimales.