Un deuxième article « Deep Dive » sur vBlog en cet été caniculaire ! Je vous propose cette fois de revoir les bases du fonctionnement réel de Docker/containerd et consorts, en partant d’une construction directe depuis une machine Linux, sans rien installer.

Pour avoir un peu de contexte, je vais travailler sur une machine virtuelle simple sous Linux Debian 13 Trixie. Bon, ça n’a pas d’importance en fait, car nous n’allons utiliser que des commandes présentes dans toute distribution avec un noyau Linux à peu près à jour.

L’objectif n’est ni plus ni moins, avec ce tutoriel, de comprendre vraiment sur quoi s’appuient les gestionnaires de conteneurs que nous utilisons habituellement. Vous avez sûrement entendu parler ça et là de cgroups, voire même d’overlays. Nous allons précisément créer ces isolations de processus ainsi que de systèmes de fichiers un peu spécifique que sont les overlays.

Les idées reçues on la vie dure !

Beaucoup imaginent Docker comme un empilement de couches (comme un oignon) qui ralentit tout, un peu comme une machine virtuelle. C’est faux ! En réalité, un conteneur est un processus Linux standard (comme nginx ou bash) qui tourne sur le même noyau que la machine hôte. La différence, c’est que ce processus est isolé du reste du système et voit un système de fichiers différent (grâce à l’overlay filesystem, on va le voir) ; il a son propre réseau, ses PID (ses identifiants de processus), sa mémoire, etc. (grâce aux namespaces au sein du noyau). Il peut même avoir des limites en termes de ressources (CPU, RAM) (grâce aux cgroups).

Le système de fichiers : l’overlay filesystem

Pour illustrer la chose, je créé un dossier de base qui s’appelle conteneur ainsi que 4 sous-dossiers base, rw, work et montage. Ensuite je monte le filesystem overlay via une commande mount avec quelques options :

  • Le répertoire base va contenir l’image initiale de ce conteneur, en lecture seule. Je vais prendre l’image de base de alpinelinux en mode conteneur.
  • Le répertoire rw contient toutes les modifications qui seront apportées sur ce filesystem.
  • Le répertoire montage est juste un « placeholder » sur lequel je vais plugger via mount le filesystem qu’on est en train de construire.
  • Enfin le répertoire temp est un répertoire qui contient les fichiers temporaires éventuellement nécessaires pendant le fonctionnement de ce filesystem.

On y va ? C’est parti :

cidou@guenievre:~ mkdir conteneur
cidou@guenievre:~ cd conteneur/
cidou@guenievre:~/conteneur mkdir -p {base,rw,montage,temp}
cidou@guenievre:~ tree conteneur
conteneur
├── base
├── montage
├── rw
└── work
cidou@guenievre:~/conteneur sudo mount -t overlay overlay -o lowerdir=base,upperdir=rw,workdir=work montage
cidou@guenievre:~/conteneur df /home/cidou/conteneur/montage
cidou@guenievre:~/conteneur df montage
Filesystem      Size  Used Avail Use% Mounted on
overlay         295G   15G  265G   6% /home/cidou/conteneur/montage

Ensuite, on va télécharger dans le répertoire base un distribution que vous connaissez sans doute bien si vous faite du conteneur … alpinelinux (je récupère le miniroot de la distribution, car on a pas besoin de l’isolation complète évidemment) :

cidou@guenievre:~/conteneur wget https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.12-x86_64.tar.gz
--2026-08-13 19:01:59--  https://dl-cdn.alpinelinux.org/alpine/v3.18/releases/x86_64/alpine-minirootfs-3.18.12-x86_64.tar.gz
Resolving dl-cdn.alpinelinux.org (dl-cdn.alpinelinux.org)... 140.248.138.132, 2a04:4e42:92::644
Connecting to dl-cdn.alpinelinux.org (dl-cdn.alpinelinux.org)|140.248.138.132|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 3294132 (3.1M) [application/octet-stream]
Saving to:alpine-minirootfs-3.18.12-x86_64.tar.gz

alpine-minirootfs-3.18.12-x86_64.tar.gz   100%[====================================================================================>]   3.14M  --.-KB/s    in 0.09s

2026-08-13 19:01:59 (33.1 MB/s) -alpine-minirootfs-3.18.12-x86_64.tar.gzsaved [3294132/3294132]

cidou@guenievre:~/conteneur ls
alpine-minirootfs-3.18.12-x86_64.tar.gz  base  montage  rw  temp
cidou@guenievre:~/conteneur tar xvfz alpine-minirootfs-3.18.12-x86_64.tar.gz ^C
cidou@guenievre:~/conteneur cd base
cidou@guenievre:~/conteneur/base tar xvfz ../alpine-minirootfs-3.18.12-x86_64.tar.gz
./
./tmp/
./media/
./media/floppy/
./media/usb/
./media/cdrom/
./bin/
./bin/chown
./bin/gunzip
./bin/echo
./bin/ps
(...)
./sys/
./srv/
./root/
cidou@guenievre:~/conteneur/base ls
bin  dev  etc  home  lib  media  mnt  opt  proc  root  run  sbin  srv  sys  tmp  usr  var

Notre premier conteneur, à la main !

Notre premier filesystem overlay est prêt, il ne nous reste plus qu’à démarrer un processus qui sera « chrooté » et isolé des autres process de la machine. Un processus peut-être isolé de plusieurs manières :

NamespaceRôleExemple dans la vidéo
IPCIsoler la communication inter-processus.Un pipe dans le conteneur reste privé.
MountIsoler les points de montage (pour le chroot).Le / du conteneur = montage/.
NetworkCréer un stack réseau indépendant.Le conteneur a son propre lo, son eth0 spécifique.
PIDIsoler des autres processus (le conteneur voit seulement ses propres PID).ps aux ne montre que sh et top.
UserIsoler les utilisateurs (optionnel).Non utilisé pour ce tuto
UTSDonner un nom d’hôte personnalisé.

La commande magique ici est unshare. On active donc tous les isolements et on va construire notre conteneur en bonne et due forme ensuite. ps ou top ne sont pas dispo ? on va monter le /proc (leur source d’info).

cidou@guenievre:~/conteneur sudo unshare --pid --uts --ipc --net --mount --fork -- chroot montage /bin/sh
root@guenievre:/ top
top: no process info in /proc
root@guenievre:/ ls
bin    dev    etc    home   lib    media  mnt    opt    proc   root   run    sbin   srv    sys    tmp    usr    var    work
root@guenievre:/ mount -t proc proc /proc
root@guenievre:/ ps -a
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    7 root      0:00 ps -a

Forcément j’ai envie de dire, pas de réseau (on l’a isolé). On va monter une interface virtuelle dans le process /bin/sh, pour ce faire il faut en premier récupérer le PID du shell en question … comme ma machine virtuelle n’est pas très grosse, un petit pidof /bin/sh devrait suffire sur le celle-ci (attention pas dans le conteneur, forcément, l’énumération des processus démarre toujours à 1 comme toute machine ou environnement Unix en général).

Coté host :

cidou@guenievre:~ pidof /bin/sh
1134566
cidou@guenievre:~ sudo ip link add veth0 type veth peer name veth-monprocess
cidou@guenievre:~ sudo ip link set veth-monprocess netns 1134566
cidou@guenievre:~ sudo ip addr add 10.0.0.1/24 dev veth0
cidou@guenievre:~ sudo ip link set veth0 up
cidou@guenievre:~ ping 10.0.0.1
PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.
64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.105 ms
64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.090 ms
^C
--- 10.0.0.1 ping statistics ---
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 0.090/0.097/0.105/0.007 ms

Coté conteneur (notre process /bin/sh 1134566) :

root@guenievre:/ ip link
1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
853: veth-monprocess@if854: <BROADCAST,MULTICAST,M-DOWN> mtu 1500 qdisc noop state DOWN qlen 1000
    link/ether fe:73:78:94:48:de brd ff:ff:ff:ff:ff:ff
root@guenievre:/ ip addr add 10.0.0.2/24 dev veth-monprocess
root@guenievre:/ ip link set veth-monprocess up
root@guenievre:/ ip route add default via 10.0.0.1

Du réseau ? Ne nous arrêtons pas en si bon chemin !

Pour ouvrir un peu le truc, histoire de tester l’installation d’un démon style nginx, je vais activer le nat du conteneur qu’on vient de créer de toute pièce. Attention, pensez toujours à la route de retour et le « RELATED » pour l’ICMP si vous faites des ping … (je l’ai moi meme oubliée et j’ai été obligé de débugguer pendant 5/10 minutes avec du tcpdump et consors …) :

cidou@guenievre:~ sudo iptables -t nat -A POSTROUTING -o enp54 -j MASQUERADE
cidou@guenievre:~ sudo iptables -A FORWARD -i veth0 -o enp54 -j ACCEPT
cidou@guenievre:~ sudo iptables -A FORWARD -i enp54 -o veth0 -m state --state RELATED,ESTABLISHED -j ACCEPT

C’est fini ! le process a accès au net et on peut en profiter pur mettre à jour sa micro distrib Alpine et installer nginx :

root@guenievre:/ apk update
fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.18/community/x86_64/APKINDEX.tar.gz
v3.18.12-192-gb0b6b9a320c [https://dl-cdn.alpinelinux.org/alpine/v3.18/main]
v3.18.12-192-gb0b6b9a320c [https://dl-cdn.alpinelinux.org/alpine/v3.18/community]
OK: 20070 distinct packages available
root@sandbox:/ apk add nginx
OK: 9 MiB in 17 packages
root@sandbox:/ nginx
root@sandbox:/ ps -aef
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
   12 root      0:00 nginx: master process nginx
   13 nginx     0:00 nginx: worker process
   14 nginx     0:00 nginx: worker process
   17 root      0:00 ps -aef
root@sandbox:/ apk add iproute2-ss
(1/3) Installing libcap2 (2.69-r0)
(2/3) Installing libmnl (1.0.5-r1)
(3/3) Installing iproute2-ss (6.3.0-r0)
Executing busybox-1.36.1-r7.trigger
OK: 9 MiB in 20 packages
root@sandbox:/ ss -ltpn
State         Recv-Q        Send-Q                 Local Address:Port                 Peer Address:Port        Process
LISTEN        0             511                          0.0.0.0:80                        0.0.0.0:*           (...)
LISTEN        0             511                             [::]:80                           [::]:*           (...)

Si vous faites un curl http://10.0.0.2, vous allez normalement récupérer la page d’accueil de nginx !

C’était rafraîchissant et plein de tech dites-moi ! Pas si mal pour un jour si chaud 🙂

Vous avez tout compris ?

Les conteneurs Linux ne sont pas de la magie, mais une combinaison intelligente de mécanismes natifs du kernel : les namespaces permettent de l’isolation (PID, réseau, montage), les cgroups pour les limites de ressources, et l’overlay filesystem pour un système de fichiers spécifique qui trie les fichier en lecture seul des fichiers dynamique en lecture/écriture. J’espère que ce petit tuto vous aura aidé en recréant un conteneur à la main, sans Docker, avec just unsharechroot et les virtual nic. La complexité iptables pour le NAT illustre en partie la manière qu’a Docker de filtrer et gérer les multibles réseaux nommés.

En fait, vous ne le saviez peut-être pas mais le kernel Linux sait déjà faire ça depuis au moins 10 ans : les cgroups dans de la bonne vieille 2.6 (les années 2000 !) en v1 je crois tandis que l’overlay filesystem est plus récent et date des années 2015 (à vérifier là encore sur le net, ça se trouve facilement).

Je pense que c’est bien de revenir aux fondamentaux quelques fois, pour savoir d’où viennent toutes les technologies qu’on utilise quotidiennement aujourd’hui.

Références (pour aller plus loin) :
– Article librement inspiré de l’excellente vidéo de « That DevOps Guy » : https://www.youtube.com/watch?v=_rwjwdKNPnQ
– L’overlay Filesystem : https://docs.kernel.org/filesystems/overlayfs.html
– Les cgroups : https://docs.kernel.org/admin-guide/cgroup-v1/cgroups.html
– La commande unshare : https://man7.org/linux/man-pages/man1/unshare.1.html
– La commande nsenter (gestion des namespaces) : https://www.man7.org/linux/man-pages/man1/nsenter.1.html
– Enfin, l’article que m’a fourni un pote, Timo : https://labs.iximiuz.com/roadmaps/docker

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *