Conteneurs distroless : réduire l’image sans sacrifier la sécurité
Une image Docker embarque souvent bien plus que ce dont une application a besoin pour s’exécuter. Gestionnaire de paquets, shell, utilitaires système, bibliothèques inutilisées et fichiers de documentation augmentent la taille finale, élargissent la surface d’attaque et ralentissent les déploiements.
Les images distroless répondent à ce problème en conservant uniquement les composants nécessaires au démarrage du processus applicatif. Associées à une construction multi-étapes et à une bonne analyse des dépendances, elles permettent de produire des conteneurs légers, reproductibles et plus difficiles à compromettre.
Pourquoi supprimer l’image de base classique
Une image comme node:22 ou python:3.12 contient un environnement généraliste pratique pour développer. Elle fournit un shell, des certificats, des outils de diagnostic et un gestionnaire de paquets. Ces éléments facilitent le dépannage, mais ils ne sont pas toujours nécessaires en production.
Une image minimale réduit la quantité de code susceptible de contenir une vulnérabilité. Elle diminue aussi le temps de téléchargement depuis le registre, la consommation de stockage et la durée de démarrage des workloads Kubernetes. Le gain est particulièrement visible lorsque plusieurs replicas téléchargent la même image sur des nœuds vierges.
| Approche | Contenu habituel | Usage adapté | Limites |
|---|---|---|---|
| Image standard | Runtime, shell, outils, paquets système | Développement et diagnostic | Taille et surface d’attaque élevées |
| Image alpine | Runtime compact, utilitaires réduits | Production généraliste | Compatibilité libc à contrôler |
| Image distroless | Runtime et bibliothèques strictement nécessaires | Exécution en production | Débogage depuis le conteneur limité |
scratch |
Aucun système de fichiers initial | Binaire statique autonome | Configuration et certificats à gérer |
Le terme « sans image de base » désigne souvent l’utilisation de scratch, tandis que distroless fournit une base minimale spécialisée. Dans les deux cas, l’objectif est de ne pas transporter un système d’exploitation complet autour de l’application.
Comprendre le modèle distroless
Une image distroless ne correspond pas à une distribution Linux miniature traditionnelle. Elle ne propose généralement ni shell, ni apt, ni apk, ni outils interactifs. Elle contient le runtime, les bibliothèques nécessaires, les certificats CA et quelques fichiers indispensables au lancement.
Cette contrainte modifie les habitudes d’exploitation. Une commande comme docker exec -it conteneur sh échoue volontairement. Les logs, les métriques, les traces et les tests externes doivent donc être prévus avant le déploiement. Le conteneur devient un artefact d’exécution, pas un serveur à administrer manuellement.
Pour Node.js, une variante distroless adaptée au runtime permet de copier uniquement le code compilé et les dépendances de production. Pour Go ou Rust, un binaire statique peut fonctionner avec scratch, à condition de traiter correctement les certificats, les fuseaux horaires et la résolution DNS.
Construire avec plusieurs étapes Docker
La construction multi-étapes sépare l’environnement de compilation de celui qui exécute le programme. La première étape installe les dépendances, lance les tests et produit les artefacts. La seconde repart d’une image distroless ou vide et ne récupère que ces fichiers.
FROM node:22 AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
FROM gcr.io/distroless/nodejs22-debian12
WORKDIR /app
COPY --from=build /app/package*.json ./
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/dist ./dist
USER nonroot
CMD ["dist/server.js"]
Le fichier .dockerignore doit exclure .git, les journaux, les caches, les fichiers de test et les dépendances locales. Il faut également verrouiller les versions avec un fichier de lock, utiliser un utilisateur non privilégié et éviter de copier l’ensemble du répertoire de travail dans l’étape finale.
Réduire efficacement les octets embarqués
La taille ne dépend pas uniquement de l’image choisie. Les dépendances applicatives représentent souvent la plus grande part du volume final. Supprimer les modules inutilisés, préférer les imports ciblés et produire un bundle optimisé peuvent offrir un gain supérieur au remplacement d’une distribution par une autre.
Pour les applications JavaScript, npm ci --omit=dev, le tree shaking et la compilation TypeScript limitent les fichiers transportés. Il faut toutefois vérifier les dépendances dynamiques, les fichiers de configuration et les ressources chargées au runtime. Une optimisation agressive qui oublie un template ou un certificat provoquera un échec en production.
Vérifications utiles avant publication
- Mesurer chaque couche avec
docker historyetdocker image inspect. - Scanner l’image avec Trivy, Grype ou une solution intégrée au registre.
- Comparer la taille compressée et la taille extraite sur le nœud.
- Contrôler les fichiers réellement ouverts par l’application.
L’ordre des instructions Docker influence aussi le cache. Les fichiers de manifeste doivent être copiés avant le code source afin de réutiliser l’installation des dépendances lorsque seul le code métier change.
Éléments à externaliser
- Les logs vers stdout et stderr plutôt que vers un fichier local.
- Les secrets dans un coffre ou des variables injectées au démarrage.
- Les fichiers temporaires vers un volume éphémère dédié.
- Les outils de développement dans une image séparée.
Sécuriser l’exécution sans shell
L’absence de shell limite plusieurs techniques d’attaque post-compromission. Un intrus ne peut pas simplement installer un paquet ou lancer un utilitaire présent par défaut. Cette protection ne remplace toutefois pas les contrôles applicatifs : validation des entrées, gestion des secrets, mises à jour du runtime et principe du moindre privilège restent essentiels.
Le conteneur doit fonctionner avec un utilisateur non root, un système de fichiers en lecture seule lorsque c’est possible et des capacités Linux supprimées. Les paramètres readOnlyRootFilesystem, allowPrivilegeEscalation: false et capabilities.drop: ["ALL"] sont particulièrement pertinents dans Kubernetes.
Les probes doivent appeler un endpoint applicatif simple ou vérifier un mécanisme interne, plutôt que dépendre de curl ou wget installé dans l’image. Pour diagnostiquer les incidents, il est préférable d’utiliser un conteneur éphémère de debug dans le même pod, ou de reproduire le problème avec l’artefact de build correspondant.
Observer une application minimale
Une image réduite ne doit pas devenir une boîte noire. Les métriques Prometheus, les logs structurés et les traces distribuées doivent être intégrés au code ou fournis par l’infrastructure. L’application peut ainsi être observée sans ajouter de commandes système dans le conteneur.
La télémétrie OpenTelemetry permet de corréler les requêtes, les appels sortants et les erreurs entre services. Les équipes qui utilisent .NET peuvent consulter ce guide sur l’instrumentation avec Grafana, dont les principes restent utiles pour concevoir une observabilité indépendante du système de fichiers local.
Les signaux de santé doivent distinguer l’état du processus, la disponibilité de l’application et celle de ses dépendances. Une sonde trop large peut redémarrer inutilement un conteneur lors d’une panne de base de données ; une sonde trop permissive peut laisser du trafic atteindre une instance dégradée.
Adapter les dépendances et la chaîne CI/CD
La réduction d’image commence dans le projet. Une dépendance abandonnée, un module complet importé pour une fonction mineure ou une bibliothèque native mal maîtrisée compliquent le passage à une image distroless. Pour un projet Node.js, comparer les solutions d’accès aux données dans ce comparatif des ORM Node.js aide à évaluer le poids des bibliothèques et leurs contraintes de production.
La pipeline doit construire, tester, scanner et signer l’image avant sa publication. Un contrôle de taille peut bloquer une régression, tandis qu’un SBOM permet d’identifier les composants présents dans l’artefact. Le déploiement doit référencer un digest immuable plutôt qu’un tag mutable comme latest.
Les applications qui manipulent des sessions ou des cookies doivent aussi séparer la sécurité applicative de celle du conteneur. Les recommandations de ce guide sur la sécurisation des cookies Node.js complètent les réglages Docker, notamment pour les attributs HttpOnly, Secure et SameSite.
Choisir le bon niveau de minimalisme
scratch convient surtout aux binaires statiques autonomes. Il réduit fortement la surface disponible, mais exige une gestion explicite des certificats racine, des utilisateurs, des fuseaux horaires et parfois des fichiers de configuration. Une image distroless est souvent un compromis plus simple pour Node.js, Java, Python ou les applications nécessitant un runtime.
La taille doit être considérée avec la compatibilité et l’exploitabilité. Une image légèrement plus grande, mais correctement instrumentée et régulièrement mise à jour, sera préférable à un conteneur minuscule impossible à diagnostiquer. Pour un environnement de production, la décision doit s’appuyer sur des mesures : temps de pull, temps de démarrage, vulnérabilités détectées et comportement sous charge.
Pour explorer les usages de l’intelligence artificielle et des outils modernes autour du développement, HelloML constitue également une ressource complémentaire, notamment lorsque des services conteneurisés doivent intégrer des pipelines de traitement ou d’inférence.
Passez au déploiement
Commencez par une application peu critique, construisez-la avec une étape de compilation séparée, puis comparez une image standard, Alpine et distroless. Mesurez la taille, exécutez les tests d’intégration, vérifiez les certificats et observez le comportement des probes avant de généraliser la méthode.
Publiez ensuite un artefact versionné et signé, configurez un utilisateur non privilégié et ajoutez le scan de vulnérabilités à chaque pipeline. Cette démarche transforme la réduction d’image en pratique durable : moins de données transférées, moins de composants exposés et une chaîne de livraison plus maîtrisée.