Cours Python · Module 12: Projet final

Déploiement en production

6 min de lecture
Dans cette leçon4

« Chez moi, ça marche » est la phrase la plus répandue de l'histoire de la programmation. Ton ordinateur a la bonne version de Python, les paquets installés et les variables d'environnement définies. Un serveur dans le cloud n'a rien de tout ça. Le déploiement, c'est le moment où le code devient un produit, et notre travail consiste à plier le campement pour qu'il se déploie à l'identique n'importe où dans la savane.

Docker - La conteneurisation

Docker emballe l'application avec le système, Python et les dépendances dans une image. Un conteneur, c'est une image en cours d'exécution. On utilise un build multi-stage : la première étape construit les paquets, la seconde ne récupère que le résultat, ce qui rend l'image finale plus petite. Première étape :

1# Dockerfile
2FROM python:3.12-slim AS builder
3
4WORKDIR /app
5COPY requirements.txt .
6RUN pip wheel --no-cache-dir --no-deps --wheel-dir /wheels -r requirements.txt

pip wheel transforme chaque dépendance en fichier .whl prêt à l'emploi. L'option --no-deps signifie que pip ne va pas chercher les dépendances indirectes, donc requirements.txt doit contenir la liste complète et figée (par exemple issue de pip freeze). Deuxième étape :

1FROM python:3.12-slim
2
3WORKDIR /app
4
5# Dépendances
6COPY --from=builder /wheels /wheels
7RUN pip install --no-cache-dir /wheels/*
8
9# Sécurité
10RUN useradd -m -u 1000 appuser
11USER appuser
12
13# Application
14COPY --chown=appuser:appuser . .
15
16EXPOSE 8000
17CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

L'ordre compte. On installe les paquets encore en root, et seulement ensuite on passe à l'utilisateur ordinaire appuser, car l'application ne doit pas tourner avec les droits root. Si on changeait d'utilisateur avant pip install, pip installerait les paquets dans le répertoire personnel et la commande uvicorn ne serait pas dans le chemin. Tu construis l'image avec docker build -t app:latest . (le point désigne le dossier du Dockerfile) et tu la lances en local avec docker run -p 8000:8000 app:latest. L'ordre de travail typique : Dockerfile, build, test local, push vers un registry, déploiement sur un serveur.

Docker Compose gère plusieurs conteneurs comme un seul service. Notre application a besoin de Qdrant et de Redis, et on décrit tout dans un seul fichier. D'abord le service de l'application :

1# docker-compose.yml
2services:
3  app:
4    build: .
5    ports:
6      - "8000:8000"
7    environment:
8      - OPENAI_API_KEY=${OPENAI_API_KEY}
9      - QDRANT_URL=http://qdrant:6333
10      - REDIS_URL=redis://redis:6379
11    depends_on:
12      - qdrant
13      - redis
14    healthcheck:
15      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
16      interval: 30s
17      timeout: 10s
18      retries: 3

depends_on fixe l'ordre de démarrage, et healthcheck interroge l'endpoint /health toutes les 30 secondes. On utilise pour ça Python et sa bibliothèque standard, car l'image python:3.12-slim ne contient pas curl. Le champ version en haut du fichier est obsolète, Docker Compose actuel l'ignore, donc on ne l'écrit pas. Ensuite, les bases de données et les volumes :

1  qdrant:
2    image: qdrant/qdrant:latest
3    volumes:
4      - qdrant_data:/qdrant/storage
5    ports:
6      - "6333:6333"
7
8  redis:
9    image: redis:alpine
10    volumes:
11      - redis_data:/data
12
13volumes:
14  qdrant_data:
15  redis_data:

Les volumes permettent aux données de survivre à un redémarrage du conteneur. Le tag latest est pratique pour démarrer, mais en production fixe une version précise de l'image pour qu'une mise à jour n'arrive pas par surprise.

GitHub Actions - CI/CD

CI/CD signifie Continuous Integration et Continuous Deployment : chaque push passe automatiquement par les tests, le build et le déploiement. Le pipeline enchaîne les étapes : test, build, push vers le registry, déploiement. La première étape lance les tests :

1# .github/workflows/deploy.yml
2name: Deploy
3
4on:
5  push:
6    branches: [main]
7
8jobs:
9  test:
10    runs-on: ubuntu-latest
11    steps:
12      - uses: actions/checkout@v4
13      - uses: actions/setup-python@v5
14        with:
15          python-version: "3.12"
16      - run: pip install -r requirements.txt
17      - run: pytest tests/ -v
18

Si les tests échouent, les étapes suivantes ne s'exécutent pas. La deuxième étape construit l'image et l'envoie sur Docker Hub (needs: test signifie « seulement après des tests réussis ») :

1  build:
2    needs: test
3    runs-on: ubuntu-latest
4    steps:
5      - uses: actions/checkout@v4
6
7      - name: Build Docker image
8        run: docker build -t app:latest .
9
10      - name: Push to Registry
11        run: |
12          echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.DOCKER_USERNAME }} --password-stdin
13          docker tag app:latest ${{ secrets.DOCKER_USERNAME }}/ai-assistant:latest
14          docker push ${{ secrets.DOCKER_USERNAME }}/ai-assistant:latest
15

Le mot de passe arrive à docker login via --password-stdin, il n'apparaît donc pas dans les logs. La dernière étape se connecte au serveur en SSH et remplace les conteneurs :

1  deploy:
2    needs: build
3    runs-on: ubuntu-latest
4    steps:
5      - name: Deploy to server
6        uses: appleboy/ssh-action@v1
7        with:
8          host: ${{ secrets.SERVER_HOST }}
9          username: ${{ secrets.SERVER_USER }}
10          key: ${{ secrets.SSH_KEY }}
11          script: |
12            cd /app
13            docker compose pull
14            docker compose up -d

On fixe l'action sur une version (@v1) et pas sur la branche master, car une branche peut changer sans prévenir. C'est d'autant plus important que l'action reçoit la clé SSH de ton serveur.

Options de déploiement dans le cloud

Où faire tourner le conteneur ? Voici les quatre grandes voies avec leurs avantages et leurs inconvénients :

1"""
2Options de déploiement :
3
41. VPS (DigitalOcean, Hetzner)
5   - Contrôle total
6   - Coût faible (selon le fournisseur et la taille de la machine)
7   - Nécessite de la gestion
8
92. Platform as a Service
10   - Railway, Render, Fly.io
11   - Déploiement facile
12   - Auto-scaling
13
143. Kubernetes
15   - Pour les grands systèmes
16   - Haute disponibilité
17   - Configuration complexe
18
194. Serverless
20   - AWS Lambda, Google Cloud Run
21   - Paiement à l'usage
22   - Cold starts
23"""

Les prix changent souvent, compare-les donc dans les grilles tarifaires actuelles des fournisseurs. Mon conseil pour un premier projet de portfolio : une plateforme PaaS, car tu déploies un conteneur en quelques minutes sans avoir à administrer de serveur.

Monitoring

Après le déploiement, tu dois savoir si l'application est vivante et à quelle vitesse elle répond. La bibliothèque prometheus_client fournit des compteurs (Counter) et des histogrammes de durée (Histogram), que Prometheus lit sur l'endpoint /metrics. app est l'application FastAPI de la leçon précédente :

1# Métriques Prometheus
2from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest
3from fastapi import Response
4
5REQUEST_COUNT = Counter('requests_total', 'Total requests', ['method', 'endpoint'])
6REQUEST_LATENCY = Histogram('request_latency_seconds', 'Request latency')
7
8@app.middleware("http")
9async def metrics_middleware(request, call_next):
10    REQUEST_COUNT.labels(request.method, request.url.path).inc()
11    with REQUEST_LATENCY.time():
12        response = await call_next(request)
13    return response
14
15@app.get("/metrics")
16async def metrics():
17    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

Le middleware compte chaque requête et mesure sa durée. CONTENT_TYPE_LATEST est le bon type de contenu pour le format Prometheus, plus sûr qu'un text/plain tapé à la main. Attention au label avec le chemin : avec des URL contenant des identifiants, le nombre de séries peut croître sans limite.

Ton projet est prêt pour la production ! Dans la prochaine leçon, on construira un portfolio qui le montrera au monde.

Souviens-toi : un campement bien plié se déploie à l'identique n'importe où dans la savane, et c'est exactement ce que t'apporte un conteneur.

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quel est l'avantage du build Docker multi-stage ?

  2. 2. Que permet de faire Docker Compose ?

Voici 2 des 3 questions de cette leçon. Réponds aux autres dans le jeu.

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Clique sur les éléments pour construire un pipeline de déploiement dans le bon ordre :

  • Tri vertical

    Range les étapes du travail avec Docker dans le bon ordre :

  • Éditeur de code

    Écris un Dockerfile pour une application d'IA en Python

  • Tri horizontal

    Range la commande Docker build avec un tag dans le bon ordre :

  • Clique dans l'ordre

    Clique sur les éléments pour construire une commande docker run :

  • Éditeur de code

    Crée un fichier docker-compose.yml pour une application d'IA avec une base de données

  • Tri vertical

    Range les étapes d'un pipeline CI/CD dans le bon ordre :

Articles utiles