- Même en exécutant
docker run ubuntu, le conteneur partage le noyau Linux de l’hôte et Ubuntu ne fournit que les outils de l’espace utilisateur
- Le résultat de
uname -r affiche la version du noyau de l’hôte, tandis que seul /etc/os-release indique des informations Ubuntu
- Chaque VM possède son propre noyau et met plusieurs minutes à démarrer, alors qu’un conteneur démarre en quelques millisecondes et partage le noyau de l’hôte via une isolation au niveau du système d’exploitation, sans virtualisation matérielle, ce qui réduit l’overhead
- Grâce à la stabilité de l’ABI des appels système Linux, des conteneurs issus de distributions variées peuvent fonctionner sur un même noyau
- Dans un environnement avec 16 Go de RAM, la limite pratique se situe autour de 50 à 100 conteneurs légers, 10 à 30 de taille moyenne et 5 à 10 grands conteneurs
- Comprendre cette architecture est essentiel, car une vulnérabilité du noyau affecte tous les conteneurs, et le choix de l’image de base a un impact direct sur la compatibilité et la sécurité
Ce que signifie « exécuter Ubuntu »
- En lançant
docker run ubuntu:22.04, on obtient un prompt bash qui ressemble à Ubuntu, et on peut exécuter apt update ainsi qu’installer des paquets
- Pourtant, si l’on exécute
uname -r dans le conteneur, c’est la version du noyau de l’hôte (par ex. 6.5.0-44-generic) qui s’affiche
- Le fichier
/etc/os-release indique Ubuntu 22.04, mais le noyau est celui de la machine hôte, et la partie « Ubuntu » n’est rien d’autre que le système de fichiers qui compose l’espace utilisateur
Conteneurs vs machines virtuelles : comparaison d’architecture
- Les VM virtualisent le matériel, tandis que les conteneurs virtualisent le système d’exploitation
- Principales différences :
- Noyau : chaque VM possède son propre noyau, les conteneurs partagent celui de l’hôte
- Temps de démarrage : plusieurs minutes pour une VM, quelques millisecondes pour un conteneur
- Overhead mémoire : 512 Mo à 4 Go pour une VM, 1 à 10 Mo pour un conteneur
- Utilisation disque : 10 à 100 Go pour une VM, 10 à 500 Mo pour une image de conteneur
- Niveau d’isolation : niveau matériel pour les VM, niveau processus pour les conteneurs
- Performances : environ 5 à 10 % d’overhead pour une VM, performances proches du natif pour les conteneurs
Ce que contient réellement une image de base
- Contenu du tarball téléchargé lors d’un pull de
ubuntu:22.04 :
-
1. Binaires essentiels (/bin, /usr/bin)
/bin/bash (shell), /bin/ls (liste des fichiers), /bin/cat (affichage des fichiers)
/usr/bin/apt (gestionnaire de paquets), /usr/bin/dpkg (outil de paquets Debian)
-
2. Bibliothèques partagées (/lib, /usr/lib)
- glibc et d’autres bibliothèques partagées auxquelles les programmes se lient
/lib/x86_64-linux-gnu/libc.so.6 (bibliothèque C, base de tous les programmes C)
- Bibliothèques essentielles comme
libpthread.so.0, libm.so.6, etc.
-
3. Fichiers de configuration (/etc)
/etc/apt/sources.list (dépôts de paquets)
/etc/passwd (base des utilisateurs)
/etc/resolv.conf (configuration DNS, généralement montée depuis l’hôte)
-
4. Base de données des paquets
/var/lib/dpkg/status (paquets installés)
/var/lib/apt/lists/ (cache des paquets disponibles)
- Le noyau, le bootloader et les pilotes ne sont pas inclus
Le noyau ne change pas, tout le reste oui
- Le noyau Linux fournit : ordonnancement des processus, gestion mémoire, opérations sur le système de fichiers, pile réseau, pilotes de périphériques, appels système
- Quand un processus dans un conteneur appelle
open(), read() ou fork(), l’appel est transmis directement au noyau de l’hôte
- Le noyau ne sait pas — et ne se soucie pas — si ce processus appartient à un « conteneur Ubuntu » ou à un « conteneur Alpine »
-
Stabilité de l’interface des appels système
- L’ABI des syscalls Linux est très stable
- Pourquoi un binaire compilé avec glibc 2.31 (Ubuntu 20.04) fonctionne aussi sur un noyau Ubuntu 24.04 :
- le noyau maintient la rétrocompatibilité
- les numéros d’appels système ne changent pas
- de nouvelles fonctions sont ajoutées, mais les anciennes sont rarement supprimées
- C’est aussi la raison pour laquelle on peut exécuter un conteneur Ubuntu 18.04 sur un hôte avec le noyau 6.5
Démonstration : même noyau, espace utilisateur différent
- En interrogeant le même noyau depuis plusieurs images de base, on constate que toutes les images partagent le noyau de l’hôte
ubuntu:22.04, debian:12, alpine:3.19, fedora:39, archlinux:latest affichent toutes la même version de noyau (6.5.0-44-generic)
- Ce qui change selon le conteneur, ce sont le binaire
uname, la libc et, plus largement, la composition du userland
Pourquoi les conteneurs sont si efficaces
-
1. Pas de duplication du noyau
- Chaque VM charge un noyau complet en mémoire (environ 100 à 500 Mo)
- 10 VM consomment la mémoire de 10 noyaux, alors que 10 conteneurs n’utilisent qu’un seul noyau
-
2. Démarrage immédiat
- Séquence de boot d’une VM : BIOS → bootloader → noyau → système d’init → services
- Un conteneur existe en quelques millisecondes grâce à de simples appels
fork() et exec()
- Boot typique d’une VM : 30 à 60 secondes / démarrage d’un conteneur : environ 0,347 s
-
3. Couches d’image partagées
- Si l’on exécute 100 conteneurs à partir de
ubuntu:22.04, les couches de l’image de base n’existent qu’une seule fois sur le disque
- Chaque conteneur ne reçoit qu’une fine couche copy-on-write pour ses modifications
-
4. Partage mémoire via le noyau
- Le page cache du noyau est partagé
- Si 50 conteneurs lisent le même fichier, le noyau ne le met en cache qu’une seule fois
- Avec les mêmes bibliothèques partagées, il est aussi possible de partager des pages mémoire via copy-on-write
Calcul des limites d’exécution des conteneurs
-
Analyse mémoire (VM avec 16 Go de RAM)
- RAM totale : 16 384 Mo
- Overhead de l’OS hôte : -1 024 Mo
- Démon Docker : -256 Mo
- Overhead du runtime des conteneurs : -512 Mo
- Mémoire disponible pour les conteneurs : 14 592 Mo
-
Utilisation mémoire par type de conteneur
- Minimal (
sleep) : environ 1 Mo
- Alpine + petite application : environ 25 Mo
- Ubuntu + application Python : environ 120 Mo
- Ubuntu + application Java : environ 500 Mo
- Service Node.js : environ 200 Mo
-
Maximum théorique
- Conteneur minimal (1 Mo) : 14 592
- Alpine + petite application (25 Mo) : 583
- Ubuntu + Python (120 Mo) : 121
- Microservice Java (500 Mo) : 29
-
Limites réelles
- Autres facteurs à considérer au-delà de la mémoire :
- Ordonnancement CPU : trop de conteneurs en concurrence peut provoquer des pics de latence
- Descripteurs de fichiers :
ulimit par défaut à 1024
- Ports réseau : seulement 65 535 ports disponibles pour le mapping
- PIDs : limite de
/proc/sys/kernel/pid_max (par défaut : 32 768)
- I/O disque : overhead d’OverlayFS et nécessité de parcourir de nombreuses couches
- Sur une VM de 16 Go exécutant de vraies charges de travail, les limites pratiques sont :
- Conteneurs légers (API, workers) : 50 à 100
- Conteneurs intermédiaires (DB, cache) : 10 à 30
- Grands conteneurs (modèles ML, applications JVM) : 5 à 10
Compatibilité entre distributions Linux
-
La promesse de l’ABI du noyau
- Linux conserve une interface de syscalls stable
- Des binaires compilés pour d’anciens noyaux fonctionnent sur des noyaux plus récents
- Un binaire Ubuntu 18.04 s’exécute normalement sur un noyau 6.5
-
Quand la compatibilité casse
- Exigences de fonctionnalités du noyau : si le conteneur a besoin d’une fonction absente du noyau (par ex. io_uring exige le noyau 5.1+)
- Dépendances à des modules noyau : Wireguard nécessite le module noyau wireguard, les conteneurs NVIDIA ont besoin du pilote noyau nvidia
- Restrictions seccomp/capabilities : si l’hôte bloque un syscall requis par le conteneur (par ex. pour utiliser
ptrace, il faut --cap-add SYS_PTRACE)
Guide de choix des images de base
| Image de base |
Taille |
Gestionnaire de paquets |
Usage |
scratch |
0 Mo |
Aucun |
Binaires Go/Rust compilés statiquement |
alpine |
7 Mo |
apk |
Conteneurs minimaux, musl libc |
distroless |
20 Mo |
Aucun |
Axé sécurité, sans shell ni gestionnaire de paquets |
debian-slim |
80 Mo |
apt |
Équilibre entre taille et compatibilité |
ubuntu |
78 Mo |
apt |
Confort de développement |
fedora |
180 Mo |
dnf |
Paquets récents, SELinux |
-
Quand utiliser chaque image
- scratch : pour les binaires compilés statiquement, sans aucun OS, uniquement le binaire
- alpine : image minimale avec accès shell, utilise musl libc au lieu de glibc, ce qui peut poser certains problèmes de compatibilité
- distroless : image de production axée sécurité, sans shell ni gestionnaire de paquets ; plus difficile à déboguer mais plus sûre
La frontière entre espace utilisateur et noyau
-
Ce qui vient de l’image de base (espace utilisateur)
- shell (
/bin/bash, /bin/sh)
- bibliothèque C (glibc, musl)
- gestionnaire de paquets (apt, apk, yum)
- utilitaires de base (ls, cat, grep)
- configuration du système d’init (mais généralement pas systemd lui-même)
- utilisateurs et groupes par défaut (
/etc/passwd)
- chemins de bibliothèques et configuration
-
Ce qui vient de l’hôte (noyau)
- ordonnancement des processus et gestion mémoire
- pile réseau (TCP/IP, routage)
- opérations sur le système de fichiers (lecture, écriture, montage)
- fonctions de sécurité (namespaces, cgroups, seccomp)
- pilotes de périphériques (GPU, réseau, stockage)
- gestion du temps et de l’horloge
- chiffrement et génération aléatoire
-
L’illusion créée par les namespaces
- Le noyau fournit des namespaces qui donnent au conteneur l’impression d’être isolé
- Le processus vu comme PID 1 à l’intérieur du conteneur existe sur l’hôte avec un PID plus élevé (par ex. 45678)
- Le noyau maintient ce mapping : PID 1 du conteneur → PID 45678 sur l’hôte
- C’est ainsi que l’isolation fonctionne sans virtualisation
Ce que cela implique en production
-
1. Une vulnérabilité du noyau affecte tous les conteneurs
- Si le noyau hôte présente une faille, tous les conteneurs y sont exposés
- Maintenir l’hôte à jour est indispensable
-
2. Le noyau de l’hôte limite les capacités du conteneur
- Pour utiliser io_uring, il faut un noyau hôte 5.1+
- Les fonctions eBPF exigent un noyau 4.15+ avec certaines options activées
-
3. L’importance de glibc vs musl
- Alpine utilise musl libc
- Certains binaires compilés pour glibc peuvent ne pas fonctionner
- Exemple : sur Alpine, l’exécution d’un binaire glibc peut échouer avec une erreur indiquant l’absence de
/lib/x86_64-linux-gnu/libc.so.6
-
4. Le « système d’exploitation » du conteneur est une notion purement organisationnelle
- Du point de vue du noyau, il n’y a aucune différence entre un « conteneur Ubuntu » et un « conteneur Debian »
- Ce sont simplement des processus qui émettent des syscalls
Idées reçues courantes
- ❌ « Les conteneurs sont des VM légères » : un conteneur est un processus avec une isolation avancée, alors qu’une VM virtualise le matériel et exécute un noyau distinct
- ❌ « Chaque conteneur possède son propre noyau » : tous les conteneurs partagent le noyau de l’hôte ; l’« OS » du conteneur n’est qu’un ensemble de fichiers d’espace utilisateur
- ❌ « Exécuter un conteneur Ubuntu = exécuter Ubuntu » : on exécute le noyau de l’hôte avec des outils Ubuntu ; si l’hôte est Debian, c’est en réalité un noyau Debian qui tourne
- ❌ « L’image de base contient un système d’exploitation complet » : l’image de base ne contient qu’un espace utilisateur minimal, sans noyau, bootloader ni pilotes
- ❌ « Plus de conteneurs = plus de mémoire consommée » : avec les couches partagées et le page cache du noyau, les conteneurs partagent souvent efficacement la mémoire
Résumé essentiel
- Une image de base Docker est un instantané du système de fichiers des composants d’espace utilisateur d’une distribution Linux
- les binaires, bibliothèques et configurations qui donnent à Ubuntu son apparence d’Ubuntu
- Le véritable système d’exploitation, le noyau, est partagé avec l’hôte
- Cette architecture permet :
- un démarrage en quelques millisecondes (pas de boot du noyau)
- un overhead mémoire minimal (un seul noyau, pages partagées)
- une forte densité (des centaines de conteneurs par hôte)
- des performances proches du natif (syscalls directs vers le noyau)
- Le compromis est une isolation plus faible que celle des VM : comme les conteneurs partagent le noyau, un exploit noyau affecte tous les conteneurs
- Pour la plupart des workloads, ce compromis en vaut la peine
Aucun commentaire pour le moment.