Faire tourner des applications Python avec Gunicorn et Nginx sur un VPS
Déployer une application Flask ou FastAPI sur Ubuntu avec un virtualenv, un service systemd Gunicorn, un reverse proxy Nginx, TLS, fichiers d’environnement, journalisation, et une liste de contrôle pour les erreurs 502 et le dimensionnement des workers.

Le serveur Flask intégré et uvicorn --reload sont pour le développement. Sur un VPS public, vous voulez un gestionnaire de processus qui redémarre les workers, se lie à localhost, et s’assoit derrière un reverse proxy qui gère TLS et les clients lents. Gunicorn est le choix WSGI habituel pour Flask et Django. FastAPI peut tourner sous Gunicorn avec une classe de worker Uvicorn. Nginx (ou Caddy) termine HTTPS et transmet vers 127.0.0.1:8000.
Ce guide parcourt une organisation qui survit aux redémarrages : projet dans /srv/app, virtualenv, .env avec secrets, une unité gunicorn.service, un bloc server Nginx, Let's Encrypt, et des fichiers de journal que vous pouvez vraiment grepper. Nous parlerons aussi du nombre de workers, des délais d’attente, et pourquoi 502 Bad Gateway n’est presque jamais « Nginx est cassé » — c’est presque toujours Gunicorn qui ne tourne pas, lié au mauvais socket, ou qui plante à l’import. L’exemple utilise Flask, avec des notes pour FastAPI là où la commande diffère.
Pourquoi cette pile
Gunicorn préforke des processus workers. Chaque worker traite une requête à la fois sauf si vous utilisez une autre classe de worker. Nginx met en tampon les clients lents pour que les workers ne restent pas coincés à envoyer des octets vers un réseau mobile. systemd redémarre l’application si elle meurt. Ensemble, c’est ennuyeux, ce que vous voulez à 3 h du matin.
- Gunicorn : modèle de processus WSGI/ASGI stable
- systemd : démarrage au boot, redémarrage en cas de plantage, journaux journald
- Nginx : TLS, fichiers statiques, limites de taille de requête, gzip
- venv : le Python système reste propre
- liaison localhost : l’application n’est joignable que via Nginx
Prérequis
Python 3.10+ sur Ubuntu 22.04/24.04 convient. N’exécutez pas pip en root dans les site-packages système. Il vous faut un domaine pour le TLS. Si vous préférez Caddy comme proxy, l’unité Gunicorn de cet article reste la même — seule la config frontale change (voir l’article Caddy).
- VPS Ubuntu 22.04 ou 24.04
- Votre application avec un requirements.txt ou équivalent
- Un point d’entrée WSGI (pour Flask : app:app) ou ASGI (pour FastAPI : app:app avec workers uvicorn)
- Enregistrement A de domaine pour HTTPS
Étape 1 : Paquets système, utilisateur et répertoire projet
Créez un utilisateur système qui ne peut pas se connecter de façon interactive, propriétaire du code, et qui lance Gunicorn. Installer python3-venv et les outils de compilation évite les échecs pip sur les paquets qui compilent encore des extensions C.
ssh root@YOUR_VPS_IP
apt update && apt -y upgrade
apt -y install python3 python3-venv python3-pip python3-dev build-essential nginx curl
adduser --system --group --home /srv/app appuser
mkdir -p /srv/app
chown appuser:appuser /srv/app
# Copy your code (example):
# rsync -a --delete ./myproject/ appuser@YOUR_VPS_IP:/srv/app/Étape 2 : Virtualenv et dépendances
Créez le venv en tant qu’appuser pour que les propriétaires de fichiers soient corrects. Épinglez les versions en production. Après l’installation, confirmez que vous pouvez importer l’application avec un gunicorn --check-config ponctuel ou un import Python. Les erreurs d’import ici sont les mêmes qui deviennent des 502 plus tard.
sudo -u appuser -H bash -lc '
cd /srv/app
python3 -m venv /srv/app/venv
/srv/app/venv/bin/pip install --upgrade pip
/srv/app/venv/bin/pip install -r /srv/app/requirements.txt gunicorn
'
# Flask example check:
sudo -u appuser -H /srv/app/venv/bin/python -c "from app import app; print('import ok')"Étape 3 : Fichier d’environnement
Ne codez pas en dur SECRET_KEY ou les URL de base dans l’unité systemd d’une façon qui finisse dans un git lisible par tous. Utilisez EnvironmentFile. chmod 640, propriétaire root, groupe appuser (ou propriétaire appuser si vous préférez).
cat >/srv/app/.env <<'EOF'
FLASK_ENV=production
SECRET_KEY=replace-with-openssl-rand-hex-32
DATABASE_URL=postgresql://app:password@127.0.0.1:5432/app
EOF
chown appuser:appuser /srv/app/.env
chmod 600 /srv/app/.envÉtape 4 : Service systemd Gunicorn
Liez-vous à 127.0.0.1:8000, pas 0.0.0.0, sauf raison de sauter Nginx. Le nombre de workers est souvent (2 × CPU) + 1 pour les workers sync ; sur un VPS 2 vCPU cela fait 5, ce qui peut être trop si chaque worker charge un gros modèle ML — alors utilisez 2–3. Pour FastAPI, réglez --worker-class uvicorn.workers.UvicornWorker et installez uvicorn. Les délais d’attente doivent dépasser votre requête honnête la plus lente, pas 30 secondes si vous avez des exports de 2 minutes.
cat >/etc/systemd/system/gunicorn.service <<'EOF'
[Unit]
Description=Gunicorn for the web app
After=network.target
[Service]
User=appuser
Group=appuser
WorkingDirectory=/srv/app
EnvironmentFile=/srv/app/.env
ExecStart=/srv/app/venv/bin/gunicorn --workers 3 --bind 127.0.0.1:8000 --timeout 60 --access-logfile - --error-logfile - app:app
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
systemctl enable --now gunicorn
systemctl status gunicorn --no-pager
ss -tulpn | grep 8000Étape 5 : Reverse proxy Nginx et TLS
Nginx écoute sur 80/443 et proxie vers Gunicorn. client_max_body_size compte pour les téléversements. proxy_read_timeout doit égaler ou dépasser le délai de Gunicorn. Une fois le bloc server fonctionnel en HTTP, émettez un certificat avec Certbot (ou passez le front à Caddy). L’extrait ci-dessous est HTTP uniquement pour tester ; puis lancez Certbot qui peut modifier le fichier.
cat >/etc/nginx/sites-available/app <<'EOF'
server {
listen 80;
server_name app.example.com;
client_max_body_size 20m;
location /static/ {
alias /srv/app/static/;
}
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;
proxy_read_timeout 90s;
}
}
EOF
ln -sf /etc/nginx/sites-available/app /etc/nginx/sites-enabled/app
nginx -t && systemctl reload nginx
ufw allow OpenSSH
ufw allow 'Nginx Full'
ufw enable
apt -y install certbot python3-certbot-nginx
certbot --nginx -d app.example.comÉtape 6 : Fichiers statiques, permissions et spécificités Flask/FastAPI
Nginx devrait servir les fichiers statiques si possible ; c’est plus rapide que Gunicorn. Django collectstatic, Flask send_from_directory pour les toutes petites applications, ou un CDN plus tard. appuser doit pouvoir lire l’arborescence. Si vous utilisez FastAPI, installez uvicorn[standard] dans le venv et changez ExecStart pour utiliser UvicornWorker. Si vous utilisez des sockets Unix au lieu de TCP, pointez proxy_pass vers le socket et alignez les permissions pour que www-data puisse y écrire.
# FastAPI ExecStart example:
# ExecStart=/srv/app/venv/bin/gunicorn -k uvicorn.workers.UvicornWorker --workers 2 --bind 127.0.0.1:8000 app:app
# Unix socket variant:
# --bind unix:/run/gunicorn/app.sock
# Nginx: proxy_pass http://unix:/run/gunicorn/app.sock:
chown -R appuser:appuser /srv/appWorkers, mémoire et rechargements sans coupure
Les workers Gunicorn sync sont simples et suffisent pour des API requête/réponse légères en CPU. S’il vous faut beaucoup d’attentes I/O lentes concurrentes, envisagez gevent ou un worker ASGI — mesurez, ne devinez pas. Chaque worker charge votre application ; RAM ≈ workers fois le RSS de l’application. Un VPS 2 Go avec 8 workers d’une application de 300 Mo swappera et semblera « lent au hasard ». systemctl reload gunicorn (HUP) peut redémarrer les workers avec le nouveau code si vous avez déployé les fichiers en place ; un redémarrage complet est plus clair quand les dépendances changent.
# After git pull / rsync of new code:
sudo -u appuser -H /srv/app/venv/bin/pip install -r /srv/app/requirements.txt
systemctl restart gunicorn
curl -I https://app.example.com/healthDépanner les 502 et les plantages silencieux
502 signifie que Nginx n’a pas obtenu de réponse valide de l’amont. journalctl -u gunicorn -e est la première commande. Causes courantes : mauvais nom module:app, clé .env manquante, Postgres arrêté, lié à 127.0.0.1 mais Nginx sur un autre hôte, SELinux (rare sur Ubuntu), ou l’application n’écoute que sur IPv6. 504 est un délai d’attente. Les boucles 301 arrivent quand l’application redirige vers HTTP alors que X-Forwarded-Proto est ignoré.
- systemctl status gunicorn — est-il actif ?
- journalctl -u gunicorn -n 100 — ImportError, env manquante, base de données
- curl -v http://127.0.0.1:8000/ depuis le VPS — si cela échoue, Nginx est innocent
- nginx -t et error.log — upstream prematurely closed connection
- ss -tulpn | grep 8000 — rien n’écoute
- Disque plein — les workers plantent de façons mystérieuses
Sécurité
L’application ne se lie jamais publiquement. Les secrets restent dans .env. Gardez le venv et l’OS à jour. Ne faites pas tourner Gunicorn en root. Si vous gérez des connexions, définissez les cookies de session Secure et SameSite, et configurez le cadre pour qu’il fasse confiance à X-Forwarded-Proto uniquement depuis Nginx. Limitez le débit des routes de connexion dans Nginx ou dans l’application.
- lier 127.0.0.1 ou un socket Unix
- chmod 600 .env
- User= non root dans systemd
- TLS via Certbot ou Caddy
- Désactiver le mode debug et le rechargement automatique en production
Conseils
- Ajoutez une route /health qui vérifie la connectivité à la base pour Compose et les équilibreurs de charge
- Livrez les assets statiques avec un en-tête cache-control dans Nginx
- Utilisez un worker ou une file séparés (Redis + systemd) pour les e-mails et les tâches lourdes
- Épinglez les versions de gunicorn et uvicorn dans requirements.txt
- Prenez un instantané avant la première bascule en production
Une application Python sur un VPS est prête pour la production quand elle tourne sous Gunicorn comme service systemd, ne se lie qu’à localhost, et est atteinte via Nginx ou Caddy avec TLS. Mettez les secrets dans un EnvironmentFile, dimensionnez les workers selon la RAM plutôt que selon une formule de blog, et déboguez d’abord les 502 depuis le journal Gunicorn. Une fois ce chemin documenté pour votre dépôt, chaque déploiement ultérieur est rsync ou git pull, pip install, et systemctl restart gunicorn.