Docker permet d'empaqueter une application et toutes ses dépendances dans un conteneur portable. Dans ce tutoriel, nous allons créer une API Python avec FastAPI, l'encapsuler dans une image Docker et l'exécuter en quelques commandes — le même résultat garanti sur toutes les machines.
Définition : Plateforme de conteneurisation permettant d'empaqueter une application et toutes ses dépendances (runtime, librairies, code) dans une unité portable appelée conteneur.
But : Garantir que l'application fonctionne de manière identique peu importe où elle s'exécute (machine locale, serveur, cloud).
Pourquoi ici : Docker élimine le problème "ça marche sur ma machine" en isolant complètement l'environnement d'exécution.
On commence par une API simple avec deux routes. Créez le dossier du projet :
mkdir mon-api && cd mon-api
Créez le fichier main.py :
# Imports : charger FastAPI, gestion d'erreurs HTTP et modèles Pydantic
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
import datetime
# Créer l'instance FastAPI avec métadonnées pour la documentation Swagger
app = FastAPI(
title="Mon API",
description="API exemple conteneurisée avec Docker",
version="1.0.0"
)
# Définir le modèle de données Item avec validation Pydantic
class Item(BaseModel):
name: str
price: float
in_stock: bool = True
# Simuler une base de données en mémoire pour ce tutoriel
items_db: List[Item] = [
Item(name="Laptop", price=999.99),
Item(name="Souris", price=29.99),
]
# Route GET racine : répondre avec un message et timestamp
@app.get("/")
def root():
return {
"message": "API opérationnelle 🚀",
"timestamp": datetime.datetime.now().isoformat()
}
# Route GET /items : retourner la liste complète des items
@app.get("/items", response_model=List[Item])
def get_items():
"""Retourne tous les articles disponibles."""
return items_db
# Route GET /items/{item_id} : récupérer un item spécifique par ID
@app.get("/items/{item_id}", response_model=Item)
def get_item(item_id: int):
if item_id < 0 or item_id >= len(items_db):
raise HTTPException(status_code=404, detail="Article non trouvé")
return items_db[item_id]
# Route POST /items : ajouter un nouvel item à la base de données
@app.post("/items", response_model=Item, status_code=201)
def create_item(item: Item):
items_db.append(item)
return item
# Route GET /health : vérifier que l'API est en fonctionnement
@app.get("/health")
def health_check():
return {"status": "healthy"}
Créez le fichier requirements.txt :
# Framework web moderne pour construire les APIs REST
fastapi==0.111.0
# Serveur ASGI pour exécuter FastAPI (asynchrone, performant)
uvicorn[standard]==0.29.0
# Validation de données et sérialisation JSON
pydantic==2.7.0
# Créer un environnement virtuel isolé pour ce projet
python -m venv venv
# Activer l'environnement (Windows : venv\Scripts\activate)
source venv/bin/activate
# Installer les dépendances listées dans requirements.txt
pip install -r requirements.txt
# Lancer l'API avec rechargement auto lors des modifications
uvicorn main:app --reload --port 8000
Le Dockerfile est la recette qui décrit comment construire l'image de votre application.
Définition : Fichier texte contenant une suite d'instructions pour assembler une image Docker. Chaque instruction crée une nouvelle couche (layer) dans l'image.
But : Automatiser la création d'une image de manière reproductible et documentée.
Pourquoi ici : Au lieu de créer manuellement des conteneurs et les configurer, le Dockerfile permet à n'importe qui de générer l'image identique simplement en exécutant "docker build".
# ── Image de base officielle Python ──
# python:3.11-slim : variante légère (~150MB vs ~1GB pour l'image complète)
FROM python:3.11-slim
# ── Variables d'environnement pour Python ──
# PYTHONDONTWRITEBYTECODE=1 : ne pas générer les fichiers .pyc inutiles
# PYTHONUNBUFFERED=1 : envoyer les logs directement à stdout (temps réel)
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
# ── Définir le répertoire de travail du conteneur ──
WORKDIR /app
# ── Copier les dépendances EN PREMIER ──
# Docker utilise le cache : si requirements.txt ne change pas, cette couche est réutilisée
# Les modifications du code source ne forcent pas une réinstallation des dépendances
COPY requirements.txt .
# --no-cache-dir : ne pas stocker le cache pip (économise de l'espace)
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt
# ── Copier le code source de l'application ──
COPY . .
# ── Port exposé par l'application (documentation) ──
# Note: EXPOSE ne publie pas le port, il faut utiliser -p lors du docker run
EXPOSE 8000
# ── Commande de démarrage ──
# 0.0.0.0 rend l'API accessible depuis l'extérieur du conteneur
# Par défaut, 127.0.0.1 ne serait accessible que depuis le conteneur lui-même
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
Comme .gitignore, le fichier .dockerignore empêche de copier des fichiers inutiles dans l'image :
Définition : Fichier listant les motifs de fichiers/dossiers à exclure lors de la copie dans le conteneur (instruction COPY dans le Dockerfile).
But : Réduire la taille de l'image en évitant de copier des fichiers non nécessaires à la production.
Pourquoi ici : Votre environnement virtuel local (venv/), les fichiers Python compilés (__pycache__/), et le historique Git (.git/) ne doivent jamais entrer dans l'image Docker.
# Dossiers temporaires et environnements locaux
venv/
__pycache__/
*.pyc
*.pyo
# Secrets et configuration locale (ne jamais inclure dans l'image)
.env
.env.local
# Contrôle de version et documentation
.git
.gitignore
*.md
# Cache et fichiers de test
.pytest_cache/
.coverage
Définition : Ensemble immuable de couches empilées (layers) contenant un système d'exploitation minimal, des dépendances et du code. C'est un modèle ("blueprint") qui sert à créer des conteneurs.
But : Avoir un paquet portable et versionnné qui peut être exécuté sur n'importe quelle machine ayant Docker.
Pourquoi ici : Une image est à un conteneur ce qu'une classe est à une instance. On bâtit l'image une fois, puis on crée autant de conteneurs qu'on veut à partir de cette image.
Définition : Snapshot d'un changement de filesystem créé par une instruction Docker (FROM, COPY, RUN, etc.). Les layers sont empilées pour former l'image finale.
But : Permettre au Docker d'utiliser le cache : si une layer n'a pas changé, elle peut être réutilisée.
Pourquoi ici : Comprendre les layers est essentiel pour optimiser les builds. L'ordre des instructions dans le Dockerfile détermine l'efficacité du cache.
# Construire l'image Docker
# -t mon-api:v1 : donne un nom et une étiquette (tag) à l'image
# . : utilise le Dockerfile du répertoire courant
docker build -t mon-api:v1 .
# Vérifier que l'image est créée et voir sa taille
docker images | grep mon-api
mon-api v1 abc123def456 2 minutes ago 180MBDéfinition : Instance en exécution d'une image Docker. C'est un processus isolé avec son propre système de fichiers, réseau et variables d'environnement.
But : Isoler complètement l'application : un conteneur ne voit que ses propres fichiers, ports et variables, sans interférer avec la machine hôte ou d'autres conteneurs.
Pourquoi ici : Contrairement à une machine virtuelle lourde, un conteneur Docker est léger (quelques secondes au démarrage) et peut tourner directement sur le kernel du système hôte.
# Lancer un conteneur basé sur l'image mon-api:v1
# -d : détaché (mode background, libère le terminal)
# -p 8000:8000 : publie le port 8000 du conteneur sur le port 8000 de l'hôte
# (sans cela, l'API serait inaccessible de l'extérieur du conteneur)
# --name api-container : donne un nom au conteneur (plus lisible que un ID aléatoire)
docker run -d -p 8000:8000 --name api-container mon-api:v1
# Lister les conteneurs en cours d'exécution
docker ps
# Afficher les logs du conteneur en temps réel (-f = follow)
docker logs -f api-container
# Test 1 : route racine
curl http://localhost:8000/
# Retour : {"message":"API opérationnelle 🚀","timestamp":"2025-01-15T10:30:00"}
# Test 2 : récupérer tous les items (GET /items)
curl http://localhost:8000/items
# Retour : [{"name":"Laptop","price":999.99,"in_stock":true},...]
# Test 3 : créer un nouvel item (POST /items)
curl -X POST http://localhost:8000/items \
-H "Content-Type: application/json" \
-d '{"name":"Clavier","price":79.99,"in_stock":true}'
# Retour : {"name":"Clavier","price":79.99,"in_stock":true}
# Test 4 : health check pour vérifier que l'API répond
curl http://localhost:8000/health
# Retour : {"status":"healthy"}
En production, on ne code jamais les secrets en dur. On utilise les variables d'environnement :
# Importer os pour lire les variables d'environnement
import os
from fastapi import FastAPI
# Lire les variables d'environnement, avec valeurs par défaut
# En production, ces valeurs seront surchargées par le déploiement
APP_ENV = os.getenv("APP_ENV", "development")
SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production")
DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")
app = FastAPI()
# Endpoint qui expose l'environnement courant (utile pour déboguer)
@app.get("/")
def root():
return {"env": APP_ENV, "message": "API opérationnelle"}
# Méthode 1 : Passer les variables directement avec -e
docker run -d -p 8000:8000 \
-e APP_ENV=production \
-e SECRET_KEY=mon-super-secret \
-e DATABASE_URL=postgresql://user:pass@db:5432/mydb \
--name api-prod \
mon-api:v1
# Méthode 2 : Charger toutes les variables d'un fichier .env
# (plus pratique en production, moins de verbosité)
docker run -d -p 8000:8000 \
--env-file .env \
--name api-prod \
mon-api:v1
En pratique, votre API a besoin d'une base de données. Docker Compose orchestre plusieurs conteneurs ensemble :
Définition : Outil pour définir et exécuter plusieurs conteneurs Docker en une seule commande via un fichier YAML (docker-compose.yml).
But : Gérer des architectures multi-conteneurs : API + base de données + cache + queue, tout en local avant de déployer.
Pourquoi ici : Une API seule n'est pas utile. Docker Compose permet de tester l'intégration entre plusieurs services avant la production.
Définition : Serveur centralisé stockant des images Docker publiques ou privées. Docker Hub est le registry officiel gratuit.
But : Partager et télécharger des images préconstruites (postgres, redis, nginx, etc.) au lieu de les créer de zéro.
Pourquoi ici : Nous utilisons postgres:16-alpine et adminer depuis Docker Hub plutôt que de les créer nous-mêmes, gagnant du temps.
# Version de la syntaxe Docker Compose
version: '3.9'
services:
# ── Service API FastAPI ──
api:
# build: . va construire l'image à partir du Dockerfile
build: .
ports:
# Mapper port 8000 hôte vers port 8000 du conteneur
- "8000:8000"
environment:
# Variables d'environnement pour ce conteneur
- APP_ENV=development
# @db : le conteneur db est accessible par ce hostname (réseau Docker interne)
- DATABASE_URL=postgresql://user:password@db:5432/mydb
depends_on:
# Attendre que le service db soit "healthy" avant de démarrer l'API
db:
condition: service_healthy
volumes:
# Monter le répertoire local /app du conteneur
# Permet le hot reload : modifications du code = rechargement auto
- .:/app
# Commande override : lancer uvicorn avec --reload (watch mode)
command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload
# ── Service PostgreSQL ──
db:
# Utiliser l'image PostgreSQL 16 sur base Alpine (ultra-légère)
image: postgres:16-alpine
environment:
# Identifiants de la base de données
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
volumes:
# Monter un volume pour persister les données entre les redémarrages
- postgres_data:/var/lib/postgresql/data
# Health check : Docker Compose teste si PostgreSQL est prêt
healthcheck:
# Commande pour vérifier la santé du service
test: ["CMD-SHELL", "pg_isready -U user"]
# Tester toutes les 5 secondes
interval: 5s
# Timeout de la commande : 5 secondes
timeout: 5s
# Nombre d'essais avant de considérer le service unhealthy
retries: 5
# ── Service Adminer : interface web pour PostgreSQL ──
adminer:
# Image officielle pour gérer les bases de données graphiquement
image: adminer
ports:
- "8080:8080"
# Attendre que PostgreSQL soit prêt
depends_on:
- db
# ── Volumes nommés ──
# postgres_data : crée un volume Docker pour persister les données PostgreSQL
# Survit même si le conteneur est supprimé
volumes:
postgres_data:
# Démarrer tous les services définis dans docker-compose.yml
# -d : mode détaché (background)
docker compose up -d
# Voir les logs en direct de tous les services
# -f : follow (continuez à regarder les nouveaux logs)
docker compose logs -f
# Arrêter et supprimer tous les conteneurs (données du volume persistent)
docker compose down
# Arrêter ET supprimer les volumes (réinitialise complètement la DB)
# ⚠️ À utiliser avec prudence : supprime les données !
docker compose down -v
# ── Gérer les conteneurs ──
# Lister les conteneurs en cours d'exécution
docker ps
# Lister tous les conteneurs (actifs et arrêtés)
docker ps -a
# Arrêter proprement un conteneur (SIGTERM)
docker stop mon-conteneur
# Supprimer un conteneur arrêté
docker rm mon-conteneur
# Forcer la suppression immédiate d'un conteneur (même s'il tourne)
docker rm -f mon-conteneur
# ── Entrer dans un conteneur (debug interactif) ──
# Ouvrir un shell Bash dans le conteneur
docker exec -it mon-conteneur bash
# Exécuter une commande sans interaction
docker exec mon-conteneur ls /app
# ── Gérer les images ──
# Lister toutes les images téléchargées/construites
docker images
# Supprimer une image spécifique
docker rmi mon-api:v1
# Supprimer les images non utilisées par aucun conteneur
docker image prune
# ── Nettoyage complet ──
# Supprimer conteneurs, images, réseaux et caches non utilisés
# ⚠️ Radical : à utiliser avec prudence !
docker system prune -a
mon-api/
├── main.py # Code de l'API FastAPI
├── requirements.txt # Dépendances Python
├── Dockerfile # Recette de l'image Docker
├── docker-compose.yml # Orchestration multi-services
├── .dockerignore # Fichiers exclus de l'image
└── .env # Variables d'environnement (ne pas committer !)
.env contenant des secrets sur Git. Ajoutez-le dans votre .gitignore.Vous savez maintenant :
La prochaine étape logique est d'optimiser l'image pour la production avec les multi-stage builds, ou de déployer sur Cloud Run en quelques minutes.