Docker
Les applications modernes s'appuient sur une combinaison de services (base de données, cache, broker de messages, runtime applicatif), chacun avec ses dépendances de version spécifiques. Sans conteneurs, chaque développeur configure son environnement local manuellement, les versions divergent entre machines, et le déploiement implique de reconfigurer chaque serveur à la main. Docker résout ces problèmes en encapsulant l'application et ses dépendances dans une unité portable et reproductible.
Le problème sans conteneurs
Développement
Dans une équipe sans conteneurs, chaque développeur installe directement les services sur son OS : PostgreSQL, Redis, Node.js, Python. Le processus varie selon l'OS (apt, brew, installeur Windows), les versions diffèrent d'une machine à l'autre, et la moindre mise à jour peut casser la configuration locale d'un autre développeur. Le problème classique : "ça fonctionne sur ma machine" mais pas en CI, ni en production.
Déploiement
Le déploiement traditionnel repose sur un guide textuel transmis à l'équipe ops : installer les dépendances, configurer les variables d'environnement, démarrer les services dans le bon ordre. Chaque étape est manuelle, chaque serveur est configuré à la main, et toute divergence entre la documentation et la réalité se traduit par une panne en production.
Dev → artifact (jar, wheel, binaire)
+ instructions textuelles
→ Ops configure le serveur
→ déploiement (erreurs fréquentes)
La solution Docker
Docker encapsule l'application et son environnement d'exécution dans une image. L'image contient le code, les dépendances système, les variables de configuration par défaut : tout ce qu'il faut pour faire tourner le service. L'image est construite une fois, testée en CI, et déployée identiquement sur chaque environnement.
Dev → Dockerfile → image Docker
→ CI : tests
→ Registry : stockage
→ Prod : docker run
La seule dépendance sur le serveur de production est le daemon Docker, une installation unique, indépendante de l'application déployée.
Image et conteneur
Une image est un artefact statique : un snapshot du filesystem de l'application à un instant donné, versionné et stocké dans un registry.
Un conteneur est une instance en cours d'exécution d'une image. On peut démarrer plusieurs conteneurs à partir de la même image, chacun dans son propre namespace réseau et filesystem.
# Télécharger une image
docker pull postgres:16-alpine
# Démarrer un conteneur depuis cette image
docker run -d \
--name postgres \
-e POSTGRES_PASSWORD=secret \
-p 5432:5432 \
postgres:16-alpine
# Lister les conteneurs en cours
docker ps
Structure d'une image en couches
Une image Docker est constituée de couches superposées. Les instructions qui modifient le système de fichiers (RUN, COPY, ADD) produisent chacune une couche ; les autres (CMD, ENV, EXPOSE, WORKDIR...) n'ajoutent que des métadonnées à la configuration de l'image. Les couches sont immuables et partagées entre images : si deux images utilisent la même base Alpine, la couche Alpine n'est stockée qu'une fois sur le disque.
# Couches de l'image de base
FROM python:3.12-alpine
# Nouvelle couche : dépendances
RUN pip install fastapi uvicorn
# Nouvelle couche : code source
COPY . /app
# Métadonnée uniquement : commande de démarrage
CMD ["uvicorn", "main:app", "--app-dir", "/app"]
Dans un Dockerfile, un commentaire doit occuper une ligne entière : un # placé après une instruction est transmis comme argument de cette instruction.
Quand l'image est reconstruite, seules les couches modifiées et celles qui les suivent sont recalculées. Si le code source change mais pas les dépendances, la couche RUN pip install est réutilisée depuis le cache. L'ordre des instructions détermine donc l'efficacité du cache : les éléments qui changent rarement (dépendances) se placent avant ceux qui changent souvent (code source). L'article Docker : bonnes pratiques détaille ces optimisations.
Docker Compose
Pour une application multi-services, un fichier Compose (compose.yaml, ou docker-compose.yml pour compatibilité) décrit l'ensemble de la stack et les relations entre services :
services:
api:
build: ./api
ports:
- "8000:8000"
environment:
DATABASE_URL: postgresql://postgres:secret@db:5432/myapp
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
environment:
POSTGRES_PASSWORD: secret
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 5
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
docker compose up -d # démarrer la stack en arrière-plan
docker compose logs -f # suivre les logs
docker compose down # arrêter et supprimer les conteneurs
depends_on sous sa forme courte (une simple liste) garantit seulement que le conteneur db est démarré avant api, pas que PostgreSQL accepte déjà les connexions. La condition service_healthy fait attendre Compose jusqu'à ce que le healthcheck de db réussisse. Le nom du service (db) sert de hostname dans le réseau Docker Compose : api peut joindre la base de données via db:5432 sans connaître l'IP du conteneur. L'article Docker Compose approfondit ces mécanismes.