Passer au contenu principal
← Blog

Docker : c'est quoi un conteneur au niveau OS ?

Namespaces, cgroups, overlay filesystem -- un conteneur n'est pas une VM légère, c'est bien plus simple.

Radnoumane Mossabely · · 8 min read · 0 vues
DockerLinuxConteneursNamespacesKernel

TL;DR

  • Un conteneur n'est pas une VM légère. C'est un processus Linux normal avec des restrictions d'isolation et de ressources.
  • Trois mécanismes du kernel font tout le travail : namespaces (isolation), cgroups (limites), overlay filesystem (couches).
  • docker run fait essentiellement un fork() + unshare() + chroot() + execve(). Pas de magie.
  • Docker Desktop sur macOS fait tourner une VM Linux cachee -- parce que les conteneurs sont une technologie purement Linux.
  • Comprendre ces mécanismes change ta façon de debugger, de securiser et d'optimiser tes conteneurs.

Le mythe de la "VM légère"

Si tu as déjà lu une intro a Docker, tu as probablement vu ce schema : une VM a gauche avec un hyperviseur et des OS complets, un conteneur a droite "beaucoup plus leger". Et l'explication : "un conteneur, c'est comme une VM légère."

C'est faux. Un conteneur n'est pas une version light d'une VM. C'est un concept fondamentalement différent.

Une VM emule du hardware. Elle fait tourner un OS complet -- kernel, drivers, systemd, tout. Un conteneur ne fait rien de tout ca. C'est un processus Linux normal qui tourne sur le kernel de l'hôte, avec trois mécanismes d'isolation qui lui donnent l'illusion d'etre seul sur la machine.

Ces trois mécanismes, ce sont les namespaces, les cgroups, et l'overlay filesystem. Tout le reste -- Docker, containerd, Kubernetes -- c'est de la plomberie autour de ces trois briques.

Les namespaces : l'isolation des processus

Les namespaces sont le mécanisme d'isolation du kernel Linux. Chaque namespace donne au processus une vue restreinte d'une ressource système.

Il y en a sept :

NamespaceIsoleEffet concret
pidArbre des processusLe conteneur voit PID 1 comme son processus racine
netInterfaces réseauLe conteneur a sa propre stack TCP/IP
mntPoints de montageLe conteneur a son propre filesystem
utsHostnameLe conteneur a son propre nom de machine
ipcCommunication inter-processusMemoire partagee isolee
userUID/GIDRoot dans le conteneur != root sur l'hôte
cgroupVue cgroupLe conteneur ne voit que ses propres limites

Le point cle : chaque namespace est indépendant. Tu peux créer un processus avec un nouveau namespace pid mais qui partage le namespace net de l'hôte. C'est exactement ce que fait docker run --network host.

Pour vérifier, lance un conteneur et regarde ses namespaces :

hljs bash
# Lance un conteneur
docker run -d --name test nginx

# Trouve le PID du processus sur l'hote
docker inspect --format '{{.State.Pid}}' test

# Liste ses namespaces
ls -la /proc/<PID>/ns/

Tu verras des liens symboliques vers les namespaces du conteneur. Chaque lien pointe vers une instance de namespace différente de celle de l'hôte.

Les cgroups : les limites de ressources

Les namespaces isolent la visibilité. Les cgroups (control groups) limitent la consommation. Ce sont deux problèmes distincts : un processus peut etre isole sans etre limite, et inversement.

Les cgroups controlent :

  • CPU : temps CPU alloue, nombre de coeurs utilisables
  • Memoire : limite dure, soft limit, swap
  • I/O : bande passante disque, nombre d'opérations
  • PIDs : nombre maximum de processus

Quand tu fais docker run -m 512m --cpus 2, Docker crée un cgroup avec ces limites et y place le processus du conteneur. Le kernel applique ces limites de manière transparente.

hljs bash
# Voir les limites memoire d'un conteneur
cat /sys/fs/cgroup/docker/<container-id>/memory.max

# Voir l'utilisation CPU
cat /sys/fs/cgroup/docker/<container-id>/cpu.stat

Point important : quand un processus dépasse sa limite mémoire cgroup, le kernel le tue (OOM kill). C'est pourquoi tes conteneurs meurent avec un exit code 137 -- ce n'est pas Docker qui les tue, c'est le kernel via les cgroups.

L'overlay filesystem : les couches d'images

Le troisieme pilier, c'est le système de fichiers en couches. C'est ce qui rend les images Docker si efficaces.

Le principe est simple : au lieu de copier un filesystem complet pour chaque conteneur, on empile des couches en lecture seule avec une couche en écriture par-dessus.

Couche ecriture (conteneur)   ← tes modifications
────────────────────────────
Couche 3 : COPY app.js        ← ton code
Couche 2 : RUN npm install    ← tes dependances
Couche 1 : FROM node:20       ← l'image de base

Quand le conteneur lit un fichier, le système overlay cherche de haut en bas : d'abord la couche écriture, puis les couches en lecture seule en ordre inverse. Quand il écrit, le fichier est copie dans la couche écriture (copy-on-write).

C'est pourquoi :

  • Deux conteneurs bases sur la même image partagent les couches en lecture seule
  • Supprimer un fichier dans un Dockerfile ne réduit pas la taille de l'image (la couche précédente le contient toujours)
  • L'ordre des instructions dans un Dockerfile impacte l'efficacite du cache
hljs bash
# Voir les couches d'une image
docker inspect --format '{{.RootFS.Layers}}' nginx

# Voir le point de montage overlay d'un conteneur
docker inspect --format '{{.GraphDriver.Data}}' <container-id>

Comment docker run fonctionne vraiment

Quand tu tapes docker run nginx, voici ce qui se passe au niveau système :

En version simplifiee, c'est l'équivalent de :

hljs c
// Version simplifiee de ce que fait runc
pid = clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS);
if (pid == 0) {
    // Dans le nouveau processus isole
    setup_cgroups();
    setup_overlay_mount();
    pivot_root("/overlay/merged");
    execve("/usr/sbin/nginx", argv, envp);
}

Il n'y a pas de VM, pas d'hyperviseur, pas de kernel séparé. C'est un processus comme un autre, avec des restrictions.

Conteneur vs VM : la vraie différence

La consequence directe :

  • Un conteneur démarre en millisecondes (c'est un fork + exec). Une VM démarre en secondes (boot d'un OS).
  • Un conteneur consomme la mémoire de son processus. Une VM réserve la mémoire de son OS entier.
  • Un conteneur partage le kernel de l'hôte. Une VM a son propre kernel.

Le revers de la medaille : l'isolation d'un conteneur est plus faible. Un exploit kernel affecte tous les conteneurs sur l'hôte. Une VM offre une isolation hardware réelle.

Et Docker Desktop sur macOS ?

Si tu utilises Docker Desktop sur macOS ou Windows, il y a un twist. Les conteneurs sont une technologie purement Linux -- les namespaces et cgroups n'existent pas sur macOS.

Docker Desktop contourne le problème en faisant tourner une VM Linux légère en arriere-plan (via Apple Hypervisor Framework sur macOS). Tes conteneurs tournent dans cette VM, pas directement sur macOS.

C'est ironique : pour utiliser des conteneurs (qui ne sont pas des VM) sur macOS, tu as besoin d'une VM.

hljs bash
# Verifier la VM Docker Desktop
docker info | grep "Operating System"
# Affiche : Docker Desktop (avec un kernel Linux)

# Sur Linux natif
docker info | grep "Operating System"
# Affiche : Ubuntu 22.04 (ou ta distro)

Ca explique pourquoi Docker est plus lent sur macOS que sur Linux, et pourquoi les volumes montes ont des problèmes de performance -- les I/O traversent la couche de virtualisation.

Pourquoi comprendre tout ça ?

Ce n'est pas de la théorie gratuite. Comprendre les mécanismes sous-jacents change ta pratique :

Debugging : quand un conteneur est OOM killed (exit 137), tu sais que c'est un problème de cgroup mémoire, pas un bug Docker. Tu regardes docker stats et les limites, pas les logs Docker.

Securite : tu comprends que --privileged désactivé les restrictions de namespaces et cgroups, et que c'est essentiellement équivalent a du root sur l'hôte. Tu comprends pourquoi les user namespaces importent.

Performance : tu sais que l'overhead d'un conteneur est quasi nul (pas de couche de virtualisation), et que les problèmes de performance viennent des cgroups mal configures ou de l'overlay filesystem.

Architecture : tu comprends pourquoi "un processus par conteneur" est une best practice -- parce que le PID 1 dans le namespace pid reçoit les signaux, et si ton conteneur lance plusieurs processus, la gestion des signaux devient compliquee.

Le minimum a retenir

Un conteneur, c'est trois choses :

  1. Des namespaces pour l'isolation (le processus croit etre seul)
  2. Des cgroups pour les limites (le processus ne peut pas tout consommer)
  3. Un overlay filesystem pour les couches (les images sont efficaces)

Tout le reste -- Docker, Podman, containerd, CRI-O -- ce sont des outils qui simplifient la création et la gestion de ces trois mécanismes. Le kernel fait le vrai travail.

Ressources

Articles Similaires