Comment dockeriser une application FastAPI

 

Dans ce tutoriel, nous allons dockeriser une API web FastAPI avec un Dockerfile multi-étapes : une première étape prépare les dépendances Python, la seconde produit une image Alpine qui ne contient que ce qu’il faut pour faire tourner l’API avec uvicorn.

Dans cet article :

  • Structure du projet
  • Le Dockerfile multi-étapes
  • La healthcheck
  • Le fichier .dockerignore
  • Construire et lancer l’image

Pré-requis : savoir créer une API FastAPI minimale (sinon, commencez par Python : Comment faire une api web avec FastAPI) et avoir installé Docker (guide officiel).

Structure du projet

On part d’un projet minimal, avec trois fichiers à la racine :

.
├── app.py
├── requirements.txt
└── Dockerfile

Le fichier app.py contient l’application FastAPI, avec deux routes : GET / pour le “hello world”, et GET /info/status que Docker appellera pour vérifier que l’API répond.

from fastapi import FastAPI

app = FastAPI()

@app.get("/")
def root():
    return "Hello World"

@app.get("/info/status")
def info_status():
    # Route appelée par la healthcheck du Dockerfile
    return {"status": "ok"}

Le fichier requirements.txt liste les dépendances :

fastapi
uvicorn[standard]

L’extra standard d’uvicorn installe notamment uvloop, une boucle d’événements plus rapide que celle d’asyncio, et httptools pour analyser les requêtes HTTP : uvicorn les utilise automatiquement quand ils sont présents. Pour que deux builds donnent la même image, il vaut mieux fixer les versions dans ce fichier (pip freeze donne celles de votre environnement).

Le Dockerfile multi-étapes

Un Dockerfile multi-étapes contient plusieurs FROM. Chaque étape part de sa propre image, et l’image finale ne garde que la dernière étape, plus ce qu’on y copie explicitement depuis les précédentes. On s’en sert ici pour garder les outils de compilation hors de l’image finale.

Créez ce Dockerfile à la racine du projet :

# ============================================================================
# Étape 1 : builder, construit les wheels (.whl) des dépendances Python
# ----------------------------------------------------------------------------
FROM python:3.13.5-alpine3.22 AS builder

# Dossier de travail dans le conteneur (tous les chemins seront relatifs à /app)
WORKDIR /app

# Outils pour compiler les dépendances qui n'ont pas de wheel pour Alpine
# (build-base contient gcc, make et les en-têtes de la libc musl)
RUN apk add --no-cache \
    build-base \
    libffi-dev \
    openssl-dev

# On copie uniquement les dépendances pour profiter du cache Docker
COPY requirements.txt .

# On construit les wheels pour toutes les dépendances spécifiées
RUN pip wheel --no-cache-dir --wheel-dir /app/wheels -r requirements.txt

# ============================================================================
# Étape 2 : image finale, sans les outils de compilation
# ----------------------------------------------------------------------------
FROM python:3.13.5-alpine3.22

# Dossier de travail
WORKDIR /app

# (Optionnel) Installer curl si vous utilisez la healthcheck basée sur curl
# RUN apk add --no-cache curl

# On copie les wheels produites par l'étape builder
COPY --from=builder /app/wheels /app/wheels

# On installe les dépendances à partir des wheels locales (pas d'accès réseau)
COPY requirements.txt .
RUN pip install --no-cache-dir --no-index --find-links=/app/wheels -r requirements.txt \
    && rm -rf /app/wheels \
    && rm -rf /root/.cache/pip \
    && find /usr/local -type d -name __pycache__ -exec rm -rf {} +

# On copie le code de l'application (app.py, etc.)
COPY . .

# On expose le port d'écoute de l'API dans le conteneur
EXPOSE 8000

# Healthcheck : vérifie périodiquement que l'API répond sur /info/status.
# L'image Alpine fournit déjà wget (via busybox), donc pas besoin d'installer curl.
# Variante avec curl (si vous l'avez installé plus haut) :
#   CMD curl -fs http://127.0.0.1:8000/info/status | grep -q '"status":"ok"' || exit 1
HEALTHCHECK --interval=60s --timeout=10s --start-period=5s --retries=3 \
  CMD wget -qO- http://127.0.0.1:8000/info/status || exit 1

# Commande de lancement : Uvicorn sert l'application FastAPI (objet `app`)
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "8000"]

La première étape s’appelle builder (AS builder). On y installe build-base (gcc, make et les en-têtes de musl, la libc d’Alpine) et les en-têtes de libffi et d’OpenSSL, utilisés par certaines dépendances écrites en C. Puis pip wheel prépare une wheel, un paquet Python prêt à installer, pour chaque dépendance. Inutile d’ajouter le paquet Alpine python3-dev : il contient les en-têtes du Python 3.12 de la distribution, alors que l’image officielle python embarque déjà ceux de son propre Python 3.13 dans /usr/local/include.

Avec le requirements.txt de cet article, pip trouve pour toutes les dépendances des wheels déjà compilées pour Alpine (musllinux), et ne compile rien (vérifié en octobre 2026). Les outils de compilation ne serviront que le jour où vous ajouterez une dépendance sans wheel pour Alpine.

La seconde étape repart de l’image python d’origine : les compilateurs restent dans l’étape builder, et seules les wheels sont copiées. pip install --no-index --find-links les installe ensuite sans aller chercher quoi que ce soit sur PyPI. Les dépendances sont installées avant de copier le code : tant que requirements.txt ne change pas, Docker réutilise ces couches en cache, et une modification de app.py ne relance pas l’installation.

Attention, le rm -rf /app/wheels fait disparaître les wheels du système de fichiers de l’image, mais ne la fait pas maigrir : elles restent stockées dans la couche créée par le COPY --from=builder (une dizaine de Mo ici), et une couche suivante ne peut que les masquer. Pour éviter cette couche, BuildKit, le moteur de build par défaut de Docker, permet de monter le dossier de l’étape builder le temps d’un RUN. On remplace alors le COPY --from=builder et le RUN pip install par :

COPY requirements.txt .
RUN --mount=type=bind,from=builder,source=/app/wheels,target=/tmp/wheels \
    pip install --no-cache-dir --no-index --find-links=/tmp/wheels -r requirements.txt \
    && find /usr/local -type d -name __pycache__ -exec rm -rf {} +

Enfin, CMD lance uvicorn sur 0.0.0.0 : par défaut il n’écoute que sur 127.0.0.1, et l’API ne serait alors pas joignable depuis l’extérieur du conteneur. Si votre application est découpée en plusieurs fichiers, comme dans l’article Organiser une application FastAPI en plusieurs fichiers, adaptez le module à lancer (app.main:app au lieu de app:app).

La healthcheck

L’instruction HEALTHCHECK demande à Docker d’appeler /info/status toutes les 60 secondes. wget, fourni par busybox dans l’image Alpine, renvoie un code d’erreur si l’API ne répond pas ou répond avec une erreur HTTP : pas besoin d’installer curl. Après 3 échecs consécutifs (--retries=3), le conteneur est marqué unhealthy.

Si vous préférez curl, décommentez la ligne RUN apk add --no-cache curl et utilisez la variante donnée en commentaire, qui vérifie en plus le contenu de la réponse.

Le fichier .dockerignore

COPY . . copie tout le dossier du projet dans l’image. Pour ne pas y embarquer un environnement virtuel, l’historique git ou un fichier .env contenant des secrets, créez un fichier .dockerignore à côté du Dockerfile :

__pycache__
*.pyc
*.pyo
*.pyd
.env
.venv
venv
.git
.gitignore
.DS_Store
.idea
.vscode

# Dossiers de build
build
wheels

Construire et lancer l’image

Depuis la racine du projet (là où se trouve le Dockerfile) :

docker build -t fastapi-app:latest .

On lance le conteneur en mappant son port 8000 sur le port 8000 de la machine :

docker run --rm -p 8000:8000 --name fastapi-app fastapi-app:latest

L’API répond alors sur http://127.0.0.1:8000/, avec sa documentation sur /docs (Swagger UI) et /redoc (ReDoc) :

curl http://127.0.0.1:8000/
"Hello World"

curl http://127.0.0.1:8000/info/status
{"status":"ok"}

Pour voir l’état de la healthcheck :

docker inspect --format='{{json .State.Health}}' fastapi-app | jq

Le champ Status vaut starting jusqu’au premier contrôle réussi, puis healthy. Depuis la version 27, Docker lance un contrôle toutes les 5 secondes pendant la start-period : le conteneur passe donc à healthy environ 5 secondes après son démarrage. Avec une version plus ancienne, le premier contrôle n’a lieu qu’au bout de l’interval, soit 60 secondes ici.

Voir aussi