Aller au contenu

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.

Fenêtre de terminal
sudo apt update
sudo apt install python3-venv python3-dev build-essential
sudo adduser --system --group --home /srv/monapp monapp
Fenêtre de terminal
sudo -u monapp git clone https://git.exemple.com/monapp.git /srv/monapp/app
sudo -u monapp python3 -m venv /srv/monapp/venv
sudo -u monapp /srv/monapp/venv/bin/pip install -r /srv/monapp/app/requirements.txt
sudo -u monapp /srv/monapp/venv/bin/pip install gunicorn

Le dépôt privé demande une clé de déploiement ou un jeton : ne pas les enregistrer dans le dépôt.

/etc/monapp.env (lisible par le service seulement) :

APP_ENV=production
SECRET_KEY=valeur-aleatoire-longue
DATABASE_URL=postgresql://app@127.0.0.1/boutique
Fenêtre de terminal
sudo chown root:monapp /etc/monapp.env
sudo chmod 640 /etc/monapp.env

Générer un secret : python3 -c 'import secrets; print(secrets.token_urlsafe(48))'.

Pour une application Flask exposant app dans monapp/__init__.py (ou wsgi.py) :

Fenêtre de terminal
cd /srv/monapp/app
sudo -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.

/etc/systemd/system/monapp.service :

[Unit]
Description=Application monapp (Gunicorn)
After=network.target
[Service]
User=monapp
Group=monapp
WorkingDirectory=/srv/monapp/app
EnvironmentFile=/etc/monapp.env
ExecStart=/srv/monapp/venv/bin/gunicorn --bind 127.0.0.1:8000 --workers 3 --access-logfile - wsgi:app
Restart=on-failure
RestartSec=3
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/monapp
[Install]
WantedBy=multi-user.target
Fenêtre de terminal
sudo systemctl daemon-reload
sudo systemctl enable --now monapp
systemctl status monapp
curl -I http://127.0.0.1:8000/
sudo journalctl -u monapp -f

Voir systemd : écrire un service et durcissement.

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.

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
}
Fenêtre de terminal
cd /srv/monapp/app
sudo -u monapp git pull
sudo -u monapp /srv/monapp/venv/bin/pip install -r requirements.txt
sudo systemctl restart monapp
sudo journalctl -u monapp -n 30 --no-pager

Sauvegarder la base avant une mise à jour qui modifie le schéma.

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.

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.