Python : mise en production
Déploiement type d’une application web Python (WSGI : Flask, Django) derrière un reverse proxy. Noms génériques : application monapp, utilisateur monapp, dossier /srv/monapp, port local 8000.
Préparer le serveur
Section intitulée « Préparer le serveur »sudo apt updatesudo apt install python3-venv python3-dev build-essentialsudo adduser --system --group --home /srv/monapp monappDéployer le code et les dépendances
Section intitulée « Déployer le code et les dépendances »sudo -u monapp git clone https://git.exemple.com/monapp.git /srv/monapp/appsudo -u monapp python3 -m venv /srv/monapp/venvsudo -u monapp /srv/monapp/venv/bin/pip install -r /srv/monapp/app/requirements.txtsudo -u monapp /srv/monapp/venv/bin/pip install gunicornLe dépôt privé demande une clé de déploiement ou un jeton : ne pas les enregistrer dans le dépôt.
Variables d’environnement
Section intitulée « Variables d’environnement »/etc/monapp.env (lisible par le service seulement) :
APP_ENV=productionSECRET_KEY=valeur-aleatoire-longueDATABASE_URL=postgresql://app@127.0.0.1/boutiquesudo chown root:monapp /etc/monapp.envsudo chmod 640 /etc/monapp.envGénérer un secret : python3 -c 'import secrets; print(secrets.token_urlsafe(48))'.
Gunicorn
Section intitulée « Gunicorn »Pour une application Flask exposant app dans monapp/__init__.py (ou wsgi.py) :
cd /srv/monapp/appsudo -u monapp /srv/monapp/venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 wsgi:app| Option | Rôle |
|---|---|
--bind 127.0.0.1:8000 |
Écoute en local uniquement (le reverse proxy est public) |
--workers N |
Processus de travail : environ 2 × nombre de cœurs + 1 |
--timeout 30 |
Durée maximale d’une requête |
--access-logfile - |
Journal d’accès vers la sortie standard (donc journald) |
Pour une application ASGI (FastAPI, Starlette), utiliser uvicorn : uvicorn monapp.main:app --host 127.0.0.1 --port 8000 --workers 3.
Service systemd
Section intitulée « Service systemd »/etc/systemd/system/monapp.service :
[Unit]Description=Application monapp (Gunicorn)After=network.target
[Service]User=monappGroup=monappWorkingDirectory=/srv/monapp/appEnvironmentFile=/etc/monapp.envExecStart=/srv/monapp/venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 --access-logfile - wsgi:appRestart=on-failureRestartSec=3NoNewPrivileges=truePrivateTmp=trueProtectSystem=strictProtectHome=trueReadWritePaths=/srv/monapp
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now monappsystemctl status monappcurl -I http://127.0.0.1:8000/sudo journalctl -u monapp -fVoir systemd : écrire un service et durcissement.
Reverse proxy
Section intitulée « Reverse proxy »Caddy :
exemple.com { encode zstd gzip reverse_proxy 127.0.0.1:8000}Nginx :
location / { proxy_pass http://127.0.0.1:8000; 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;}Voir Caddy : exemples réutilisables et Nginx : reverse proxy. Pour un exemple complet avec pare-feu et Fail2ban : Exemple : VPS avec Caddy.
Fichiers statiques
Section intitulée « Fichiers statiques »Servir les fichiers statiques par le serveur web plutôt que par Python :
exemple.com { handle /static/* { root * /srv/monapp/app file_server } reverse_proxy 127.0.0.1:8000}Mettre à jour l’application
Section intitulée « Mettre à jour l’application »cd /srv/monapp/appsudo -u monapp git pullsudo -u monapp /srv/monapp/venv/bin/pip install -r requirements.txtsudo systemctl restart monappsudo journalctl -u monapp -n 30 --no-pagerSauvegarder la base avant une mise à jour qui modifie le schéma.
Tâches planifiées
Section intitulée « Tâches planifiées »Un script Python lancé périodiquement s’écrit comme un minuteur systemd (voir systemd : minuteurs) avec ExecStart=/srv/monapp/venv/bin/python /srv/monapp/app/tache.py.
Dépannage
Section intitulée « Dépannage »| Symptôme | Piste |
|---|---|
502 Bad Gateway |
Service arrêté ou port erroné : systemctl status monapp, curl http://127.0.0.1:8000 |
| Le service redémarre en boucle | journalctl -u monapp -n 50 : import raté, variable manquante, port déjà utilisé |
ModuleNotFoundError dans le service |
Le service doit utiliser l’interpréteur du venv (/srv/monapp/venv/bin/…) |
Permission denied |
Droits de /srv/monapp ou chemin couvert par ProtectSystem : ajouter ReadWritePaths |
| Variables d’environnement absentes | EnvironmentFile : chemin, droits, syntaxe (CLE=valeur, sans export) |
Address already in use |
Autre processus sur le port : sudo ss -tlnp | grep 8000 |
| Après une montée de version d’Ubuntu, le service échoue | Recréer le venv (la version de Python a changé) |
Ubuntu 24.04 et 26.04 : différences à connaître
Section intitulée « Ubuntu 24.04 et 26.04 : différences à connaître »La procédure est identique. Sur 26.04 (Python 3.14), recréer l’environnement virtuel à chaque changement de version de Python, vérifier la compatibilité de Gunicorn et des dépendances, et tenir compte du changement de méthode de démarrage de multiprocessing (forkserver) si l’application crée des processus.