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
baseva 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
rwcontient toutes les modifications qui seront apportées sur ce filesystem. - Le répertoire
montageest juste un « placeholder » sur lequel je vais plugger via mount le filesystem qu’on est en train de construire. - Enfin le répertoire
tempest 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/montageEnsuite, 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.gz’ saved [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 :
| Namespace | Rôle | Exemple dans la vidéo |
|---|---|---|
| IPC | Isoler la communication inter-processus. | Un pipe dans le conteneur reste privé. |
| Mount | Isoler les points de montage (pour le chroot). | Le / du conteneur = montage/. |
| Network | Créer un stack réseau indépendant. | Le conteneur a son propre lo, son eth0 spécifique. |
| PID | Isoler des autres processus (le conteneur voit seulement ses propres PID). | ps aux ne montre que sh et top. |
| User | Isoler les utilisateurs (optionnel). | Non utilisé pour ce tuto |
| UTS | Donner 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 msCoté 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.1Du 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 ACCEPTC’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 unshare, chroot 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
