71 points par GN⁺ 2026-01-21 | Aucun commentaire pour le moment. | Partager sur WhatsApp
  • 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.

Aucun commentaire pour le moment.