Docker est souvent mal compris. Est-ce de la virtualisation ? Non. Est-ce un chroot glorifié ? Presque, mais avec des super-pouvoirs comme les namespaces & cgroups.
Mais oublions l'informatique pure un instant. Pour comprendre Docker, il faut savoir cuisiner, et plus précisément, cuisiner des pancakes.
Le monde d'avant
Dans notre boutique de pancakes, il n'y a qu'une seule cuisine commune pour tout le monde, (l'OS Host).
- Le client A (Symfony) veut des pancakes aux myrtilles (besoin de PHP 8.2).
- Le client B (une vieille version de WordPress) veut des crêpes au sarrasin (besoin de PHP 7.4).
- Le client C (une vieille version de Node.js) veut un kebab (besoin de Node 18).
La guerre est déclarée ! Pour cuire la crêpe B, vous devez désinstaller la poêle "PHP 8.2" et visser la poêle "PHP 7.4". Il reste des traces de myrtilles. Le kebab a un goût de sarrasin. Une fuite de mémoire (un cuisinier maladroit) ruine tous les plats.
La solution Docker ? Et si chaque plat était cuisiné dans sa propre mini-cuisine hermétique, blanche, stérile et équipée uniquement du nécessaire ?
L'image : la recette
L'image Docker, c'est la recette plastifiée : elle est immuable. Sur mon site, j'utilise une recette sophistiquée en plusieurs étapes (Multi-Stage Build). Regardons le fichier réel devops/frankenphp/Dockerfile :
# 1. La Base Commune (La Farine)
FROM dunglas/frankenphp:1-php8.5 AS frankenphp_upstream
# 2. Les Ustensiles (Extensions PHP)
FROM frankenphp_upstream AS frankenphp_base
RUN apt-get update && apt-get install -y --no-install-recommends \
libmagickwand-dev libwebp-dev ...
Ici, je prépare la pâte mère. Chaque instruction RUN ajoute une couche (layer) invisible.
Si je change la farine (l'image de base), tout le monde est affecté. Mais si je change juste le décor (le code source), la pâte n'est pas refaite.
L'optimisation COPY --link
Une particularité de mon Dockerfile est l'utilisation massive de COPY --link :
COPY --link devops/frankenphp/conf.d/10-app.ini $PHP_INI_DIR/app.conf.d/
Sans --link, Docker devrait revérifier tous les ingrédients précédents. Avec --link, il ajoute cet ingrédient indépendamment, comme une cerise posée à la toute fin car c'est instantané.
Le multi-stage : dev vs prod
Notre Dockerfile se sépare ensuite en deux univers parallèles :
# Univers DEV : Cuisine Ouverte (On goûte, on modifie)
FROM frankenphp_base AS frankenphp_dev
RUN install-php-extensions xdebug
CMD [ "frankenphp", "run", "--config", "/etc/caddy/Caddyfile", "--watch" ]
# Univers PROD : Cuisine Fermée (Scellé pour le client)
FROM frankenphp_base AS frankenphp_prod
ENV APP_ENV=prod
COPY --link . ./
RUN composer install --no-dev --classmap-authoritative
- En Dev (
--watch) : Le pancake reste "cru" au centre. On peut rajouter des pépites de chocolat (modifier le code) pendant la cuisson. - En Prod : Le pancake est cuit, emballé sous vide (
--classmap-authoritative). Il est immuable.
Le Containeur
Le Containeur, c'est le moment où on verse la pâte dans la poêle. C'est l'instanciation de l'Image. Techniquement, Docker prend les layers en lecture seule (la recette) et ajoute une fine couche en écriture (Copy-on-Write) par-dessus.
Si le pancake brûle, on ne gratte pas le noir. On le jette (docker rm) et on recommence une cuisson parfaite (docker run).
L'orchestration : docker compose
Un pancake tout seul, c'est triste. Un petit-déjeuner complet, c'est :
- Le pancake (PHP, FrankenPHP)
- Le sirop d'érable (PostgreSQL)
- Le jus d'orange (Redis)
- Le toasteur (Meilisearch)
Docker compose est votre serveur. Il dresse toute la table d'un coup.
Analysons le compose.yaml :
services:
php:
depends_on:
database:
condition: service_healthy # "Attends que le sirop soit chaud !"
Le healthcheck : le chef goûte
database:
healthcheck:
test: [ "CMD", "pg_isready", "-d", "${POSTGRES_DB:-app_dev}" ]
interval: 10s
Docker ne se contente pas d'allumer le gaz (lancer le processus). Il goûte (pg_isready) toutes les 10 secondes. Tant que le sirop n'est pas à température, il n'autorise pas le service des pancakes. Fini les crashs au démarrage !
Les volumes : le tupperware magique
volumes:
- database_data:/var/lib/postgresql/data:rw
Rappelez-vous : si je jette mon pancake brûlé (conteneur), je perds tout ce qui est dedans. Pour la base de données, c'est inacceptable.
J'utilise donc un volume (database_data). C'est un tupperware indestructible posé sur une étagère hors de la cuisine isolée. Même si la cuisine explose, le tupperware reste intact.
Le mot de la fin
Docker n'est pas magique. C'est une exploitation brillante du Kernel Linux (namespaces = isolation, cgroups = quotas).
Mais gardez cette image : vos serveurs ne sont plus des animaux de compagnie qu'on soigne quand ils sont malades (Servers as Pets).
- Un "Pet" a un nom (Gandalf, Zeus). S'il est malade, on passe la nuit à le soigner. Sa mort est une tragédie.
Vos conteneurs sont du bétail (Servers as Cattle).
- Ils n'ont pas de nom, juste un ID.
- S'ils sont malades, on ne les soigne pas. On les abat (pardon, on les stoppe) et on en déploie un nouveau immédiatement.
C'est la philosophie du pancake. S'il est râté, on le jette. Pourquoi ?
Parce qu'on a la recette parfaite (Dockerfile) pour en refaire un million d'autres identiques à la seconde près.
Bon appétit !