Chapitre 10.5 — Python & le pattern générique
⏱️ TL;DR — Python en prod (Django, FastAPI, Flask) suit exactement le même patron que Node : un process qui écoute en local (
gunicornpour du WSGI,uvicornpour de l’ASGI), lancé par un service systemd, dans un virtualenv, avec Nginx en façade quiproxy_passvers un port local ou un socket Unix. Et c’est là qu’on referme la partie : ce patron n’est pas propre à Python. Go compile un binaire qui écoute → même chose. Ruby lancepuma→ même chose. Rust, Java, Elixir → même chose. Toute app web se ramène à « un process local + un reverse proxy ». La seule vraie exception, c’est PHP (FastCGI au lieu de HTTP) et le statique (aucun process). Morale du cours : apprends le pattern, pas 15 recettes. Ce chapitre le formule noir sur blanc et en donne le tableau récapitulatif.
🎯 Objectifs
- Déployer une app Python (WSGI/ASGI) avec
gunicorn/uvicornen service systemd derrière Nginx. - Comprendre virtualenv, le choix port local vs socket Unix, et le
proxy_pass. - Situer Django, FastAPI, Flask dans ce schéma.
- Généraliser le patron à Go, Ruby, Rust, Java… et savoir le reconnaître partout.
- Retenir le tableau des stacks : ce qui change, ce qui ne change jamais.
Python : le même patron que Node
Une app Python ne se sert pas « toute seule » en prod. Le serveur de dev (python manage.py runserver, flask run, uvicorn --reload) est fait pour développer, pas pour encaisser du trafic. En prod, on met un serveur d’application devant le code — c’est le rôle de gunicorn ou uvicorn — et Nginx devant lui.
Deux mondes selon que ton framework est synchrone ou asynchrone :
- WSGI (l’interface synchrone historique) : Django, Flask. Serveur d’app :
gunicorn. - ASGI (l’interface asynchrone moderne, WebSockets, async/await) : FastAPI, Django en mode async, Starlette. Serveur d’app :
uvicorn(souvent piloté par gunicorn avec des workers uvicorn).
Tu reconnais le schéma du cycle de vie d’une requête et de la Partie 9 : Nginx en façade, app en local. Seuls les noms changent.
1. Le virtualenv : isoler les dépendances
Python installe les dépendances par projet dans un environnement virtuel (virtualenv) : un dossier qui contient son interpréteur et ses paquets, sans polluer le Python système ni les autres projets. C’est l’équivalent du node_modules local.
cd /var/www/api-python
# Crée un virtualenv dans le dossier .venv
python3 -m venv .venv
# L'active (le shell utilise désormais ce Python et ce pip)
source .venv/bin/activate
# Installe les dépendances du projet + le serveur d'app
pip install -r requirements.txt gunicorn uvicorn⚠️ Piège — Installer les paquets en global (
sudo pip install ...) « pour aller plus vite ». Tu mélanges alors les dépendances de tous les projets et du système, et une mise à jour en casse un autre. Un virtualenv par app, toujours. Le service systemd pointera l’interpréteur du virtualenv (.venv/bin/gunicorn), pas le Python système.
2. Lancer gunicorn/uvicorn sur un socket local
On fait écouter le serveur d’app en local uniquement — soit un socket Unix (recommandé, comme PHP-FPM), soit un port sur 127.0.0.1. Jamais sur 0.0.0.0 : seul Nginx doit lui parler.
# WSGI (Django/Flask) via gunicorn, sur un socket Unix
gunicorn --workers 3 --bind unix:/run/api-python.sock monprojet.wsgi:application
# ASGI (FastAPI) via gunicorn pilotant des workers uvicorn
gunicorn --workers 3 --worker-class uvicorn.workers.UvicornWorker \
--bind unix:/run/api-python.sock main:app--workers 3: nombre de process qui traitent les requêtes en parallèle (règle de départ courante :2 × cœurs + 1, à ajuster selon la RAM — même logique quepm.max_childrenen 10.1).--bind unix:/run/api-python.sock: écoute sur un socket Unix local. On aurait pu écrire--bind 127.0.0.1:8000pour un port local.monprojet.wsgi:application/main:app: où trouver l’application (module:objet).
3. Le service systemd
Comme pour Node, on ne lance jamais gunicorn à la main dans un terminal : il faut qu’il redémarre au boot et après un crash. C’est le rôle de systemd (Partie 3). On écrit un unit file.
# /etc/systemd/system/api-python.service
[Unit]
Description=API Python (gunicorn) pour FormaCampus
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/api-python
# On appelle le gunicorn DU virtualenv, pas le système
ExecStart=/var/www/api-python/.venv/bin/gunicorn \
--workers 3 --bind unix:/run/api-python.sock monprojet.wsgi:application
Restart=always
# Variables d'environnement (ou EnvironmentFile=/var/www/api-python/.env)
Environment=DJANGO_SETTINGS_MODULE=monprojet.settings.prod
[Install]
WantedBy=multi-user.targetsudo systemctl daemon-reload # recharge les units après édition
sudo systemctl enable --now api-python # démarre ET active au boot
sudo systemctl status api-python # vérifie que ça tourne
journalctl -u api-python -f # suit les logs en direct💡 Réflexe —
enable --nowfait d’un coupenable(démarrage au boot) etstart(démarrage immédiat). L’oubli classique est de fairestartsansenable: ça marche… jusqu’au premier reboot, où le service ne remonte pas.enableest ce qui te sauve après un redémarrage du VPS.
4. Nginx devant : proxy_pass
Le server block est le même que pour Node : Nginx reçoit le trafic public et le relaie vers le socket local du serveur d’app.
server {
listen 80;
server_name api-py.formacampus.fr;
location / {
# Relaie vers le socket Unix de gunicorn
proxy_pass http://unix:/run/api-python.sock;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# Django/Flask ne servent pas le statique en prod : Nginx s'en charge
location /static/ {
alias /var/www/api-python/staticfiles/;
expires 30d;
}
}Les en-têtes X-Forwarded-* disent à l’app d’où vient vraiment la requête (l’IP du client, le protocole HTTPS), puisqu’elle ne voit directement que Nginx. Le bloc location /static/ rappelle un point important : le serveur d’app Python ne sert pas les fichiers statiques en prod (Django les rassemble via collectstatic), c’est Nginx qui les sert — exactement l’idée du chapitre 10.4.
🔒 Sécurité — Le serveur d’app écoute sur un socket Unix (ou
127.0.0.1), jamais sur0.0.0.0. Ainsi il est injoignable directement depuis Internet : on ne peut l’atteindre qu’à travers Nginx, qui filtre, chiffre (TLS) et applique ses règles. Un gunicorn bindé sur0.0.0.0:8000avec le pare-feu mal réglé, c’est ton app exposée nue, sans HTTPS ni filtrage. Le socket local est aussi une mesure de sécurité, pas qu’une histoire de perf.
5. Django, FastAPI, Flask : les spécificités
Le patron est identique ; seuls quelques gestes diffèrent :
- Django :
python manage.py collectstatic(rassemble le statique pour Nginx) etpython manage.py migrate(migrations, comme Doctrine en 10.2) au déploiement ;DEBUG = FalseetALLOWED_HOSTSbien réglés en prod (l’équivalent duAPP_DEBUG=0de Symfony). Servi par gunicorn (projet.wsgi). - FastAPI : ASGI, servi par uvicorn (souvent via gunicorn + workers uvicorn) ; idéal pour l’async et les WebSockets.
- Flask : WSGI minimaliste, servi par gunicorn (
app:app). Le serveur intégré de Flask est explicitement marqué « pas pour la prod ».
⚠️ Piège — Laisser
DEBUG = Trueen prod côté Django (ou lancerrunserver/flask runcomme serveur de prod). C’est le pendant exact duAPP_DEBUG=1de Symfony (10.2) : pages d’erreur qui exposent le code et la config, et serveur de dev monothread incapable de tenir la charge. En prod :DEBUG = False, un vrai serveur d’app (gunicorn/uvicorn) en service, Nginx devant.
Le pattern générique : ce qui ne change jamais
Recule d’un pas. Depuis la Partie 7, tout ce qu’on déploie tient dans une seule phrase :
Une app web en prod = un process qui écoute en local + Nginx en façade qui relaie.
Ce qui change d’une techno à l’autre est mineur : le nom du serveur d’app (Node lui-même, gunicorn, uvicorn, puma…) et le protocole entre Nginx et l’app (HTTP proxifié, ou FastCGI pour PHP). Ce qui ne change jamais : Nginx devant, l’app en local (socket ou port privé), un service systemd pour la maintenir en vie, la base non exposée, HTTPS via Let’s Encrypt.
C’est pour ça qu’apprendre à déployer Go, Ruby ou Rust ne demandera pas un nouveau cours :
- Go :
go buildproduit un binaire qui écoute sur un port. Service systemd +proxy_pass. Même patron. - Ruby on Rails : serveur d’app
pumasur un socket. Service systemd +proxy_pass. Même patron. - Rust (Axum, Actix) : un binaire qui écoute. Même patron.
- Java (Spring Boot) : un
.jarqui écoute sur un port. Même patron.
Le tableau à retenir
| Stack | Ce qui tourne en local | Nginx lui parle en… | Service systemd ? |
|---|---|---|---|
| Node / Next.js | node (l’app elle-même) | HTTP (proxy_pass) | Oui |
| PHP / Symfony / WordPress | PHP-FPM | FastCGI (fastcgi_pass) | Oui (FPM) |
| Python (Django/FastAPI/Flask) | gunicorn / uvicorn | HTTP (proxy_pass) | Oui |
| Go / Rust / Java | le binaire / le .jar | HTTP (proxy_pass) | Oui |
| Ruby on Rails | puma | HTTP (proxy_pass) | Oui |
| Statique / SPA | rien | Nginx sert les fichiers | Non |
Deux familles, en réalité : « ça parle HTTP en local » (presque tout : proxy_pass), « c’est PHP » (FastCGI vers FPM), et le cas béni du statique (Nginx seul). Voilà tout le paysage.
💡 Réflexe — Face à une stack que tu ne connais pas, ne cherche pas « le tuto déploiement de X ». Pose trois questions : (1) Comment cette techno écoute-t-elle en local (quel serveur d’app, quel port/socket) ? (2) Comment la garder en vie (service systemd) ? (3) Comment Nginx lui parle (proxy HTTP, ou statique) ? Tu as répondu au déploiement de n’importe quelle app web. C’est un pattern, pas un catalogue.
🧭 Sur FormaCampus — FormaCampus a beau être surtout Next.js + Symfony, son équipe data a un micro-service de recommandation en Python/FastAPI (ASGI, uvicorn) qui tourne sur le VPS comme service systemd, écoute sur
/run/reco.sock, et est exposé par Nginx sous/api/reco/— le même patron que l’API Symfony et le front Next, juste un serveur d’app différent. C’est la preuve par l’exemple du cours entier : une fois le patron « process local + Nginx » acquis, ajouter une brique dans n’importe quel langage ne coûte qu’un virtualenv, un unit systemd et unlocationde plus. FormaCampus tient front Next, API Symfony (FPM), blog WordPress, docs statiques et micro-service Python sur un seul VPS, avec un seul modèle mental.
📚 La doc — Références : la doc gunicorn (« Deploying Gunicorn », qui recommande précisément Nginx devant), uvicorn (« Deployment »), et les guides « Deployment » officiels de Django, FastAPI et Flask — tous décrivent ce même montage serveur d’app + reverse proxy. Pour systemd,
man systemd.service.
✏️ Exercices
Exercice 1 — Le service Python. Écris le unit systemd minimal pour une API FastAPI dans /var/www/reco, servie par gunicorn avec des workers uvicorn, écoutant sur /run/reco.sock, sous l’utilisateur www-data, redémarrant après un crash.
✅ Solution
# /etc/systemd/system/reco.service
[Unit]
Description=API reco FastAPI (uvicorn)
After=network.target
[Service]
User=www-data
Group=www-data
WorkingDirectory=/var/www/reco
ExecStart=/var/www/reco/.venv/bin/gunicorn \
--workers 3 --worker-class uvicorn.workers.UvicornWorker \
--bind unix:/run/reco.sock main:app
Restart=always
[Install]
WantedBy=multi-user.targetPuis sudo systemctl daemon-reload && sudo systemctl enable --now reco. Points clés : le gunicorn du virtualenv (.venv/bin/gunicorn), --worker-class uvicorn.workers.UvicornWorker (car ASGI), le socket Unix local, Restart=always, et enable pour le boot.
Exercice 2 — Traduis le patron. Un collègue doit déployer une API Go (un binaire qui écoute sur 127.0.0.1:8080) et te demande « comment on fait, il n’y a pas de tuto Go dans le cours ? ». Réponds en trois étapes concrètes.
✅ Solution
C’est le même patron, aucun tuto spécifique nécessaire. (1) Faire tourner : un service systemd dont ExecStart lance le binaire Go (qui écoute déjà sur 127.0.0.1:8080), avec Restart=always et enable pour le boot. (2) Nginx devant : un server block avec location / { proxy_pass http://127.0.0.1:8080; } et les en-têtes X-Forwarded-*. (3) HTTPS via Let’s Encrypt (Partie 8) et base non exposée. Go, Python, Node : même montage, seul le process local change.
Exercice 3 — Range les stacks. Classe ces stacks en deux colonnes : « Nginx parle HTTP (proxy_pass) » et « autre cas ». Node/Next, WordPress, FastAPI, un site Astro statique, Rails, Symfony.
✅ Solution
Nginx parle HTTP (proxy_pass) : Node/Next, FastAPI (gunicorn/uvicorn), Rails (puma). Autres cas : WordPress et Symfony → FastCGI vers PHP-FPM (fastcgi_pass) ; site Astro statique → aucun process, Nginx sert les fichiers directement. On retrouve les deux vraies familles : « parle HTTP en local » (la majorité), « c’est PHP » (FastCGI), plus le cas béni du statique.
🧠 Quiz de révision
1. Quels serveurs d’app utilise-t-on pour du WSGI et pour de l’ASGI en Python ?
WSGI (synchrone : Django, Flask) → gunicorn. ASGI (asynchrone : FastAPI, Django async) → uvicorn, souvent piloté par gunicorn avec des workers uvicorn. Dans les deux cas, Nginx est en façade et proxy_pass vers le socket local.
2. Pourquoi un virtualenv, et que doit viser l’ExecStart du service systemd ?
ExecStart du service systemd ?Le virtualenv isole les dépendances par projet (son interpréteur, ses paquets), sans polluer le Python système ni les autres apps. Le service systemd doit viser le binaire du virtualenv (.venv/bin/gunicorn), pas le Python global.
3. Formule en une phrase le pattern générique de déploiement du cours.
Une app web en prod = un process qui écoute en local + Nginx en façade qui relaie (via proxy_pass HTTP dans la plupart des cas, via FastCGI pour PHP), le tout maintenu en vie par systemd, base non exposée, HTTPS devant.
4. Qu’est-ce qui change, et qu’est-ce qui ne change jamais, entre déployer du Python, du Go et du Node ?
Ce qui change : le nom du serveur d’app (node, gunicorn/uvicorn, le binaire Go) et le port/socket local. Ce qui ne change jamais : Nginx en façade, l’app en local (socket ou 127.0.0.1), un service systemd, la base non exposée, HTTPS via Let’s Encrypt. Même patron partout (sauf PHP en FastCGI et le statique sans process).
5. Pourquoi Django/Flask ne servent-ils pas leurs fichiers statiques en prod ?
Parce que c’est le travail de Nginx, bien plus efficace pour ça (chapitre 10.4). En prod, Django rassemble le statique avec collectstatic dans un dossier, et Nginx le sert via un location /static/. Le serveur d’app Python ne traite que le dynamique.
Fin de la Partie 10. Tu as vu que le patron « app derrière Nginx » se décline à toutes les stacks — PHP en FastCGI, Python/Node/Go en proxy, statique sans process. Il reste à donner à ces apps où stocker leurs données : Partie 11 — Bases de données en prod.