# Fichier: docker_cheats/cheatsheets/docker_python.txt
# Docker pour Python - Guide Complet pour Débutants


[OK] DOCKER POUR PYTHON - INTRODUCTION

# === POURQUOI DOCKER AVEC PYTHON? ===

# Imaginez cette situation courante:
# Vous développez une application Python sur votre machine.
# Ça marche parfaitement chez vous.
# Vous l'envoyez à un collègue ou déployez en production...
# Et ça ne marche plus ! Pourquoi?

# Problèmes SANS Docker:

# 1. "Ça marche sur ma machine" 
#    - Votre machine: Python 3.11, Ubuntu 22.04
#    - Serveur production: Python 3.8, CentOS 7
#    - Résultat: comportements différents, bugs mystérieux

# 2. Conflits de versions Python
#    - Projet A besoin de Python 3.8
#    - Projet B besoin de Python 3.11
#    - Installation simultanée = complexe (pyenv, virtualenv, etc.)

# 3. Conflits de dépendances
#    - Projet A: Django 3.2
#    - Projet B: Django 4.2
#    - Même avec virtualenv, gérer tout ça devient compliqué

# 4. Installation compliquée pour nouveaux développeurs
#    - "Installe Python 3.11"
#    - "Installe PostgreSQL"
#    - "Installe Redis"
#    - "Configure les variables d'environnement"
#    - "Crée la base de données"
#    - = 2 heures pour commencer à travailler!

# 5. Environnements différents
#    - Développement: SQLite, debug activé
#    - Staging: PostgreSQL, configs spéciales
#    - Production: PostgreSQL, Redis, configs strictes
#    - = Trois configs à maintenir, bugs qui apparaissent en prod

# Avantages AVEC Docker:

# [OK] Même environnement PARTOUT
#    - Un Dockerfile décrit EXACTEMENT l'environnement
#    - Développement, staging, production = IDENTIQUES
#    - Plus de "ça marche chez moi mais pas chez toi"

# [OK] Isolation complète entre projets
#    - Chaque projet dans son propre conteneur
#    - Projet A: Python 3.8 + Django 3.2
#    - Projet B: Python 3.11 + Django 4.2
#    - Aucun conflit, tout coexiste paisiblement

# [OK] Setup ultra-simple
#    - Nouveau développeur arrive dans l'équipe
#    - Il fait: git clone + docker-compose up
#    - C'EST TOUT! En 5 minutes il développe

# [OK] Tous les services inclus
#    - PostgreSQL? Lancé automatiquement
#    - Redis? Lancé automatiquement
#    - Elasticsearch? Lancé automatiquement
#    - Tout est défini dans docker-compose.yml

# [OK] Déploiement simplifié
#    - docker build + docker push
#    - Sur le serveur: docker pull + docker run
#    - Fini les scripts de déploiement compliqués

# === CONCEPTS CLÉS À COMPRENDRE ===

# 1. IMAGE vs CONTENEUR
#    IMAGE = modèle, blueprint, recette de cuisine
#    CONTENEUR = instance qui tourne, application en cours d'exécution
#    
#    Analogie:
#    - Image = Plan d'une maison
#    - Conteneur = Maison construite et habitée
#    
#    Vous pouvez avoir:
#    - 1 image "myapp"
#    - 10 conteneurs qui tournent depuis cette image

# 2. DOCKERFILE
#    Fichier texte qui décrit COMMENT construire une image
#    C'est la RECETTE pour créer votre environnement
#    
#    Exemple simple:
#    FROM python:3.11        <- Partir de l'image Python officielle
#    COPY . /app             <- Copier votre code
#    RUN pip install ...     <- Installer dépendances
#    CMD python app.py       <- Lancer l'application

# 3. DOCKER-COMPOSE
#    Outil pour gérer plusieurs conteneurs ensemble
#    Un fichier YAML qui décrit:
#    - Votre app Python
#    - PostgreSQL
#    - Redis
#    - Nginx
#    Et comment ils communiquent entre eux
#    
#    Une seule commande: docker-compose up
#    Lance TOUT automatiquement!

# 4. VOLUMES
#    Permettent de persister les données
#    Sans volume: données perdues quand conteneur s'arrête
#    Avec volume: données sauvegardées sur votre disque
#    
#    Deux types:
#    - Volume nommé: postgres_data (géré par Docker)
#    - Bind mount: ./code:/app (lie un dossier de votre machine)

# 5. RÉSEAU
#    Les conteneurs peuvent communiquer entre eux
#    Par défaut, docker-compose crée un réseau
#    Les conteneurs se voient par leur nom de service
#    
#    Exemple:
#    - Service "web" peut accéder à "db" via le nom "db"
#    - Pas besoin d'IP, Docker gère le DNS automatiquement

# === WORKFLOW TYPIQUE ===

# Sans Docker (traditionnel):
# 1. Installer Python sur chaque machine
# 2. Créer virtualenv
# 3. pip install -r requirements.txt
# 4. Installer PostgreSQL
# 5. Créer base de données
# 6. Configurer variables d'environnement
# 7. python manage.py migrate
# 8. python manage.py runserver
# = 30+ minutes, plein d'étapes manuelles

# Avec Docker:
# 1. git clone <repo>
# 2. docker-compose up
# = 2 minutes, TOUT est automatisé!

# === PRÉREQUIS ===

# Avant de commencer, vous devez avoir installé:
# - Docker Desktop (Windows/Mac) ou Docker Engine (Linux)
# - docker-compose (généralement inclus dans Docker Desktop)

# Vérifier l'installation:
docker --version
# Output: Docker version 24.0.0, build xxx

docker-compose --version
# Output: Docker Compose version v2.23.0

# Si ces commandes fonctionnent, vous êtes prêt!


[OK] DOCKERFILE PYTHON BASIQUE

# === VERSION SIMPLE (POUR COMMENCER) ===

# Créez un fichier nommé "Dockerfile" (pas d'extension!)
# dans le même dossier que votre application Python

# Dockerfile
FROM python:3.11-slim

# WORKDIR = dossier de travail dans le conteneur
# Comme faire "cd /app" mais permanent
WORKDIR /app

# Copier requirements.txt dans le conteneur
COPY requirements.txt .

# Installer les dépendances Python
RUN pip install --no-cache-dir -r requirements.txt

# Copier tout le code de l'application
COPY . .

# Port que l'application utilise (informatif)
EXPOSE 8000

# Commande à exécuter au démarrage
CMD ["python", "app.py"]

# === EXPLICATION DÉTAILLÉE LIGNE PAR LIGNE ===

# ========================================
# FROM python:3.11-slim
# ========================================
# 
# C'est l'IMAGE DE BASE, votre point de départ
# 
# Qu'est-ce qu'une image de base?
# - C'est une image préconstruite que vous utilisez comme fondation
# - python:3.11-slim contient déjà:
#   * Ubuntu Linux
#   * Python 3.11 installé et configuré
#   * pip installé
#   * Bibliothèques système de base
# 
# Pourquoi "slim"?
# - python:3.11 = version COMPLÈTE (~900 MB)
#   * Contient beaucoup d'outils, compilateurs, librairies
#   * Utile si vous compilez du code C/C++
# 
# - python:3.11-slim = version ALLÉGÉE (~150 MB)
#   * Contient juste Python et l'essentiel
#   * Idéal pour la plupart des applications Python
#   * 6x plus petit!
# 
# - python:3.11-alpine = version MINIMALISTE (~50 MB)
#   * Basée sur Alpine Linux (très léger)
#   * ATTENTION: peut avoir des problèmes avec certains packages
#   * Certaines bibliothèques Python (numpy, pandas) sont difficiles à installer
#   * Compilation depuis source souvent nécessaire
# 
# Recommandation pour débutants: TOUJOURS utiliser "slim"
# C'est le meilleur compromis taille/compatibilité
# 
# Exemples d'autres images de base:
# FROM python:3.11-slim         <- Recommandé
# FROM python:3.10-slim         <- Python 3.10
# FROM python:3.9-slim-bullseye <- Debian Bullseye spécifique
# FROM python:3.11              <- Version complète (grosse)
# FROM python:3.11-alpine       <- Très légère (peut avoir problèmes)

# ========================================
# WORKDIR /app
# ========================================
# 
# Définit le DOSSIER DE TRAVAIL dans le conteneur
# 
# Que fait WORKDIR?
# 1. Crée le dossier /app s'il n'existe pas
# 2. Se déplace dans ce dossier (comme "cd /app")
# 3. Tous les chemins relatifs seront basés sur /app
# 
# Pourquoi /app?
# - C'est une CONVENTION (pas obligatoire)
# - Vous pourriez utiliser /code, /src, /myproject
# - Mais /app est le standard, tout le monde l'utilise
# 
# Exemple concret:
# Sans WORKDIR:
#   Vous êtes dans / (racine du système)
#   COPY . . copierait dans /
#   RUN pip install installerait dans /
#   = Bordel dans la racine du système!
# 
# Avec WORKDIR /app:
#   Vous êtes dans /app
#   COPY . . copie dans /app
#   RUN pip install installe dans /app
#   = Tout est organisé dans un dossier dédié
# 
# Équivalent bash:
# RUN mkdir -p /app && cd /app
# 
# Mais WORKDIR est:
# - Plus lisible
# - Affecte TOUTES les instructions suivantes
# - Change aussi le dossier pour CMD/ENTRYPOINT

# ========================================
# COPY requirements.txt .
# ========================================
# 
# Copie un fichier de VOTRE MACHINE vers le CONTENEUR
# 
# Syntaxe: COPY <source> <destination>
# 
# requirements.txt = fichier source (sur votre machine)
# . = destination (dossier actuel dans conteneur = /app)
# 
# Pourquoi copier requirements.txt EN PREMIER?
# C'EST CRUCIAL pour l'OPTIMISATION!
# 
# Docker utilise un système de CACHE par layers:
# - Chaque instruction (FROM, COPY, RUN) crée une "layer"
# - Si une layer ne change pas, Docker la réutilise (CACHE)
# - Si une layer change, Docker refait cette layer ET TOUTES LES SUIVANTES
# 
# Scénario 1 (MAUVAIS):
# COPY . .                          <- Copie TOUT (change souvent)
# RUN pip install -r requirements.txt   <- Réexécuté à CHAQUE changement de code!
# 
# Vous modifiez un commentaire dans votre code
# -> Docker voit que COPY . . a changé
# -> Il refait pip install (peut prendre 5 minutes!)
# 
# Scénario 2 (BON):
# COPY requirements.txt .               <- Change rarement
# RUN pip install -r requirements.txt   <- Utilisé depuis le cache si requirements.txt n'a pas changé
# COPY . .                              <- Copie le code (change souvent)
# 
# Vous modifiez votre code
# -> Docker voit que requirements.txt n'a pas changé
# -> Il utilise le cache pour pip install (instantané!)
# -> Il refait seulement COPY . .
# 
# Gain de temps:
# Sans optimisation: 5 minutes de build à chaque changement
# Avec optimisation: 5 secondes de build
# 
# C'est une des optimisations LES PLUS IMPORTANTES!

# ========================================
# RUN pip install --no-cache-dir -r requirements.txt
# ========================================
# 
# Exécute une commande PENDANT LA CONSTRUCTION de l'image
# 
# RUN vs CMD:
# - RUN: Exécuté pendant le BUILD (docker build)
#        Résultat sauvegardé dans l'image
#        Exemple: installation de packages
# 
# - CMD: Exécuté au DÉMARRAGE du conteneur (docker run)
#        Lance l'application
#        Exemple: python app.py
# 
# pip install -r requirements.txt
# - Installe toutes les dépendances listées dans requirements.txt
# - requirements.txt contient quelque chose comme:
#   django==4.2.8
#   requests==2.31.0
#   psycopg2-binary==2.9.9
# 
# --no-cache-dir
# - Option IMPORTANTE pour Docker
# - Par défaut, pip garde un cache des packages téléchargés
# - Le cache pip prend de l'espace (parfois 500+ MB)
# - Dans Docker, ce cache est INUTILE (l'image est figée)
# - --no-cache-dir économise 200-500 MB dans l'image finale!
# 
# Avec cache: Image de 800 MB
# Sans cache: Image de 300 MB
# 
# Autres options pip utiles:
# --no-cache-dir         <- Pas de cache (économise espace)
# --disable-pip-version-check  <- Pas de vérification version pip (plus rapide)
# --user                 <- Install dans home utilisateur (pour multi-stage)
# 
# Exemple complet avec toutes les optimisations:
# RUN pip install --no-cache-dir --disable-pip-version-check -r requirements.txt

# ========================================
# COPY . .
# ========================================
# 
# Copie TOUT le contenu du dossier actuel vers /app
# 
# Syntaxe: COPY <source> <destination>
# 
# Premier "." = SOURCE (dossier actuel sur VOTRE machine)
# Deuxième "." = DESTINATION (dossier actuel dans CONTENEUR = /app)
# 
# Qu'est-ce qui est copié?
# TOUT ce qui est dans le dossier:
# - app.py
# - models.py
# - templates/
# - static/
# - requirements.txt (déjà copié avant, mais pas grave)
# - etc.
# 
# Qu'est-ce qui N'est PAS copié?
# Les fichiers listés dans .dockerignore (voir section dédiée)
# 
# Pourquoi COPY . . est à la FIN?
# - Le code source change SOUVENT
# - En le mettant à la fin, on invalide moins le cache
# - Les étapes précédentes (pip install) restent en cache
# 
# Alternatives:
# COPY . /app          <- Même chose (explicite)
# COPY app.py .        <- Copie seulement app.py
# COPY src/ /app/src/  <- Copie seulement le dossier src
# 
# Pour projets complexes:
# COPY app/ /app/app/
# COPY tests/ /app/tests/
# COPY config/ /app/config/
# = Plus de contrôle sur ce qui est copié

# ========================================
# EXPOSE 8000
# ========================================
# 
# Documente que le conteneur écoute sur le port 8000
# 
# IMPORTANT: C'est juste de la DOCUMENTATION!
# EXPOSE ne fait RIEN de concret:
# - Ne publie PAS le port
# - N'ouvre PAS le port
# - Ne configure RIEN
# 
# C'est comme un commentaire qui dit:
# "Hey, cette application utilise le port 8000"
# 
# Pourquoi l'utiliser alors?
# 1. Documentation pour les développeurs
#    - En lisant le Dockerfile, on sait quel port utiliser
# 
# 2. Détection automatique par certains outils
#    - docker-compose peut détecter le port
#    - Certains orchestrateurs (Kubernetes) l'utilisent
# 
# 3. Utilisé par docker run -P
#    docker run -P myapp
#    <- Publie automatiquement les ports EXPOSE
# 
# Pour VRAIMENT publier le port, utilisez -p:
# docker run -p 8000:8000 myapp
#   ^         ^
#   |         └─ Port DANS le conteneur
#   └─ Port sur VOTRE machine
# 
# Exemples:
# EXPOSE 8000              <- FastAPI, Django
# EXPOSE 5000              <- Flask
# EXPOSE 80                <- Nginx
# EXPOSE 5432              <- PostgreSQL
# EXPOSE 6379              <- Redis
# 
# Plusieurs ports:
# EXPOSE 8000 8001 8002
# EXPOSE 80 443            <- HTTP et HTTPS

# ========================================
# CMD ["python", "app.py"]
# ========================================
# 
# Commande exécutée au DÉMARRAGE du conteneur
# 
# CMD vs RUN (encore):
# - RUN: Pendant le BUILD, résultat sauvegardé dans l'image
# - CMD: Au DÉMARRAGE du conteneur, exécuté à chaque fois
# 
# Formats de CMD:
# 
# 1. Format EXEC (recommandé):
#    CMD ["executable", "param1", "param2"]
#    CMD ["python", "app.py"]
#    CMD ["gunicorn", "-b", "0.0.0.0:8000", "app:app"]
#    
#    Avantages:
#    - Pas de shell intermédiaire
#    - Signaux (SIGTERM, SIGINT) reçus directement
#    - Arrêt propre du conteneur
#    - Plus rapide
# 
# 2. Format SHELL:
#    CMD python app.py
#    CMD gunicorn -b 0.0.0.0:8000 app:app
#    
#    Problèmes:
#    - Lance /bin/sh -c "votre commande"
#    - Shell intermédiaire = PID 1 n'est pas votre app
#    - Signaux mal transmis
#    - Arrêt sale du conteneur
# 
# Recommandation: TOUJOURS utiliser format EXEC!
# 
# CMD vs ENTRYPOINT:
# - CMD: Commande par défaut, peut être overridée
#   docker run myapp python other.py  <- Override CMD
# 
# - ENTRYPOINT: Commande fixe, les arguments s'ajoutent
#   ENTRYPOINT ["python"]
#   CMD ["app.py"]
#   docker run myapp other.py  <- Exécute "python other.py"
# 
# Il ne peut y avoir qu'UN SEUL CMD dans un Dockerfile!
# Si plusieurs CMD, seul le dernier compte.
# 
# Exemples courants:
# CMD ["python", "app.py"]                    <- App simple
# CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]  <- Django dev
# CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000"]  <- Flask production
# CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]   <- FastAPI
# CMD ["celery", "-A", "myapp", "worker"]     <- Celery worker
# CMD ["python", "-m", "pytest"]              <- Tests

# === BUILD ET RUN ===

# Structure de projet typique:
# myproject/
# ├── Dockerfile              <- Le fichier qu'on vient de créer
# ├── requirements.txt        <- Liste des dépendances Python
# ├── app.py                  <- Code de votre application
# ├── models.py               <- (optionnel) Vos modèles
# └── templates/              <- (optionnel) Vos templates HTML

# ========================================
# ÉTAPE 1: BUILD (Construire l'image)
# ========================================

# Commande de base:
docker build -t myapp:latest .

# Décomposition:
# docker build     <- Commande pour construire une image
# -t myapp:latest  <- "Tag" (nom) de l'image
# .                <- Contexte de build (dossier actuel)

# Qu'est-ce qu'un TAG?
# Format: nom:version
# - myapp = nom de l'image
# - latest = version (tag)
# - "latest" est une CONVENTION pour "dernière version"
# 
# Exemples de tags:
# myapp:latest     <- Version de développement
# myapp:1.0.0      <- Version 1.0.0
# myapp:v2-prod    <- Version 2 production
# myapp:dev        <- Version développement
# myuser/myapp:latest  <- Avec nom d'utilisateur (pour Docker Hub)

# Qu'est-ce que le CONTEXTE (.)?
# - Le "." signifie "dossier actuel"
# - Docker envoie TOUT ce dossier au daemon Docker
# - Le Dockerfile peut accéder à ces fichiers avec COPY
# 
# Exemple:
# Si vous êtes dans /home/user/myproject
# docker build -t myapp .
# -> Docker envoie tout /home/user/myproject
# -> COPY . . va copier tout ce contenu
# 
# Attention:
# Si le contexte est gros (node_modules, .git, etc.)
# -> Build sera LENT (envoi de Gigaoctets)
# -> Solution: créer un .dockerignore (voir section dédiée)

# Que se passe-t-il pendant le build?
# Docker exécute CHAQUE instruction du Dockerfile:
# 
# Output typique:
# [+] Building 45.2s (10/10) FINISHED
# => [1/6] FROM python:3.11-slim           5.2s
# => [2/6] WORKDIR /app                    0.1s
# => [3/6] COPY requirements.txt .         0.1s
# => [4/6] RUN pip install ...            35.4s  <- Le plus long!
# => [5/6] COPY . .                        0.3s
# => [6/6] CMD ["python", "app.py"]        0.0s
# => exporting to image                    4.1s
# 
# Chaque => est une "layer" (couche)
# Les layers sont CACHÉES si elles n'ont pas changé

# Rebuild avec cache:
# Si vous relancez docker build sans rien changer:
# [+] Building 2.1s (10/10) FINISHED
# => [1/6] FROM python:3.11-slim           CACHED
# => [2/6] WORKDIR /app                    CACHED
# => [3/6] COPY requirements.txt .         CACHED
# => [4/6] RUN pip install ...            CACHED <- Instantané!
# => [5/6] COPY . .                        CACHED
# => [6/6] CMD ["python", "app.py"]        CACHED
# 
# Build en 2 secondes au lieu de 45 secondes!

# Forcer rebuild sans cache:
docker build --no-cache -t myapp:latest .
# Utile si:
# - Vous avez des problèmes bizarres
# - Vous voulez être sûr que tout est frais

# Build avec nom différent:
docker build -t myapp:dev .
docker build -t myapp:1.0.0 .
docker build -t username/myapp:latest .

# Voir toutes vos images:
docker images
# Output:
# REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
# myapp        latest    abc123def456   5 minutes ago   250MB
# python       3.11-slim xyz789abc123   2 weeks ago     150MB

# ========================================
# ÉTAPE 2: RUN (Lancer un conteneur)
# ========================================

# Commande de base:
docker run -p 8000:8000 myapp:latest

# Décomposition:
# docker run       <- Commande pour lancer un conteneur
# -p 8000:8000     <- Mapping de port
# myapp:latest     <- Image à utiliser

# Qu'est-ce que -p (port mapping)?
# Format: -p HOST_PORT:CONTAINER_PORT
# 
# -p 8000:8000
#    ^     ^
#    |     └─ Port DANS le conteneur (votre app écoute sur 8000)
#    └─ Port sur VOTRE machine (vous accédez via localhost:8000)
# 
# Le conteneur est ISOLÉ du réseau de votre machine
# Sans -p, vous ne pouvez PAS accéder à l'application!
# 
# Exemples:
# -p 8000:8000     <- Port identique (simple)
# -p 80:8000       <- Port 80 externe, 8000 interne (comme nginx)
# -p 3000:8000     <- Port 3000 externe, 8000 interne
# -p 127.0.0.1:8000:8000  <- Seulement localhost (plus sécurisé)
# 
# Plusieurs ports:
# docker run -p 8000:8000 -p 8001:8001 myapp

# Lancer en ARRIÈRE-PLAN (detached):
docker run -d -p 8000:8000 --name myapp myapp:latest

# Options:
# -d               <- Détaché (background), terminal libre
# --name myapp     <- Nom du conteneur (plus facile à gérer)

# Sans -d:
# - Le conteneur tourne au premier plan
# - Les logs s'affichent dans le terminal
# - CTRL+C arrête le conteneur
# - Terminal bloqué
# 
# Avec -d:
# - Le conteneur tourne en arrière-plan
# - Terminal libre immédiatement
# - Conteneur continue même si vous fermez le terminal

# Accéder à l'application:
# Si tout va bien, votre app est accessible sur:
# http://localhost:8000

# Vérifier que le conteneur tourne:
docker ps
# Output:
# CONTAINER ID   IMAGE          COMMAND           STATUS         PORTS                    NAMES
# abc123def456   myapp:latest   "python app.py"   Up 2 minutes   0.0.0.0:8000->8000/tcp   myapp

# Voir les logs:
docker logs myapp
# Affiche tous les logs depuis le démarrage

docker logs -f myapp
# -f = follow (suit les logs en temps réel, comme tail -f)

docker logs --tail 50 myapp
# Affiche seulement les 50 dernières lignes

# Arrêter le conteneur:
docker stop myapp
# Arrêt propre (envoie SIGTERM puis SIGKILL après 10s)

docker stop -t 30 myapp
# -t 30 = attend 30 secondes avant SIGKILL

# Redémarrer:
docker start myapp
# Redémarre un conteneur arrêté

docker restart myapp
# Équivalent à stop + start

# Supprimer le conteneur:
docker rm myapp
# Supprime le conteneur (doit être arrêté)

docker rm -f myapp
# Force la suppression (même si en cours d'exécution)

# Entrer dans le conteneur (shell interactif):
docker exec -it myapp bash
# -i = interactif
# -t = TTY (pseudo-terminal)
# bash = shell à lancer

# Une fois dedans:
root@abc123:/app# ls
app.py  requirements.txt  ...
root@abc123:/app# python
>>> import django
>>> django.__version__
'4.2.8'
root@abc123:/app# exit

# Exécuter une commande sans entrer:
docker exec myapp python -c "print('Hello from container')"
docker exec myapp ls -la
docker exec myapp pip list


[OK] DOCKERFILE PYTHON OPTIMISÉ

# === VERSION OPTIMISÉE (PRODUCTION) ===

# Cette version est plus complexe mais BEAUCOUP plus efficace
# Elle utilise le "multi-stage build" - une technique avancée

# syntax=docker/dockerfile:1.4
# ^ Active BuildKit (moteur moderne de Docker)
# Optionnel mais donne accès à features avancées

# ========================================
# STAGE 1: BASE (Configuration commune)
# ========================================

FROM python:3.11-slim AS base
# ^ "AS base" donne un NOM à cette étape
# On pourra la référencer plus tard

# === VARIABLES D'ENVIRONNEMENT PYTHON ===

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# Explication de CHAQUE variable:

# ========================================
# PYTHONUNBUFFERED=1
# ========================================
# 
# PAR DÉFAUT, Python bufferise (met en mémoire) stdout/stderr
# Problème en Docker:
# - Vous faites print("Hello")
# - Le texte reste en mémoire
# - Il s'affiche 10 secondes plus tard (ou jamais si crash)
# - docker logs montre les logs avec RETARD
# 
# Avec PYTHONUNBUFFERED=1:
# - print("Hello") s'affiche IMMÉDIATEMENT
# - docker logs montre tout en temps réel
# - ESSENTIEL pour débugger
# 
# Exemple concret:
# Sans: print("Starting...") puis crash -> vous ne voyez RIEN
# Avec: print("Starting...") puis crash -> vous voyez où ça a planté
# 
# Cette variable est CRITIQUE en Docker!

# ========================================
# PYTHONDONTWRITEBYTECODE=1
# ========================================
# 
# PAR DÉFAUT, Python compile vos .py en .pyc (bytecode)
# Il crée des dossiers __pycache__/ partout:
# 
# myproject/
# ├── app.py
# ├── __pycache__/
# │   └── app.cpython-311.pyc    <- Fichier compilé
# ├── models.py
# └── __pycache__/
#     └── models.cpython-311.pyc
# 
# Pourquoi c'est un problème en Docker?
# 1. Ça prend de l'espace (peut ajouter 50-100 MB)
# 2. Les .pyc sont recréés à chaque démarrage de toute façon
# 3. Dans un conteneur, la compilation initiale est négligeable
# 4. Ça pollue l'image avec des fichiers inutiles
# 
# Avec PYTHONDONTWRITEBYTECODE=1:
# - Pas de __pycache__/
# - Image plus petite
# - Plus propre
# 
# Note: Ça peut ralentir le démarrage de 0.1 seconde
# Mais en production avec gunicorn, c'est invisible

# ========================================
# PIP_NO_CACHE_DIR=1
# ========================================
# 
# PAR DÉFAUT, pip garde un cache des packages téléchargés:
# ~/.cache/pip/
# ├── wheels/          <- Packages compilés
# └── http/            <- Packages téléchargés
# 
# Sur votre machine: UTILE (reinstall rapide)
# Dans Docker: INUTILE (l'image est figée)
# 
# Le cache peut prendre 200-500 MB!
# 
# Exemple:
# Sans PIP_NO_CACHE_DIR:
# - pip install numpy
# - numpy téléchargé + mis en cache
# - Image finale: numpy installé (100 MB) + cache (150 MB) = 250 MB
# 
# Avec PIP_NO_CACHE_DIR:
# - pip install numpy
# - numpy téléchargé mais PAS mis en cache
# - Image finale: numpy installé (100 MB) = 100 MB
# 
# Économie: 150 MB dans cet exemple
# Sur un projet réel: 300-500 MB économisés!

# ========================================
# PIP_DISABLE_PIP_VERSION_CHECK=1
# ========================================
# 
# PAR DÉFAUT, pip vérifie s'il y a une nouvelle version:
# "You should consider upgrading pip..."
# 
# Problèmes:
# 1. Ralentit chaque commande pip (1-2 secondes)
# 2. Logs pollués avec des avertissements inutiles
# 3. En Docker, la version de pip est fixée dans l'image
# 
# Avec PIP_DISABLE_PIP_VERSION_CHECK=1:
# - Pas de vérification
# - pip install plus rapide
# - Logs plus propres
# 
# Build sans: 45 secondes
# Build avec: 43 secondes
# Petit gain mais agréable!

WORKDIR /app

# ============================================
# STAGE 2: BUILDER (Installation dépendances)
# ============================================

FROM base AS builder
# On REPART de l'étape "base"
# On hérite de toutes les config (ENV, WORKDIR)

COPY requirements.txt .

# Installation avec --user
RUN pip install --user --no-cache-dir -r requirements.txt

Parfait ! Je vais te donner un **exemple complet mais simple** de Dockerfile multi-stage avec Flask, en utilisant exactement le concept `base -> builder -> final`.

---

### 1⃣ Structure du projet

```
flask-app/
│
├─ app.py
├─ requirements.txt
└─ Dockerfile
```

**app.py** :

```python
from flask import Flask

app = Flask(__name__)

@app.route('/')
def home():
    return "Hello Docker Flask!"

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)
```

**requirements.txt** :

```
Flask==2.3.4
```

---

### 2⃣ Dockerfile multi-stage

```dockerfile

# Étape 1: base
# =========================

FROM python:3.12-slim AS base

# Définir le répertoire de travail
WORKDIR /app

# Mettre à jour pip
RUN pip install --upgrade pip

# Étape 2: builder
# =========================

FROM base AS builder

COPY requirements.txt .

# Installer les dépendances pour l'utilisateur
RUN pip install --user --no-cache-dir -r requirements.txt

# Étape 3: final
# =========================

FROM base AS final

# Copier le code de l'application
COPY app.py .

# Copier les packages installés depuis builder
COPY --from=builder /root/.local /root/.local

# Ajouter le chemin des binaires pip install --user
ENV PATH=/root/.local/bin:$PATH

# Exposer le port de Flask
EXPOSE 5000

# Lancer Flask
CMD ["python", "app.py"]
```

---

### 3⃣ Explications étape par étape

1. **base** : image de base Python, configuration minimale, WORKDIR défini.
2. **builder** : copie `requirements.txt` et installe Flask **pour l’utilisateur**, sans toucher au système.
3. **final** : image finale, copie l’application et les packages installés dans l’étape builder.

   * On utilise `COPY --from=builder` pour ne pas réinstaller les dépendances.
   * On met à jour le `PATH` pour que les binaires Python soient accessibles.

---

### 4⃣ Construire et lancer

```bash
# Construire l'image
docker build -t flask-demo .

# Lancer le container
docker run -p 5000:5000 flask-demo
```

Puis ouvre [http://localhost:5000](http://localhost:5000) -> tu verras **Hello Docker Flask!**


# ========================================
# Pourquoi --user ?
# ========================================
# 
# --user installe dans /root/.local/ au lieu de /usr/local/
# 
# Installation normale (sans --user):
# /usr/local/
# ├── lib/
# │   └── python3.11/
# │       └── site-packages/
# │           ├── django/
# │           ├── requests/
# │           └── ... (vos packages)
# ├── bin/
# │   ├── django-admin
# │   └── ...
# └── ... (beaucoup d'autres choses)
# 
# Installation avec --user:
# /root/.local/
# ├── lib/
# │   └── python3.11/
# │       └── site-packages/
# │           ├── django/
# │           └── requests/
# └── bin/
#     └── django-admin
# 
# Avantage: On peut copier JUSTE /root/.local
# Sans trainer pip, setuptools, wheel, etc.

# ========================================
# STAGE 3: PRODUCTION (Image finale)
# ========================================

FROM base
# On REPART de "base" (pas de builder!)
# Cette image ne contient PAS pip, setuptools, etc.

# Copier SEULEMENT les packages installés
COPY --from=builder /root/.local /root/.local

# ========================================
# COPY --from=builder
# ========================================
# 
# La MAGIE du multi-stage build!
# 
# --from=builder : copie depuis l'étape "builder"
# /root/.local : dossier source dans builder
# /root/.local : dossier destination dans cette étape
# 
# Qu'est-ce qui est copié?
# SEULEMENT vos packages Python installés!
# 
# Qu'est-ce qui N'est PAS copié?
# - pip (économise 20 MB)
# - setuptools (économise 10 MB)
# - wheel (économise 5 MB)
# - Cache de compilation (économise 50+ MB)
# - Headers (.h files) (économise 30 MB)
# 
# Résultat:
# Image builder: 500 MB
# Image finale: 250 MB
# Économie: 250 MB (50%!)

# Ajouter au PATH pour trouver les executables
ENV PATH=/root/.local/bin:$PATH

# ========================================
# Pourquoi modifier PATH?
# ========================================
# 
# Les executables Python sont dans /root/.local/bin/
# Exemples: django-admin, gunicorn, celery, pytest
# 
# Sans modifier PATH:
# django-admin  <- Error: command not found
# /root/.local/bin/django-admin  <- Fonctionne mais pénible
# 
# Avec PATH=/root/.local/bin:$PATH:
# django-admin  <- Fonctionne!
# 
# PATH est une variable d'environnement qui liste
# les dossiers où chercher les executables:
# PATH=/usr/local/bin:/usr/bin:/bin
# 
# On ajoute /root/.local/bin AU DÉBUT:
# PATH=/root/.local/bin:/usr/local/bin:/usr/bin:/bin
# 
# Maintenant, tous les executables Python sont trouvés!

# Copier le code de l'application
COPY . .

# ========================================
# SÉCURITÉ: Utilisateur non-root
# ========================================

RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app

# ========================================
# Pourquoi créer un utilisateur?
# ========================================
# 
# PAR DÉFAUT, les conteneurs tournent en ROOT!
# C'est un PROBLÈME DE SÉCURITÉ majeur.
# 
# Scénario d'attaque:
# 1. Votre application a une faille (injection, RCE, etc.)
# 2. L'attaquant exécute du code dans le conteneur
# 3. Il est ROOT dans le conteneur
# 4. Il peut potentiellement accéder à l'hôte
# 5. Il a un accès complet au système
# 
# Avec un utilisateur non-root:
# 1. L'attaquant exécute du code
# 2. Il est "appuser" (pas de privilèges)
# 3. Il ne peut pas installer de packages
# 4. Il ne peut pas modifier les fichiers système
# 5. Les dégâts sont LIMITÉS
# 
# C'est une BEST PRACTICE de sécurité essentielle!

# ========================================
# Explication de la commande
# ========================================
# 
# useradd -m -u 1000 appuser
# 
# useradd : crée un nouvel utilisateur Linux
# -m : crée le home directory (/home/appuser)
# -u 1000 : UID (User ID) = 1000
# appuser : nom de l'utilisateur
# 
# Pourquoi UID 1000?
# - Sur la plupart des systèmes Linux, le premier utilisateur a UID 1000
# - En utilisant 1000, les permissions sont cohérentes
# - Si vous montez un volume, les fichiers créés auront UID 1000
# - Votre utilisateur local peut probablement les lire/écrire
# 
# chown -R appuser:appuser /app
# 
# chown : change le propriétaire (owner) des fichiers
# -R : récursif (tous les sous-dossiers)
# appuser:appuser : utilisateur:groupe
# /app : dossier à changer
# 
# Pourquoi chown?
# - COPY a été exécuté en tant que root
# - Les fichiers appartiennent à root
# - Si on passe à appuser, il ne pourra pas les lire!
# - chown donne la propriété à appuser

# Passer à l'utilisateur non-root
USER appuser

# ========================================
# USER appuser
# ========================================
# 
# Toutes les commandes SUIVANTES s'exécutent en tant que appuser
# Cela inclut CMD!
# 
# Avant USER appuser:
# root@abc123:/app# whoami
# root
# 
# Après USER appuser:
# appuser@abc123:/app$ whoami
# appuser
# 
# Vérification dans un conteneur en cours:
# docker exec myapp whoami
# -> appuser
# 
# Si vous avez besoin de root temporairement:
# docker exec -u root myapp bash

EXPOSE 8000

CMD ["python", "app.py"]

# ========================================
# COMPARAISON: Simple vs Optimisé
# ========================================
# 
# Dockerfile simple:
# - Image: 500 MB
# - Build time: 60 secondes
# - Sécurité: conteneur root (dangereux)
# - Logs: avec délai
# - Simplicité: +++
# 
# Dockerfile optimisé:
# - Image: 250 MB (50% plus petit!)
# - Build time: 45 secondes (15 sec plus rapide)
# - Sécurité: utilisateur non-root (sécurisé)
# - Logs: temps réel
# - Complexité: ++
# 
# Recommandation:
# - Prototypes, apprentissage: version simple
# - Production, projets sérieux: version optimisée


[OK] .DOCKERIGNORE (ESSENTIEL!)

# === QU'EST-CE QUE .dockerignore? ===

# C'est comme .gitignore mais pour Docker
# Liste les fichiers à NE PAS copier dans l'image

# ========================================
# Pourquoi c'est IMPORTANT?
# ========================================
# 
# Sans .dockerignore:
# docker build -t myapp .
# 
# Docker envoie TOUT le dossier au daemon:
# - Votre code (10 MB)
# - node_modules/ (500 MB)
# - venv/ (300 MB)
# - .git/ (200 MB)
# - __pycache__/ (50 MB)
# - logs/ (100 MB)
# Total: 1.16 GB envoyé!
# 
# Résultat:
# - Build prend 5 minutes (envoi des données)
# - Image finale est énorme
# - Risque de secrets exposés (.env, .git)
# 
# Avec .dockerignore:
# Docker envoie SEULEMENT:
# - Votre code (10 MB)
# Total: 10 MB envoyé!
# 
# Résultat:
# - Build prend 30 secondes
# - Image finale est petite
# - Secrets protégés

# === .dockerignore COMPLET ===

# Créer un fichier ".dockerignore" à côté de Dockerfile

# .dockerignore

# ========================================
# Python
# ========================================

__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so
*.egg
*.egg-info/
dist/
build/

# Explication:
# __pycache__/ : bytecode compilé (inutile, recréé automatiquement)
# *.pyc, *.pyo : fichiers compilés (inutiles)
# *.egg-info/ : metadata de packages (inutile dans conteneur)
# dist/, build/ : dossiers de build (ne doivent PAS être dans conteneur)

# ========================================
# Environnements virtuels
# ========================================

venv/
env/
.venv/
ENV/
env.bak/
venv.bak/

# Explication:
# Ces dossiers contiennent un environnement virtuel Python
# Ils peuvent faire 200-500 MB!
# COMPLÈTEMENT inutiles dans Docker car:
# - Docker ISOLE déjà l'environnement
# - Les dépendances sont installées via pip dans l'image

# Erreur courante de débutant:
# Créer un venv/ puis le copier dans Docker
# = Ajoute 300 MB inutiles + peut causer des bugs!

# ========================================
# IDE et éditeurs
# ========================================

.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store

# Explication:
# Fichiers de configuration de votre éditeur
# Totalement inutiles dans le conteneur
# Peuvent contenir des chemins locaux à votre machine

# ========================================
# Git
# ========================================

.git/
.gitignore
.gitattributes

# Explication:
# .git/ peut faire 100-500 MB (historique complet)
# Contient TOUT l'historique du projet
# Risque de sécurité: peut contenir des secrets dans l'historique
# 
# IMPORTANT: Vous ne versionnez PAS avec git DANS le conteneur
# Vous versionnez le Dockerfile et le code source
# L'image Docker elle-même n'a pas besoin de .git/

# ========================================
# Tests
# ========================================

.pytest_cache/
.coverage
htmlcov/
.tox/
.hypothesis/
tests/
*_test.py
test_*.py

# Explication:
# Cache de pytest (recréé à chaque run)
# Rapports de coverage (inutiles en production)
# Tests eux-mêmes (ne vont PAS en production!)
# 
# Note: Pour une image de test, NE PAS ignorer tests/
# Créer un .dockerignore.test séparé

# ========================================
# Documentation
# ========================================

docs/
*.md
LICENSE
CHANGELOG

# Explication:
# Documentation pour humains, pas pour conteneur
# README.md, CONTRIBUTING.md, etc.
# Économise quelques MB

# ========================================
# Environnement et secrets
# ========================================

.env
.env.*
.env.local
.env.production
*.pem
*.key
secrets/

# Explication:
# CRITIQUE POUR LA SÉCURITÉ!
# Ces fichiers contiennent:
# - Mots de passe de base de données
# - Clés API
# - Tokens secrets
# - Certificats privés
# 
# ILS NE DOIVENT JAMAIS être dans l'image!
# 
# Pourquoi?
# 1. Si vous poussez l'image sur Docker Hub -> secrets publics!
# 2. Impossible de changer les secrets sans rebuild
# 3. Mêmes secrets en dev/prod (mauvaise pratique)
# 
# Comment passer des secrets?
# - Variables d'environnement au docker run
# - Docker secrets (en Swarm)
# - Fichiers montés via volumes
# - Services de secrets (AWS Secrets Manager, etc.)

# ========================================
# Logs et données temporaires
# ========================================

*.log
logs/
tmp/
temp/
*.tmp
.cache/

# Explication:
# Logs de développement (ne vont pas en production)
# Peuvent être gros (100+ MB)
# En production, logs vont vers stdout (docker logs)

# ========================================
# CI/CD
# ========================================

.github/
.gitlab-ci.yml
.travis.yml
Jenkinsfile
azure-pipelines.yml

# Explication:
# Configuration CI/CD pour GitHub Actions, GitLab CI, etc.
# Inutile dans le conteneur
# Le conteneur EST le produit du CI/CD

# ========================================
# Docker lui-même
# ========================================

Dockerfile
Dockerfile.*
docker-compose.yml
docker-compose.*.yml
.dockerignore

# Explication:
# Ces fichiers servent à CONSTRUIRE l'image
# Pas besoin d'être DANS l'image
# 
# Exception: Certains aiment garder Dockerfile dans l'image
# pour traçabilité (savoir comment l'image a été construite)
# Dans ce cas, commentez ces lignes

# ========================================
# Node.js (si projet mixte Python/JS)
# ========================================

node_modules/
npm-debug.log
yarn-error.log
package-lock.json
yarn.lock

# Explication:
# Si vous avez du frontend (React, Vue, etc.)
# node_modules/ peut faire 500 MB - 2 GB!
# JAMAIS copier node_modules/ dans Docker

# === EXEMPLE .dockerignore COMPLET ===

# .dockerignore (version complète)

# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg

# Virtual environments
venv/
env/
.venv/
ENV/
env.bak/
venv.bak/

# IDE
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store
Thumbs.db

# Git
.git/
.gitignore
.gitattributes
.github/

# Tests
.pytest_cache/
.coverage
htmlcov/
.tox/
tests/
test_*.py
*_test.py

# Documentation
docs/
*.md
LICENSE

# Secrets & Environment
.env
.env.*
*.pem
*.key
secrets/

# Logs
*.log
logs/

# CI/CD
.gitlab-ci.yml
Jenkinsfile

# Docker
Dockerfile
docker-compose*.yml
.dockerignore

# Node (si applicable)
node_modules/

# === VÉRIFIER CE QUI EST COPIÉ ===

# Avant de build, vérifier ce qui sera copié:
docker build --no-cache -t myapp . 2>&1 | grep "COPY"

# Ou utiliser un truc:
# Créer un Dockerfile temporaire:
FROM alpine
WORKDIR /app
COPY . .
RUN find . -type f | sort

# Build et voir:
docker build -f Dockerfile.check -t check .
docker run --rm check

# Vous verrez TOUS les fichiers copiés
# Si vous voyez venv/, .git/, .env -> problème!


[OK] REQUIREMENTS.TXT POUR DOCKER

# === POURQUOI REQUIREMENTS.TXT? ===

# requirements.txt liste TOUTES les dépendances Python
# C'est le "package.json" du monde Python

# Exemple requirements.txt:
django==4.2.8
requests==2.31.0
psycopg2-binary==2.9.9

# ========================================
# Avantages


[OK] DOCKERFILE PYTHON OPTIMISÉ

# === VERSION OPTIMISÉE (PRODUCTION) ===

# syntax=docker/dockerfile:1.4

# === Stage 1: Base ===
FROM python:3.11-slim AS base

# Variables d'environnement Python
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR /app

# === Stage 2: Builder (installation dépendances) ===
FROM base AS builder

COPY requirements.txt .

# Installer les dépendances dans /root/.local
# --user: installe dans le home de l'utilisateur
RUN pip install --user --no-cache-dir -r requirements.txt

# === Stage 3: Production ===
FROM base

# Copier seulement les dépendances installées (pas pip, pas setuptools, etc.)
COPY --from=builder /root/.local /root/.local

# Ajouter /root/.local/bin au PATH pour trouver les executables
ENV PATH=/root/.local/bin:$PATH

# Copier le code de l'application
COPY . .

# Créer utilisateur non-root pour la sécurité
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app

# Passer à l'utilisateur non-root
USER appuser

EXPOSE 8000

CMD ["python", "app.py"]

# === EXPLICATION DES VARIABLES D'ENVIRONNEMENT ===

# PYTHONUNBUFFERED=1
# - Force Python à ne pas bufferiser stdout/stderr
# - Vous voyez les logs immédiatement dans docker logs
# - CRITIQUE pour Docker, sinon les logs sont retardés

# PYTHONDONTWRITEBYTECODE=1
# - Empêche Python de créer des fichiers .pyc et __pycache__
# - Économise de l'espace dans l'image
# - Les .pyc sont inutiles dans un conteneur (recréés à chaque démarrage)

# PIP_NO_CACHE_DIR=1
# - pip n'enregistre pas son cache
# - Économise de l'espace dans l'image

# PIP_DISABLE_PIP_VERSION_CHECK=1
# - Désactive la vérification de nouvelle version de pip
# - Accélère pip install
# - Réduit le bruit dans les logs

# === EXPLICATION MULTI-STAGE BUILD ===

# Pourquoi 3 stages?

# Stage "base":
# - Configuration commune à builder et production
# - Variables d'environnement
# - Évite duplication de code

# Stage "builder":
# - Installe toutes les dépendances
# - Peut contenir outils de compilation (gcc, etc.)
# - Cette layer est JETÉE dans l'image finale

# Stage "production":
# - Copie SEULEMENT les dépendances installées
# - Pas pip, pas setuptools, pas wheel
# - Image finale beaucoup plus petite!

# Exemple tailles:
# Sans multi-stage: 500 MB
# Avec multi-stage: 250 MB

# === EXPLICATION SÉCURITÉ ===

# useradd -m -u 1000 appuser
# - useradd: créer utilisateur
# - -m: créer home directory
# - -u 1000: UID 1000 (convention)
# - appuser: nom de l'utilisateur

# chown -R appuser:appuser /app
# - Change ownership de /app à appuser
# - -R: récursif (tous les sous-dossiers)
# - Nécessaire car COPY a été fait en root

# USER appuser
# - Toutes les commandes suivantes s'exécutent en tant que appuser
# - Le conteneur ne tourne PAS en root (sécurité++)

# === .dockerignore ===

# Créer un fichier .dockerignore à côté du Dockerfile

# .dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so

# Virtual environments
venv/
env/
.venv/

# IDE
.vscode/
.idea/
*.swp

# Git
.git/
.gitignore

# Tests
.pytest_cache/
.coverage
htmlcov/

# Documentation
docs/
*.md

# Environnement
.env
.env.*

# Build
build/
dist/
*.egg-info/

# Docker
Dockerfile
docker-compose.yml

# === BUILD ET OPTIMISATION ===

# Build avec BuildKit (plus rapide)
DOCKER_BUILDKIT=1 docker build -t myapp:latest .

# Build avec cache depuis registry
docker build \
  --cache-from myapp:latest \
  -t myapp:latest \
  .

# Voir taille de chaque layer
docker history myapp:latest

# Analyser l'image
docker run --rm -it \
  -v /var/run/docker.sock:/var/run/docker.sock \
  wagoodman/dive:latest myapp:latest


[OK] REQUIREMENTS.TXT POUR DOCKER

# === STRUCTURE RECOMMANDÉE ===

# Votre projet devrait avoir plusieurs fichiers requirements

# requirements/
# ├── base.txt          # Dépendances de base
# ├── dev.txt           # Dépendances développement
# ├── prod.txt          # Dépendances production
# └── test.txt          # Dépendances tests

# === base.txt (commun à tous) ===
# requirements/base.txt

# Web framework
django==4.2.8
djangorestframework==3.14.0

# Base de données
psycopg2-binary==2.9.9

# Outils
python-dotenv==1.0.0
requests==2.31.0

# === dev.txt ===
# requirements/dev.txt

-r base.txt

# Debug
ipython==8.18.1
django-debug-toolbar==4.2.0

# Code quality
black==23.12.1
flake8==6.1.0
mypy==1.7.1

# === prod.txt ===
# requirements/prod.txt

-r base.txt

# Production server
gunicorn==21.2.0

# Monitoring
sentry-sdk==1.39.1

# === test.txt ===
# requirements/test.txt

-r base.txt

pytest==7.4.3
pytest-django==4.7.0
pytest-cov==4.1.0
faker==21.0.0

# === PINNING DES VERSIONS ===

# [X] Mauvais (version non spécifiée)
django
requests

# [ATTENTION]  Acceptable (développement)
django>=4.2,<5.0
requests>=2.31

# [OK] Bon (production)
django==4.2.8
requests==2.31.0

# === GÉNÉRER REQUIREMENTS.TXT ===

# Depuis un venv actif
pip freeze > requirements.txt

# Nettoyer (seulement packages top-level)
pip install pipreqs
pipreqs . --force

# === DOCKERFILE AVEC REQUIREMENTS MULTIPLES ===

# Dockerfile pour développement
FROM python:3.11-slim

WORKDIR /app

COPY requirements/base.txt requirements/dev.txt ./
RUN pip install --no-cache-dir -r dev.txt

COPY . .

CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

# Dockerfile pour production
FROM python:3.11-slim

WORKDIR /app

COPY requirements/base.txt requirements/prod.txt ./
RUN pip install --no-cache-dir -r prod.txt

COPY . .

RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]


[OK] FLASK AVEC DOCKER

# === STRUCTURE PROJET FLASK ===

# myflaskapp/
# ├── app.py
# ├── requirements.txt
# ├── Dockerfile
# ├── docker-compose.yml
# └── .dockerignore

# === app.py (exemple simple) ===

# app.py
from flask import Flask
import os

app = Flask(__name__)

@app.route('/')
def hello():
    return "Hello from Flask in Docker!"

@app.route('/health')
def health():
    return {"status": "healthy"}, 200

if __name__ == '__main__':
    # 0.0.0.0 permet connexions depuis l'extérieur du conteneur
    app.run(host='0.0.0.0', port=5000, debug=True)

# === requirements.txt ===

# requirements.txt
Flask==3.0.0
gunicorn==21.2.0        # Serveur production
redis==5.0.1            # Si besoin de Redis
python-dotenv==1.0.0

# === Dockerfile DÉVELOPPEMENT ===

# Dockerfile.dev
FROM python:3.11-slim

WORKDIR /app

# Installation dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Code
COPY . .

# Port Flask par défaut
EXPOSE 5000

# Mode développement avec rechargement automatique
CMD ["flask", "run", "--host=0.0.0.0", "--reload"]

# Ou avec python directement
# CMD ["python", "app.py"]

# === Dockerfile PRODUCTION ===

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

# Créer user non-root
RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 5000

# Gunicorn pour production (pas flask run!)
CMD ["gunicorn", "-b", "0.0.0.0:5000", "-w", "4", "app:app"]

# Options gunicorn:
# -b 0.0.0.0:5000 : bind sur toutes interfaces, port 5000
# -w 4 : 4 workers (processus)
# app:app : module:application

# === docker-compose.yml ===

version: '3.8'

services:
  flask:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "5000:5000"
    volumes:
      - .:/app                    # Hot reload
    environment:
      - FLASK_ENV=development
      - FLASK_DEBUG=1
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - redis

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

# === EXPLICATION DOCKER-COMPOSE ===

# version: '3.8'
# - Version du format docker-compose
# - 3.8 est une version stable et récente

# services:
# - Liste des conteneurs à créer

# flask: (nom du service)
#   build:
#     context: .
#       - Contexte de build = dossier actuel
#     dockerfile: Dockerfile.dev
#       - Utilise Dockerfile.dev au lieu de Dockerfile
#
#   ports:
#     - "5000:5000"
#       - Expose port 5000 du conteneur sur port 5000 de l'host
#       - Format: "HOST_PORT:CONTAINER_PORT"
#
#   volumes:
#     - .:/app
#       - Monte le dossier actuel dans /app du conteneur
#       - Changements dans le code = immédiatement visibles dans conteneur
#       - ESSENTIEL pour hot reload en développement
#
#   environment:
#     - Variables d'environnement pour le conteneur
#     - Accessible en Python via os.environ['FLASK_ENV']
#
#   depends_on:
#     - redis
#       - Démarre redis AVANT flask
#       - N'attend PAS que redis soit "ready", juste "started"

# redis:
#   image: redis:7-alpine
#     - Utilise image officielle Redis (pas de build)
#     - alpine = version légère

# === COMMANDES ===

# Démarrer tout
docker-compose up

# Démarrer en background
docker-compose up -d

# Voir les logs
docker-compose logs -f flask

# Rebuild si Dockerfile change
docker-compose up --build

# Arrêter tout
docker-compose down

# Arrêter et supprimer volumes
docker-compose down -v

# Exécuter commande dans conteneur
docker-compose exec flask python
docker-compose exec flask flask shell

# === UTILISER REDIS DANS FLASK ===

# app.py avec Redis
from flask import Flask
import redis
import os

app = Flask(__name__)

# Connexion Redis
redis_client = redis.Redis(
    host='redis',                    # Nom du service docker-compose
    port=6379,
    decode_responses=True
)

@app.route('/')
def hello():
    visits = redis_client.incr('visits')
    return f"Hello! Visits: {visits}"

@app.route('/reset')
def reset():
    redis_client.set('visits', 0)
    return "Counter reset!"

# === VARIABLES D'ENVIRONNEMENT ===

# .env (ne PAS commit dans Git!)
FLASK_ENV=development
FLASK_DEBUG=1
DATABASE_URL=postgresql://user:pass@db:5432/mydb
SECRET_KEY=your-secret-key-here
REDIS_URL=redis://redis:6379/0

# docker-compose.yml
version: '3.8'

services:
  flask:
    build: .
    env_file:
      - .env                        # Charge variables depuis .env
    # Ou directement:
    environment:
      - FLASK_ENV=${FLASK_ENV}
      - SECRET_KEY=${SECRET_KEY}

# Dans Flask
# config.py
import os

class Config:
    SECRET_KEY = os.environ.get('SECRET_KEY')
    SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL')
    REDIS_URL = os.environ.get('REDIS_URL')

# === EXEMPLE COMPLET AVEC BASE DE DONNÉES ===

# docker-compose.yml
version: '3.8'

services:
  flask:
    build: .
    ports:
      - "5000:5000"
    volumes:
      - .:/app
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

# app.py avec PostgreSQL
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
import os

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.environ.get('DATABASE_URL')
db = SQLAlchemy(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True)

@app.route('/')
def hello():
    return "Flask + PostgreSQL + Redis in Docker!"

if __name__ == '__main__':
    with app.app_context():
        db.create_all()
    app.run(host='0.0.0.0', port=5000)


[OK] DJANGO AVEC DOCKER

# === STRUCTURE PROJET DJANGO ===

# myproject/
# ├── myproject/
# │   ├── __init__.py
# │   ├── settings.py
# │   ├── urls.py
# │   └── wsgi.py
# ├── app/
# │   ├── migrations/
# │   ├── __init__.py
# │   ├── models.py
# │   └── views.py
# ├── manage.py
# ├── requirements.txt
# ├── Dockerfile
# ├── docker-compose.yml
# └── .env

# === requirements.txt ===

# requirements.txt
Django==4.2.8
psycopg2-binary==2.9.9        # PostgreSQL
gunicorn==21.2.0              # Production
django-environ==0.11.2        # Variables d'env
celery==5.3.4                 # Tasks asynchrones (optionnel)
redis==5.0.1                  # Cache/Celery (optionnel)

# === Dockerfile DÉVELOPPEMENT ===

# Dockerfile.dev
FROM python:3.11-slim

# Installer dépendances système pour PostgreSQL
RUN apt-get update && apt-get install -y \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

# Commande pour développement
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

# === Dockerfile PRODUCTION ===

# Dockerfile
FROM python:3.11-slim AS base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

RUN apt-get update && apt-get install -y \
    postgresql-client \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app

# Builder stage
FROM base AS builder

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Production stage
FROM base

COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

COPY . .

# Collecter fichiers statiques
RUN python manage.py collectstatic --noinput

RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000", "--workers", "4"]

# === docker-compose.yml COMPLET ===

version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: python manage.py runserver 0.0.0.0:8000
    volumes:
      - .:/app                      # Hot reload
    ports:
      - "8000:8000"
    env_file:
      - .env
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: ${DB_NAME:-mydb}
      POSTGRES_USER: ${DB_USER:-postgres}
      POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres}"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

  celery:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: celery -A myproject worker -l info
    volumes:
      - .:/app
    env_file:
      - .env
    depends_on:
      - db
      - redis

  celery-beat:
    build:
      context: .
      dockerfile: Dockerfile.dev
    command: celery -A myproject beat -l info
    volumes:
      - .:/app
    env_file:
      - .env
    depends_on:
      - db
      - redis

volumes:
  postgres_data:

# === EXPLICATION DÉTAILLÉE ===

# web: Service Django principal
#   command: Override CMD du Dockerfile
#     - Utilise runserver (développement uniquement!)
#   volumes:
#     - .:/app
#       - Monte code source pour hot reload
#       - Changements de code = immédiatement visibles
#   depends_on avec condition:
#     - db: service_healthy
#       - Attend que PostgreSQL soit VRAIMENT prêt (healthcheck OK)
#     - redis: service_started
#       - Attend seulement que Redis démarre

# db: Service PostgreSQL
#   environment:
#     - ${DB_NAME:-mydb}
#       - Utilise variable d'env DB_NAME, ou "mydb" par défaut
#   volumes:
#     - postgres_data:/var/lib/postgresql/data
#       - Volume nommé (persiste les données!)
#       - Données conservées même si conteneur supprimé
#   healthcheck:
#     - Vérifie si PostgreSQL est prêt à accepter connexions
#     - pg_isready = commande PostgreSQL

# celery: Workers pour tâches asynchrones
#   command: celery -A myproject worker
#     - Démarre un worker Celery
#     - myproject = nom du projet Django
#   Partage même build, volumes, et env que web

# celery-beat: Scheduler pour tâches périodiques
#   command: celery -A myproject beat
#     - Démarre le scheduler
#     - Lance tâches périodiques (comme cron)

# === .env ===

# .env
DEBUG=1
SECRET_KEY=your-secret-key-change-this-in-production
ALLOWED_HOSTS=localhost,127.0.0.1

# Database
DB_NAME=mydb
DB_USER=postgres
DB_PASSWORD=postgres
DB_HOST=db
DB_PORT=5432

# Redis
REDIS_URL=redis://redis:6379/0

# Celery
CELERY_BROKER_URL=redis://redis:6379/0
CELERY_RESULT_BACKEND=redis://redis:6379/0

# === settings.py (configuration Django) ===

# myproject/settings.py
import os
import environ

# Initialize environ
env = environ.Env(
    DEBUG=(bool, False)
)

# Read .env file
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))

# Security
SECRET_KEY = env('SECRET_KEY')
DEBUG = env('DEBUG')
ALLOWED_HOSTS = env.list('ALLOWED_HOSTS', default=['localhost'])

# Database
DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.postgresql',
        'NAME': env('DB_NAME'),
        'USER': env('DB_USER'),
        'PASSWORD': env('DB_PASSWORD'),
        'HOST': env('DB_HOST'),
        'PORT': env('DB_PORT'),
    }
}

# Cache avec Redis
CACHES = {
    'default': {
        'BACKEND': 'django.core.cache.backends.redis.RedisCache',
        'LOCATION': env('REDIS_URL'),
    }
}

# Celery Configuration
CELERY_BROKER_URL = env('CELERY_BROKER_URL')
CELERY_RESULT_BACKEND = env('CELERY_RESULT_BACKEND')

# Static files
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'staticfiles')

# === COMMANDES UTILES ===

# Démarrer tous les services
docker-compose up

# Rebuild si dépendances changent
docker-compose up --build

# Exécuter migrations
docker-compose exec web python manage.py migrate

# Créer superuser
docker-compose exec web python manage.py createsuperuser

# Collecter fichiers statiques
docker-compose exec web python manage.py collectstatic --noinput

# Shell Django
docker-compose exec web python manage.py shell

# Shell PostgreSQL
docker-compose exec db psql -U postgres -d mydb

# Voir logs
docker-compose logs -f web
docker-compose logs -f celery

# Arrêter tout
docker-compose down

# Arrêter et supprimer volumes ([ATTENTION]  PERTE DE DONNÉES)
docker-compose down -v

# === SCRIPT D'INITIALISATION ===

# entrypoint.sh
#!/bin/bash

# Attendre que PostgreSQL soit prêt
echo "Waiting for PostgreSQL..."
while ! nc -z db 5432; do
  sleep 0.1
done
echo "PostgreSQL started"

# Exécuter migrations
python manage.py migrate

# Collecter fichiers statiques
python manage.py collectstatic --noinput

# Créer superuser si n'existe pas
python manage.py shell << END
from django.contrib.auth import get_user_model
User = get_user_model()
if not User.objects.filter(username='admin').exists():
    User.objects.create_superuser('admin', 'admin@example.com', 'admin')
    print('Superuser created')
END

# Démarrer l'application
exec "$@"

# Dans Dockerfile
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
CMD ["gunicorn", "myproject.wsgi:application", "--bind", "0.0.0.0:8000"]

# === PRODUCTION avec Nginx ===

# docker-compose.prod.yml
version: '3.8'

services:
  web:
    build:
      context: .
      dockerfile: Dockerfile
    command: gunicorn myproject.wsgi:application --bind 0.0.0.0:8000
    volumes:
      - static_volume:/app/staticfiles
    expose:
      - 8000
    env_file:
      - .env.prod
    depends_on:
      - db

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf
      - static_volume:/app/staticfiles
    depends_on:
      - web

  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data
    env_file:
      - .env.prod

volumes:
  postgres_data:
  static_volume:

# nginx.conf
upstream django {
    server web:8000;
}

server {
    listen 80;
    server_name localhost;

    location / {
        proxy_pass http://django;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Host $host;
        proxy_redirect off;
    }

    location /static/ {
        alias /app/staticfiles/;
    }
}

# Démarrer en production
docker-compose -f docker-compose.prod.yml up -d


[OK] FASTAPI AVEC DOCKER

# === STRUCTURE PROJET FASTAPI ===

# myfastapi/
# ├── main.py
# ├── models.py
# ├── schemas.py
# ├── database.py
# ├── requirements.txt
# ├── Dockerfile
# └── docker-compose.yml

# === main.py (exemple simple) ===

# main.py
from fastapi import FastAPI, Depends
from sqlalchemy.orm import Session
from database import engine, get_db, Base
import models

# Créer tables
Base.metadata.create_all(bind=engine)

app = FastAPI(title="My FastAPI App")

@app.get("/")
def read_root():
    return {"message": "Hello from FastAPI in Docker!"}

@app.get("/health")
def health_check():
    return {"status": "healthy"}

@app.get("/items/")
def read_items(db: Session = Depends(get_db)):
    items = db.query(models.Item).all()
    return items

@app.post("/items/")
def create_item(name: str, db: Session = Depends(get_db)):
    item = models.Item(name=name)
    db.add(item)
    db.commit()
    db.refresh(item)
    return item

# === models.py ===

# models.py
from sqlalchemy import Column, Integer, String
from database import Base

class Item(Base):
    __tablename__ = "items"
    
    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)

# === database.py ===

# database.py
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import os

DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///./test.db")

engine = create_engine(DATABASE_URL)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()

# === requirements.txt ===

# requirements.txt
fastapi==0.108.0
uvicorn[standard]==0.25.0      # Serveur ASGI
sqlalchemy==2.0.23
psycopg2-binary==2.9.9
pydantic==2.5.3
python-dotenv==1.0.0
redis==5.0.1                   # Optionnel

# === Dockerfile DÉVELOPPEMENT ===

# Dockerfile.dev
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000

# Uvicorn avec hot reload
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

# === Dockerfile PRODUCTION ===

# Dockerfile
FROM python:3.11-slim AS base

ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app

# Builder stage
FROM base AS builder

COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

# Production stage
FROM base

COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH

COPY . .

RUN useradd -m appuser && chown -R appuser:appuser /app
USER appuser

EXPOSE 8000

# Uvicorn production (plusieurs workers)
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

# === docker-compose.yml ===

version: '3.8'

services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "8000:8000"
    volumes:
      - .:/app                      # Hot reload
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/fastapi_db
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    # Commande avec reload automatique
    command: uvicorn main:app --host 0.0.0.0 --port 8000 --reload

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: fastapi_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  postgres_data:

# === EXPLICATION OPTIONS UVICORN ===

# uvicorn main:app
# - main = fichier main.py
# - app = instance FastAPI dans main.py

# --host 0.0.0.0
# - Écoute sur toutes les interfaces réseau
# - Nécessaire pour Docker (sinon seulement localhost)

# --port 8000
# - Port d'écoute

# --reload
# - Redémarre automatiquement si code change
# - DÉVELOPPEMENT UNIQUEMENT (ralentit les perfs)

# --workers 4
# - Nombre de processus workers
# - Production: généralement (2 x CPU cores) + 1
# - NE PAS utiliser avec --reload

# --log-level info
# - Niveau de logs: critical, error, warning, info, debug, trace

# === COMMANDES ===

# Démarrer
docker-compose up

# Rebuild
docker-compose up --build

# Voir logs
docker-compose logs -f api

# Accéder à l'API
# http://localhost:8000
# Documentation automatique: http://localhost:8000/docs
# Documentation alternative: http://localhost:8000/redoc

# Exécuter commande Python
docker-compose exec api python
docker-compose exec api python -c "print('Hello')"

# Shell dans conteneur
docker-compose exec api bash

# Arrêter
docker-compose down

# === FASTAPI AVEC BACKGROUND TASKS ===

# main.py avec tasks
from fastapi import FastAPI, BackgroundTasks
import time

app = FastAPI()

def send_email(email: str, message: str):
    # Tâche longue
    time.sleep(5)
    print(f"Sending email to {email}: {message}")

@app.post("/send-notification/")
async def send_notification(
    email: str,
    background_tasks: BackgroundTasks
):
    # Lance la tâche en arrière-plan
    background_tasks.add_task(send_email, email, "Welcome!")
    # Retourne immédiatement
    return {"message": "Notification will be sent"}

# === FASTAPI AVEC CELERY (TÂCHES LOURDES) ===

# celery_app.py
from celery import Celery
import os

celery_app = Celery(
    "worker",
    broker=os.getenv("CELERY_BROKER_URL", "redis://redis:6379/0"),
    backend=os.getenv("CELERY_RESULT_BACKEND", "redis://redis:6379/0")
)

@celery_app.task
def process_data(data):
    # Traitement lourd
    time.sleep(10)
    return {"result": "processed"}

# main.py avec Celery
from fastapi import FastAPI
from celery_app import celery_app, process_data

app = FastAPI()

@app.post("/process/")
async def process(data: dict):
    # Lance tâche Celery
    task = process_data.delay(data)
    return {"task_id": task.id, "status": "processing"}

@app.get("/task/{task_id}")
async def get_task(task_id: str):
    task = celery_app.AsyncResult(task_id)
    return {
        "task_id": task_id,
        "status": task.status,
        "result": task.result
    }

# docker-compose.yml avec Celery
version: '3.8'

services:
  api:
    build: .
    ports:
      - "8000:8000"
    volumes:
      - .:/app
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/fastapi_db
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
    depends_on:
      - db
      - redis

  celery_worker:
    build: .
    command: celery -A celery_app worker --loglevel=info
    volumes:
      - .:/app
    environment:
      - CELERY_BROKER_URL=redis://redis:6379/0
      - CELERY_RESULT_BACKEND=redis://redis:6379/0
    depends_on:
      - redis

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: fastapi_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine

volumes:
  postgres_data:

# === FASTAPI AVEC WEBSOCKETS ===

# main.py avec WebSocket
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List

app = FastAPI()

class ConnectionManager:
    def __init__(self):
        self.active_connections: List[WebSocket] = []

    async def connect(self, websocket: WebSocket):
        await websocket.accept()
        self.active_connections.append(websocket)

    def disconnect(self, websocket: WebSocket):
        self.active_connections.remove(websocket)

    async def broadcast(self, message: str):
        for connection in self.active_connections:
            await connection.send_text(message)

manager = ConnectionManager()

@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await manager.connect(websocket)
    try:
        while True:
            data = await websocket.receive_text()
            await manager.broadcast(f"Message: {data}")
    except WebSocketDisconnect:
        manager.disconnect(websocket)
        await manager.broadcast("Client disconnected")


[OK] JUPYTER NOTEBOOK AVEC DOCKER

# === STRUCTURE PROJET ===

# mynotebook/
# ├── notebooks/
# │   └── analysis.ipynb
# ├── data/
# │   └── dataset.csv
# ├── requirements.txt
# ├── Dockerfile
# └── docker-compose.yml

# === requirements.txt ===

# requirements.txt
jupyter==1.0.0
jupyterlab==4.0.10
pandas==2.1.4
numpy==1.26.2
matplotlib==3.8.2
seaborn==0.13.0
scikit-learn==1.3.2
plotly==5.18.0

# === Dockerfile ===

# Dockerfile
FROM python:3.11-slim

WORKDIR /workspace

# Installation dépendances
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Créer dossiers
RUN mkdir -p /workspace/notebooks /workspace/data

# Exposer port Jupyter
EXPOSE 8888

# Démarrer JupyterLab
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root"]

# === docker-compose.yml ===

version: '3.8'

services:
  jupyter:
    build: .
    ports:
      - "8888:8888"
    volumes:
      - ./notebooks:/workspace/notebooks
      - ./data:/workspace/data
    environment:
      - JUPYTER_ENABLE_LAB=yes

# === EXPLICATION ===

# volumes:
#   - ./notebooks:/workspace/notebooks
#     - Monte dossier local "notebooks" dans conteneur
#     - Notebooks sauvegardés sur votre machine!
#     - Persistent même si conteneur supprimé

# CMD ["jupyter", "lab", ...]
# - jupyter lab: lance JupyterLab (interface moderne)
# - --ip=0.0.0.0: accessible depuis l'extérieur
# - --no-browser: ne lance pas de navigateur (impossible dans Docker)
# - --allow-root: permet d'exécuter en root (nécessaire dans Docker)

# === COMMANDES ===

# Démarrer
docker-compose up

# Le terminal affiche:
# http://127.0.0.1:8888/lab?token=xxxxxxxxxxxxx
# Copier cette URL dans votre navigateur

# Ou sans token (moins sécurisé)
# Dockerfile
CMD ["jupyter", "lab", "--ip=0.0.0.0", "--port=8888", "--no-browser", "--allow-root", "--NotebookApp.token=''", "--NotebookApp.password=''"]

# Arrêter
docker-compose down

# === AVEC BASE DE DONNÉES ===

# docker-compose.yml
version: '3.8'

services:
  jupyter:
    build: .
    ports:
      - "8888:8888"
    volumes:
      - ./notebooks:/workspace/notebooks
      - ./data:/workspace/data
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/mydb
    depends_on:
      - db

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: mydb
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  postgres_data:

# Dans un notebook
import pandas as pd
from sqlalchemy import create_engine
import os

# Connexion base de données
engine = create_engine(os.environ['DATABASE_URL'])

# Lire depuis DB
df = pd.read_sql("SELECT * FROM users", engine)

# Écrire vers DB
df.to_sql('results', engine, if_exists='replace')


[OK] STREAMLIT AVEC DOCKER

# === STRUCTURE PROJET ===

# mystreamlit/
# ├── app.py
# ├── requirements.txt
# ├── Dockerfile
# └── docker-compose.yml

# === app.py ===

# app.py
import streamlit as st
import pandas as pd

st.title("My Streamlit App in Docker")

# Sidebar
option = st.sidebar.selectbox(
    "Choose a page:",
    ["Home", "Data", "About"]
)

if option == "Home":
    st.write("Welcome!")
    
elif option == "Data":
    # Upload file
    uploaded_file = st.file_uploader("Choose a CSV file")
    if uploaded_file is not None:
        df = pd.read_csv(uploaded_file)
        st.dataframe(df)

elif option == "About":
    st.write("This is a Streamlit app running in Docker")

# === requirements.txt ===

# requirements.txt
streamlit==1.29.0
pandas==2.1.4
plotly==5.18.0

# === Dockerfile ===

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8501

# Healthcheck
HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health

CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]

# === docker-compose.yml ===

version: '3.8'

services:
  streamlit:
    build: .
    ports:
      - "8501:8501"
    volumes:
      - .:/app

# === COMMANDES ===

# Démarrer
docker-compose up

# Accéder: http://localhost:8501

# Arrêter
docker-compose down


[OK] TESTS AVEC DOCKER

# === STRUCTURE PROJET AVEC TESTS ===

# myproject/
# ├── app/
# │   ├── __init__.py
# │   └── main.py
# ├── tests/
# │   ├── __init__.py
# │   └── test_main.py
# ├── requirements.txt
# ├── requirements-test.txt
# ├── Dockerfile
# ├── Dockerfile.test
# └── docker-compose.test.yml

# === requirements-test.txt ===

# requirements-test.txt
-r requirements.txt

pytest==7.4.3
pytest-cov==4.1.0
pytest-mock==3.12.0
httpx==0.25.2                  # Pour FastAPI tests
faker==21.0.0

# === tests/test_main.py ===

# tests/test_main.py
from fastapi.testclient import TestClient
from app.main import app

client = TestClient(app)

def test_read_root():
    response = client.get("/")
    assert response.status_code == 200
    assert response.json() == {"message": "Hello"}

def test_create_item():
    response = client.post("/items/", json={"name": "test"})
    assert response.status_code == 200
    assert response.json()["name"] == "test"

# === Dockerfile.test ===

# Dockerfile.test
FROM python:3.11-slim

WORKDIR /app

# Installer dépendances de test
COPY requirements-test.txt .
RUN pip install --no-cache-dir -r requirements-test.txt

# Copier code
COPY . .

# Commande de test
CMD ["pytest", "-v", "--cov=app", "--cov-report=term-missing"]

# === docker-compose.test.yml ===

version: '3.8'

services:
  test:
    build:
      context: .
      dockerfile: Dockerfile.test
    environment:
      - DATABASE_URL=postgresql://postgres:postgres@db:5432/test_db
      - TESTING=1
    depends_on:
      db:
        condition: service_healthy

  db:
    image: postgres:15-alpine
    environment:
      POSTGRES_DB: test_db
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5

# === COMMANDES TESTS ===

# Exécuter tests
docker-compose -f docker-compose.test.yml up --abort-on-container-exit

# Options utiles:
# --abort-on-container-exit: arrête tout si un conteneur se termine
# --exit-code-from test: utilise exit code du service "test"

# Exécuter tests avec coverage
docker-compose -f docker-compose.test.yml run --rm test pytest --cov=app --cov-report=html

# Tests spécifiques
docker-compose -f docker-compose.test.yml run --rm test pytest tests/test_main.py

# Tests avec verbose
docker-compose -f docker-compose.test.yml run --rm test pytest -v

# Tests avec pdb (debugger)
docker-compose -f docker-compose.test.yml run --rm test pytest --pdb

# === INTÉGRATION CI/CD ===

# .gitlab-ci.yml
test:
  stage: test
  image: docker:latest
  services:
    - docker:dind
  before_script:
    - docker-compose -f docker-compose.test.yml build
  script:
    - docker-compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from test
  after_script:
    - docker-compose -f docker-compose.test.yml down

# .github/workflows/test.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Build test image
        run: docker-compose -f docker-compose.test.yml build
      
      - name: Run tests
        run: docker-compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from test
      
      - name: Cleanup
        if: always()
        run: docker-compose -f docker-compose.test.yml down


[OK] BONNES PRATIQUES PYTHON + DOCKER

# === 1. ORDRE DES LAYERS (CACHE OPTIMAL) ===

# [X] Mauvais (cache invalidé à chaque changement de code)
FROM python:3.11-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

# [OK] Bon (cache utilisé si requirements.txt ne change pas)
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# === 2. MULTI-STAGE POUR RÉDUIRE TAILLE ===

# [OK] Image finale plus petite
FROM python:3.11-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user --no-cache-dir -r requirements.txt

FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local
ENV PATH=/root/.local/bin:$PATH
COPY . .
CMD ["python", "app.py"]

# === 3. VARIABLES D'ENVIRONNEMENT PYTHON ===

# Dans Dockerfile
ENV PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1 \
    PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

# === 4. UTILISATEUR NON-ROOT ===

# [OK] Toujours créer un utilisateur
RUN useradd -m -u 1000 appuser && \
    chown -R appuser:appuser /app
USER appuser

# === 5. .dockerignore COMPLET ===

# .dockerignore
__pycache__/
*.pyc
*.pyo
*.pyd
.Python
*.so

venv/
env/
.venv/

.pytest_cache/
.coverage
htmlcov/

.git/
.gitignore
.vscode/
.idea/

*.md
docs/

.env
.env.*

# === 6. HEALTHCHECK ===

# Dans Dockerfile
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
  CMD python -c "import requests; requests.get('http://localhost:8000/health')" || exit 1

# Ou avec curl
HEALTHCHECK CMD curl --fail http://localhost:8000/health || exit 1

# === 7. LOGGING ===

# Configurer logging dans app Python
import logging
import sys

# Log vers stdout (visible dans docker logs)
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.StreamHandler(sys.stdout)
    ]
)

logger = logging.getLogger(__name__)
logger.info("Application started")

# === 8. SECRETS ===

# [X] Mauvais: secrets dans Dockerfile
ENV API_KEY=secret123

# [X] Mauvais: secrets dans code
api_key = "secret123"

# [OK] Bon: variables d'environnement
import os
api_key = os.environ.get('API_KEY')

# [OK] Bon: fichier de secrets
with open('/run/secrets/api_key', 'r') as f:
    api_key = f.read().strip()

# === 9. GRACEFUL SHUTDOWN ===

# app.py avec signal handling
import signal
import sys

def signal_handler(sig, frame):
    print('Shutting down gracefully...')
    # Fermer connexions DB, etc.
    sys.exit(0)

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)

# Dans docker-compose.yml
services:
  app:
    stop_grace_period: 30s

# === 10. DÉPENDANCES SYSTÈME ===

# Si besoin de packages système (ex: pour psycopg2, Pillow, etc.)
FROM python:3.11-slim

RUN apt-get update && apt-get install -y \
    gcc \
    postgresql-client \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Ou utiliser wheels précompilés (psycopg2-binary)


[OK] DÉPANNAGE COURANT PYTHON + DOCKER

# === PROBLÈME: Module introuvable ===

# Erreur: ModuleNotFoundError: No module named 'mymodule'

# Solution 1: Vérifier que pip install a fonctionné
docker-compose logs app

# Solution 2: Vérifier que COPY . . est après pip install
# Dockerfile devrait avoir:
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .

# Solution 3: Rebuild l'image
docker-compose up --build

# Solution 4: Vérifier .dockerignore n'ignore pas le module

# === PROBLÈME: Logs non visibles ===

# Solution: PYTHONUNBUFFERED=1
ENV PYTHONUNBUFFERED=1

# Ou au runtime
docker run -e PYTHONUNBUFFERED=1 myapp

# === PROBLÈME: Permission denied ===

# Solution: Ownership correct
RUN useradd -m appuser && \
    chown -R appuser:appuser /app
USER appuser

# === PROBLÈME: Connexion DB échoue ===

# Solution 1: Utiliser healthcheck
depends_on:
  db:
    condition: service_healthy

# Solution 2: Retry dans code Python
import time
from sqlalchemy import create_engine
from sqlalchemy.exc import OperationalError

def get_db_engine(max_retries=5):
    for i in range(max_retries):
        try:
            engine = create_engine(DATABASE_URL)
            engine.connect()
            return engine
        except OperationalError:
            if i < max_retries - 1:
                time.sleep(2)
            else:
                raise

# === PROBLÈME: Hot reload ne marche pas ===

# Solution: Volume correctement monté
volumes:
  - .:/app

# Et reload activé
command: uvicorn main:app --reload

# === PROBLÈME: Image trop grosse ===

# Solution: Multi-stage + slim + .dockerignore
# Voir section optimisation ci-dessus

# Vérifier taille
docker images myapp

# Analyser layers
docker history myapp:latest


[OK] EXEMPLES COMPLETS PAR CAS D'USAGE

# === MICROSERVICE API SIMPLE ===

# Structure:
# api/
# ├── main.py
# ├── requirements.txt
# ├── Dockerfile
# └── docker-compose.yml

# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
USER 1000
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0"]

# docker-compose.yml
version: '3.8'
services:
  api:
    build: .
    ports:
      - "8000:8000"

# === APPLICATION WEB COMPLÈTE (Django + PostgreSQL + Redis) ===

# Voir section Django ci-dessus

# === DATA SCIENCE PIPELINE ===

# Structure:
# pipeline/
# ├── notebooks/
# ├── scripts/
# │   └── process.py
# ├── data/
# ├── requirements.txt
# └── docker-compose.yml

# docker-compose.yml
version: '3.8'
services:
  jupyter:
    image: jupyter/datascience-notebook
    ports:
      - "8888:8888"
    volumes:
      - ./notebooks:/home/jovyan/work
      - ./data:/home/jovyan/data
  
  processor:
    build: .
    volumes:
      - ./data:/app/data
    command: python scripts/process.py

# === BOT / SCRIPT CRON ===

# Dockerfile
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY bot.py .
CMD ["python", "bot.py"]

# docker-compose.yml
version: '3.8'
services:
  bot:
    build: .
    restart: always
    environment:
      - TOKEN=${BOT_TOKEN}


[OK] RESSOURCES ET AIDE

# Documentation officielle:
# Docker Python: https://docs.docker.com/language/python/
# FastAPI: https://fastapi.tiangolo.com/deployment/docker/
# Django: https://docs.djangoproject.com/en/stable/howto/deployment/

# Images officielles:
# Python: https://hub.docker.com/_/python
# PostgreSQL: https://hub.docker.com/_/postgres
# Redis: https://hub.docker.com/_/redis

# Commandes utiles de debug:
docker-compose logs -f app
docker-compose exec app python
docker-compose exec app bash
docker-compose exec db psql -U postgres