Aller au contenu principal

Docker avec FastAPI

remarque

Ce chapitre vient après Tests avec FastAPI. Il montre comment emballer l’application dans un environnement reproductible avec Docker et Docker Compose, puis comment faire évoluer ce packaging vers une stack plus complète avec PostgreSQL, Redis et Celery.

Introduction

Jusqu’ici, on a construit une API FastAPI sérieuse :

  • base CRUD,
  • authentification,
  • relations SQL,
  • gestion d’erreurs,
  • middleware,
  • tests automatisés.

Mais tant que tout tourne seulement sur ta machine dans ton environnement Python local, tu gardes plusieurs fragilités :

  • "chez moi ça marche" mais pas ailleurs,
  • dépendances installées à la main,
  • version de Python pas toujours identique,
  • base locale qui ne ressemble pas forcément à un vrai environnement d’exécution,
  • difficulté à lancer plusieurs services ensemble.

Docker sert précisément à réduire ce problème.

Ce que ce chapitre va accomplir

À la fin de ce chapitre, tu auras :

  • un vrai Dockerfile pour l’application FastAPI,
  • un .dockerignore propre,
  • un docker-compose.yml pour lancer :
    • l’API,
    • PostgreSQL,
  • une compréhension claire de :
    • image,
    • container,
    • service,
    • volume,
    • réseau Docker,
  • une variante plus proche des sources avec :
    • Redis,
    • Celery,
  • une procédure de vérification avec :
    • docker build,
    • docker compose up,
    • curl,
    • logs.

Pourquoi Docker arrive maintenant

Docker devient vraiment utile après avoir stabilisé l’application.

Pourquoi ? Parce que sinon tu emballes trop tôt un projet encore en train de changer dans tous les sens.

Le bon ordre pédagogique est donc :

  1. construire l’API,
  2. la rendre plus réaliste,
  3. la tester,
  4. ensuite seulement la packager proprement.

C’est exactement la situation actuelle de notre wiki.

Sources d’inspiration réelles

Ce chapitre s’appuie principalement sur :

  • 33-Docker/170-Dockerfile
  • 33-Docker/172-compose.yaml
  • 33-Docker/173-compose.yaml

Ces sources montrent :

  • un Dockerfile Python simple,
  • une stack api + db,
  • une version étendue avec redis et celery.

Choix pédagogique de ce chapitre

Les sources sont utiles, mais on ne va pas les recopier aveuglément.

Par exemple :

  • la source utilise python:3.13-alpine,
  • c’est compact,
  • mais ce n’est pas toujours le choix le plus agréable pour débuter,
  • surtout quand on finit par installer des dépendances Python avec extensions natives.

Pour ce wiki, on garde la logique de la source :

  • image Python,
  • WORKDIR,
  • copie des dépendances,
  • installation,
  • copie du code,
  • exécution de l’API,
  • orchestration multi-services avec Compose.

Mais on adapte certains détails pour avoir une version :

  • plus stable,
  • plus explicite,
  • plus pédagogique.

Architecture visée après ce chapitre

Après ce chapitre, le projet peut ressembler à ceci :

fastapi-base-api/
├── main.py
├── config.py
├── requirements.txt
├── .gitignore
├── .env
├── .env.example
├── .dockerignore
├── Dockerfile
├── docker-compose.yml
├── core/
│ ├── __init__.py
│ ├── security.py
│ ├── exceptions.py
│ └── middleware.py
├── api/
│ ├── __init__.py
│ ├── router.py
│ ├── dependencies.py
│ ├── routers/
│ │ ├── __init__.py
│ │ ├── task.py
│ │ └── user.py
│ └── schemas/
│ ├── __init__.py
│ ├── task.py
│ └── user.py
├── database/
│ ├── __init__.py
│ ├── models.py
│ ├── session.py
│ └── redis.py # seulement si tu actives la variante Redis / logout robuste
├── services/
│ ├── __init__.py
│ ├── task.py
│ └── user.py
├── tests/
│ ├── __init__.py
│ ├── example.py
│ ├── conftest.py
│ ├── test_auth.py
│ └── test_task.py
└── worker/
└── tasks.py # seulement si tu ajoutes Celery plus tard

Pré-requis

Avant de continuer, vérifie que tu as :

  • Docker installé,
  • Docker Compose disponible via docker compose,
  • un projet FastAPI déjà fonctionnel selon les chapitres précédents,
  • un fichier .env avec au minimum les variables PostgreSQL et JWT.

Exemple de .env côté projet :

POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=taskapi_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_real_password
JWT_SECRET=your_long_random_secret
JWT_ALGORITHM=HS256
remarque

Quand l’application tourne dans Docker Compose, on ne veut plus que POSTGRES_SERVER vaille localhost. Dans le réseau Docker, le bon hostname sera db. On va justement faire cette adaptation sans casser ton usage local hors Docker.

Repères utiles avant le code

Avant d’écrire les fichiers Docker, retiens ces cinq idées :

1. Une image n’est pas encore un container

Une image est un modèle prêt à être exécuté. Un container est une instance lancée à partir de cette image.

2. Docker Compose orchestre plusieurs services

Dans notre cas, on a au moins :

  • api
  • db

Et plus tard éventuellement :

  • redis
  • celery

3. Les services se parlent par leur nom

Dans Compose :

  • l’API parlera à PostgreSQL via db,
  • pas via localhost.

4. Les volumes servent à conserver des données

Sans volume :

  • tu supprimes le container,
  • tu perds les données de la base.

Avec volume :

  • tes données PostgreSQL survivent aux redémarrages.

5. depends_on ne remplace pas complètement l’attente de disponibilité

Un service peut être démarré sans être prêt. C’est pourquoi on va ajouter un healthcheck sur PostgreSQL.

Étape 1 — créer .dockerignore

Commence par éviter d’envoyer des fichiers inutiles dans le contexte de build.

Crée .dockerignore :

__pycache__/
*.pyc
*.pyo
*.pyd
.pytest_cache/
.mypy_cache/
.venv/
venv/
.env
.git/
.gitignore
postgres_data/
redis_data/

Pourquoi ce fichier est important ?

Il évite de copier dans l’image :

  • les caches Python,
  • l’environnement virtuel local,
  • les secrets du .env,
  • les dossiers de données locaux,
  • des fichiers Git inutiles pour l’exécution.

Résultat :

  • build plus rapide,
  • image plus propre,
  • moins de surprises.

Étape 2 — créer le Dockerfile

La source montre un Dockerfile très minimal. On garde l’idée, mais on choisit ici une base plus confortable : python:3.12-slim.

Crée Dockerfile :

FROM python:3.12-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

COPY requirements.txt .

RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

Explication ligne par ligne

FROM python:3.12-slim

On part d’une image Python officielle légère mais plus simple à vivre qu’Alpine pour un projet pédagogique.

PYTHONDONTWRITEBYTECODE=1

Évite la génération de fichiers .pyc inutiles dans le container.

PYTHONUNBUFFERED=1

Rend les logs Python plus lisibles en temps réel.

WORKDIR /app

Tous les chemins suivants sont relatifs à /app.

COPY requirements.txt .

On copie d’abord uniquement requirements.txt.

Pourquoi ? Parce que Docker peut réutiliser le cache si le code change mais pas les dépendances.

RUN pip install ...

On installe les dépendances du projet.

COPY . .

On copie le reste du code applicatif.

EXPOSE 8000

On documente le port prévu pour l’application.

CMD [...]

On lance Uvicorn sur 0.0.0.0.

attention

Dans un container, 127.0.0.1 ne suffit pas. Il faut écouter sur 0.0.0.0 pour que le port exposé soit réellement accessible depuis l’extérieur du container.

Étape 3 — tester le build seul

Avant d’orchestrer plusieurs services, tu peux déjà vérifier que l’image se construit.

docker build -t fastapi-base-api .

Si tout se passe bien, tu dois obtenir une image prête à lancer.

Tu peux vérifier :

docker images | grep fastapi-base-api

Étape 4 — comprendre le vrai besoin Compose

Le build seul est utile, mais il ne suffit pas.

Notre API dépend d’une base PostgreSQL. Donc si tu lances seulement :

docker run --rm -p 8000:8000 fastapi-base-api

l’application ne saura pas où se connecter si la base n’existe pas dans le même environnement.

C’est pour cela qu’on passe maintenant à Docker Compose.

Étape 5 — créer un docker-compose.yml propre pour api + db

Ici, on construit une version adaptée à notre projet.

Crée docker-compose.yml :

services:
api:
build: .
env_file:
- .env
environment:
POSTGRES_SERVER: db
POSTGRES_PORT: 5432
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: your_real_password
POSTGRES_DB: taskapi_db
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d taskapi_db"]
interval: 5s
timeout: 5s
retries: 10

volumes:
postgres_data:

Pourquoi cette version est adaptée à notre wiki

Côté api

On charge .env, puis on override seulement ce qui change dans Docker :

  • POSTGRES_SERVER: db
  • POSTGRES_PORT: 5432

Résultat :

  • en local hors Docker, ton .env peut rester avec localhost,
  • en Docker Compose, l’API parle au service db sans casser le reste de ton setup.

Côté db

On crée une base PostgreSQL standard.

Port 5433:5432

On expose PostgreSQL sur 5433 côté machine hôte pour éviter les collisions avec un éventuel PostgreSQL local déjà installé.

Volume postgres_data

Les données persistent même si tu redémarres les containers.

Healthcheck

L’API attend une base vraiment prête, pas juste un container lancé.

Étape 6 — garder les secrets cohérents

Dans l’exemple ci-dessus, la base reçoit :

POSTGRES_PASSWORD: your_real_password
POSTGRES_DB: taskapi_db
POSTGRES_USER: postgres

Il faut que cela corresponde à ce que ton application utilise réellement.

Donc ton .env local doit rester cohérent avec ces valeurs :

POSTGRES_SERVER=localhost
POSTGRES_PORT=5432
POSTGRES_DB=taskapi_db
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_real_password
JWT_SECRET=your_long_random_secret
JWT_ALGORITHM=HS256
astuce

Le plus important à comprendre ici n’est pas juste la valeur du mot de passe. Le plus important, c’est que l’application et PostgreSQL doivent partager exactement les mêmes identifiants, et que seul le hostname change entre local (localhost) et Docker (db).

Étape 7 — lancer la stack

Lance ensuite :

docker compose up --build

Que va faire cette commande ?

  • construire l’image de l’API,
  • télécharger l’image PostgreSQL si nécessaire,
  • créer le réseau Compose,
  • démarrer la base,
  • attendre son état healthy,
  • démarrer l’API.

Étape 8 — vérifier que tout tourne

Dans un autre terminal :

docker compose ps

Tu dois voir au minimum :

  • api
  • db

Ensuite :

curl http://localhost:8000/docs

Ou, si tu veux voir les headers et le statut :

curl -i http://localhost:8000/docs

Tu peux aussi tester une route applicative réelle :

curl -i http://localhost:8000/task/

Selon ton implémentation exacte :

  • soit tu obtiens une liste vide,
  • soit tu obtiens une erreur métier attendue,
  • soit tu dois d’abord t’authentifier pour certains endpoints.

L’important ici est surtout de vérifier que :

  • l’API répond,
  • la connexion DB fonctionne,
  • les logs ne montrent pas d’erreur de connexion PostgreSQL.

Étape 9 — lire les logs correctement

Pour suivre les logs de l’API :

docker compose logs api -f

Pour PostgreSQL :

docker compose logs db -f

Ces logs sont précieux pour diagnostiquer :

  • mauvais mot de passe,
  • mauvais nom de base,
  • problème de startup,
  • import Python cassé,
  • dépendance manquante.

Étape 10 — arrêter proprement

Pour arrêter :

docker compose down

Pour arrêter et supprimer aussi le volume de données :

docker compose down -v
attention

docker compose down -v supprime le volume PostgreSQL. Donc tu perds les données de la base de ce compose. Utilise-le seulement si c’est ce que tu veux vraiment.

Étape 11 — variante plus proche des sources : ajouter Redis et Celery

La source 173-compose.yaml pousse la stack plus loin avec :

  • redis
  • celery

Dans notre wiki, cette variante est logique si tu veux :

  • un logout robuste avec blacklist Redis,
  • des tâches asynchrones plus tard,
  • préparer un futur chapitre Celery.

Voici une version étendue du compose :

services:
api:
build: .
env_file:
- .env
environment:
POSTGRES_SERVER: db
POSTGRES_PORT: 5432
REDIS_HOST: redis
REDIS_PORT: 6379
ports:
- "8000:8000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

db:
image: postgres:16
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: your_real_password
POSTGRES_DB: taskapi_db
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d taskapi_db"]
interval: 5s
timeout: 5s
retries: 10

redis:
image: redis:7
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data

celery:
build: .
env_file:
- .env
environment:
POSTGRES_SERVER: db
POSTGRES_PORT: 5432
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
command: ["celery", "-A", "worker.tasks", "worker", "--loglevel=info"]

volumes:
postgres_data:
redis_data:

Très important sur la variante Celery

Le service :

command: ["celery", "-A", "worker.tasks", "worker", "--loglevel=info"]

est seulement un exemple adapté.

Il faudra que le chemin après -A corresponde à ton vrai module Celery.

Par exemple, plus tard, ce pourrait être :

  • worker.tasks
  • worker.celery_app
  • core.celery_app

selon la structure que tu choisiras réellement dans le chapitre Celery.

Autrement dit :

  • api + db = prêt maintenant,
  • redis + celery = prêt conceptuellement, mais dépendra du futur code réellement écrit.

Étape 12 — erreurs fréquentes et comment les comprendre

Erreur 1 — connection to server at "localhost" failed

Cause fréquente :

  • ton application tourne dans Docker,
  • mais elle essaie encore de parler à PostgreSQL sur localhost.

Correction :

  • dans Compose, assure-toi d’avoir :
environment:
POSTGRES_SERVER: db
POSTGRES_PORT: 5432

Erreur 2 — password authentication failed

Cause fréquente :

  • le mot de passe côté API ne correspond pas à celui du service PostgreSQL.

Correction :

  • aligne .env et le bloc db.environment.

Erreur 3 — ModuleNotFoundError

Cause fréquente :

  • dépendance oubliée dans requirements.txt.

Correction :

  • mets à jour requirements.txt,
  • relance :
docker compose up --build

Erreur 4 — l’API démarre trop tôt

Cause fréquente :

  • PostgreSQL n’est pas encore prêt.

Correction :

  • garde le healthcheck,
  • garde depends_on avec condition: service_healthy.

Erreur 5 — les changements de code ne sont pas visibles

Cause fréquente :

  • tu n’as pas rebuild l’image.

Correction :

docker compose up --build

Étape 13 — ce que ce chapitre ne fait pas encore

Ce chapitre ne couvre pas encore :

  • un déploiement production complet,
  • un reverse proxy nginx,
  • Alembic,
  • multi-stage build,
  • optimisation fine de taille d’image,
  • stratégie de secrets en production.

Et c’est normal.

Le but ici est de t’apprendre :

  • à dockeriser proprement une API FastAPI,
  • à la lancer avec PostgreSQL,
  • à comprendre comment la stack peut s’étendre ensuite.

Bonnes pratiques à retenir

1. Ne copie pas ton .env dans l’image

On l’injecte au runtime via Compose.

2. Préfère une première version claire à une version ultra-optimisée

Un Dockerfile compréhensible vaut mieux qu’une image trop "maligne" mais opaque.

3. Garde un compose minimal tant que le projet n’a pas besoin de plus

Commence avec :

  • api
  • db

Puis ajoute seulement ensuite :

  • redis
  • celery

4. Différencie bien : local hors Docker vs réseau Docker

  • hors Docker : localhost
  • dans Compose : nom du service (db, redis)

5. Les logs sont ton meilleur outil de diagnostic

Quand quelque chose ne marche pas, commence presque toujours par :

docker compose logs api -f
docker compose logs db -f

Checklist de validation

À la fin, tu dois pouvoir cocher tout ceci :

  • j’ai créé un .dockerignore
  • j’ai créé un Dockerfile
  • mon image se build sans erreur
  • j’ai créé un docker-compose.yml
  • l’API parle bien à PostgreSQL via db
  • docker compose up --build démarre correctement la stack
  • curl http://localhost:8000/docs répond
  • je sais lire les logs api et db
  • je comprends la différence entre la version minimale et la version étendue avec Redis / Celery

Résumé final

À ce stade, ton projet FastAPI n’est plus seulement un projet Python qui marche sur ta machine. Il commence à devenir une application exécutable dans un environnement reproductible.

C’est une étape importante, parce qu’elle rapproche ton projet de plusieurs réalités :

  • partage avec d’autres développeurs,
  • exécution cohérente sur d’autres machines,
  • préparation au déploiement,
  • montée vers une stack plus complète.

Le plus important à retenir est simple :

  • Docker ne remplace pas une bonne architecture,
  • Docker emballe une bonne architecture,
  • et Docker Compose te permet de faire vivre ensemble les services dont ton application dépend.

Pour aller plus loin