Partie 3 : Migration vers Flask + Gunicorn

Partie 3 : Migration vers Flask + Gunicorn

Octobre 2025

Pourquoi Flask ?

Après quelques jours sous Docker avec http.server, les limitations étaient évidentes :

  • Single-threaded : Une seule requête à la fois
  • Pas de routes : Impossible d'ajouter de la logique
  • Pas de templates : HTML statique uniquement
  • Performance : Pas conçu pour la production
  • Évolution : Impossible d'ajouter des features

Flask résout tous ces problèmes !

La migration Flask dans Docker

L'énorme avantage de Docker : la migration est triviale !

1. Création de requirements.txt

Flask==3.0.0
gunicorn==21.2.0

2. Création de app.py

#!/usr/bin/env python3
"""
Application Flask pour Home-Fonta.fr
"""

from flask import Flask, send_from_directory, abort
import os

app = Flask(__name__, static_folder='.', static_url_path='')

# Configuration
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0  # Désactive le cache pour dev

@app.route('/')
def index():
    """Page d'accueil"""
    return send_from_directory('.', 'index.html')

@app.route('/presentation')
def presentation():
    """Page de présentation"""  
    return send_from_directory('.', 'presentation.html')

@app.route('/services')
def services():
    """Page des services"""
    return send_from_directory('.', 'services.html')

@app.route('/galerie')
def galerie():
    """Galerie photos"""
    return send_from_directory('.', 'galerie.html')

@app.route('/menu')
def menu():
    """Menu pour AJAX"""
    return send_from_directory('.', 'menu.html')

@app.route('/static/<path:filename>')
def static_files(filename):
    """Sert les fichiers statiques (CSS, JS, images)"""
    return send_from_directory('static', filename)

@app.route('/health')
def health():
    """Health check pour Docker"""
    return {'status': 'ok', 'server': 'Flask+Gunicorn'}, 200

@app.errorhandler(404)
def not_found(e):
    """Page 404 personnalisée"""
    if os.path.exists('404.html'):
        return send_from_directory('.', '404.html'), 404
    return "404 - Page non trouvée", 404

if __name__ == '__main__':
    # Mode debug pour tests locaux uniquement
    app.run(host='0.0.0.0', port=8000, debug=True)

3. Ajout de Gunicorn (WSGI server)

# wsgi.py
from app import app

if __name__ == "__main__":
    app.run()

Gunicorn est un serveur WSGI qui permet :

  • Multi-workers : Plusieurs processus Python
  • Multi-threads : Plusieurs threads par worker
  • Graceful reload : Mise à jour sans coupure
  • Production-ready : Utilisé par Instagram, Pinterest...

4. Nouveau Dockerfile optimisé

# Dockerfile v2 - Flask + Gunicorn
FROM python:3.11-slim

LABEL maintainer="home-fonta.fr"
LABEL description="Flask + Gunicorn pour Home-Fonta.fr"

ENV PYTHONUNBUFFERED=1
ENV PYTHONDONTWRITEBYTECODE=1
ENV PORT=8000

# Créer un utilisateur non-root
RUN useradd -m -u 1000 webuser

WORKDIR /app

# Installer les dépendances Python (cache Docker si unchanged)
COPY --chown=webuser:webuser requirements.txt /app/
RUN pip install --no-cache-dir -r requirements.txt

# Copier l'application
COPY --chown=webuser:webuser . /app/

EXPOSE 8000

USER webuser

# Lancer Gunicorn avec 2 workers et 2 threads
CMD ["gunicorn", "--bind", "0.0.0.0:8000", \
     "--workers", "2", \
     "--threads", "2", \
     "--timeout", "60", \
     "--access-logfile", "-", \
     "--error-logfile", "-", \
     "wsgi:app"]

Migration sans interruption

#!/bin/bash
# setup-flask.sh - Migration Python simple → Flask

echo "Migration vers Flask + Gunicorn"

# 1. Test local Flask
python app.py
# Vérifier sur http://localhost:8000

# 2. Build nouvelle image
docker-compose down
docker-compose build --no-cache

# 3. Lancer le nouveau container
docker-compose up -d

# 4. Vérification
sleep 5
curl -I http://localhost:8001
# HTTP/1.1 200 OK
# Server: gunicorn

Comparaison des performances

J'ai fait des benchmarks avec Apache Bench :

# Test avec http.server
ab -n 1000 -c 10 http://localhost:8000/
Requests per second: 83.45 [#/sec]
Time per request: 119.844 [ms]

# Test avec Flask + Gunicorn
ab -n 1000 -c 10 http://localhost:8001/
Requests per second: 412.67 [#/sec]
Time per request: 24.232 [ms]

5x plus rapide !

Nouvelles possibilités avec Flask

1. Routes dynamiques

@app.route('/api/stats')
def stats():
    return {
        'visitors': get_visitor_count(),
        'pages': count_pages(),
        'uptime': get_uptime()
    }

2. Templates Jinja2

from flask import render_template

@app.route('/blog/<article>')
def blog(article):
    return render_template('blog.html', 
                         article=article,
                         date=datetime.now())

3. Formulaires

from flask import request

@app.route('/contact', methods=['POST'])
def contact():
    name = request.form.get('name')
    email = request.form.get('email')
    # Envoyer email, sauvegarder en DB...
    return {'status': 'sent'}

4. Sessions et cookies

from flask import session

@app.route('/login', methods=['POST'])
def login():
    session['user'] = request.form.get('username')
    return redirect('/')

Problème découvert : Fichiers statiques

Même avec Flask, servir les fichiers statiques directement depuis Python n'est pas optimal. Les images lourdes causaient encore des lenteurs.

Solution temporaire

# Route explicite pour les statiques
@app.route('/static/<path:filename>')
def static_files(filename):
    return send_from_directory('static', filename)

Mais ce n'était qu'un patch...

Monitoring avec Gunicorn

Gunicorn offre des métriques utiles :

# Logs détaillés
docker logs home-fonta-flask

[2025-10-29 10:23:45 +0000] [7] [INFO] Starting gunicorn 21.2.0
[2025-10-29 10:23:45 +0000] [7] [INFO] Listening at: http://0.0.0.0:8000 (7)
[2025-10-29 10:23:45 +0000] [7] [INFO] Using worker: threads
[2025-10-29 10:23:45 +0000] [10] [INFO] Booting worker with pid: 10
[2025-10-29 10:23:45 +0000] [11] [INFO] Booting worker with pid: 11

# Stats en temps réel
docker exec home-fonta-flask ps aux
USER  PID  %CPU %MEM    VSZ   RSS COMMAND
1000    1  0.0  0.8  56432 32768 gunicorn: master
1000   10  0.1  1.2  78652 49152 gunicorn: worker
1000   11  0.1  1.2  78652 49152 gunicorn: worker

Configuration avancée de Gunicorn

# gunicorn_config.py
bind = "0.0.0.0:8000"
workers = 2
threads = 2
worker_class = "gthread"
timeout = 60
keepalive = 5
max_requests = 1000
max_requests_jitter = 50
preload_app = True
accesslog = "-"
errorlog = "-"
log_level = "info"

Optimisations Docker + Flask

Layer caching intelligent

# Les dépendances changent rarement
COPY requirements.txt .
RUN pip install -r requirements.txt

# Le code change souvent
COPY . .

Health check amélioré

healthcheck:
  test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
  interval: 30s
  timeout: 10s
  retries: 3
  start_period: 40s  # Gunicorn met du temps à démarrer

Résultats après Flask

Métrique http.server Flask+Gunicorn Amélioration
Req/sec 83 412 5x
Latence 120ms 24ms 5x
Workers 1 2 2x
Threads 1 4 4x
CPU usage 95% 45% 2x
RAM 50MB 100MB Acceptable

Leçons apprises

  1. Flask transforme un site statique en app web
  2. Gunicorn est indispensable en production
  3. Multi-workers = performance
  4. Docker rend les migrations triviales
  5. Les fichiers statiques restent un problème

Prochaine étape évidente

Le site tournait bien mieux avec Flask, mais je savais que ce n'était qu'une étape. La vraie transformation viendrait avec Kubernetes.

Mon NAS hébergeait plein d'autres services Docker. Pourquoi ne pas tout migrer vers un vrai cluster ?

L'idée : Créer un cluster K3s avec 4 Raspberry Pi !


Suite : Partie 4 - L'aventure Kubernetes commence

Read more