Rewrite DEPLOIEMENT.md to match actual codebase and stack

- Fix .env example: OPENAI_API_KEY → MISTRAL_API_KEY, SESSION_SECRET → SECRET_KEY,
  add ADMIN_SECRET
- Remove manual DB init section (init_db() is automatic at service startup)
- Fix build command: remove non-existent vite.config.selfhost.ts, use pnpm build
- Rewrite env vars table: Mistral-first, add Redis/hCaptcha/flood/IP blacklist vars,
  fix FILTER_MODEL / SYNTHESIS_MODEL names
- Remove Python code snippet in "Modèles IA" section (env vars suffice)
- Clarify nginx.conf listens on 8080 (behind HAProxy) vs standalone (port 80)
- Fix section 12: remove "décommentez le bloc HTTPS" (no such block in nginx.conf)
- Fix dépannage: OpenAI → Mistral AI, correct API endpoint and curl command
- Add VITE_* build-time var troubleshooting entry
- Fix project structure: remove vite.config.selfhost.ts, add api-zod, update pages list
- Fix pg_hba.conf: md5 → scram-sha-256

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-24 08:58:08 +02:00
parent b45e9fa0d2
commit f704f48e72
+126 -108
View File
@@ -1,5 +1,5 @@
# Guide d'auto-hébergement — La Voix du Peuple
## RockyLinux 9 (ou RHEL 9, AlmaLinux 9)
## Rocky Linux 9 (ou RHEL 9, AlmaLinux 9)
---
@@ -18,17 +18,20 @@ Internet
- **Frontend** : React + Vite, servi comme fichiers statiques par Nginx
- **Backend** : Flask + Gunicorn (4 workers), accessible uniquement via Nginx
- **Base de données** : PostgreSQL 15+
- **IA** : Mistral AI (MISTRAL_API_KEY requis)
- **IA** : Mistral AI (`MISTRAL_API_KEY` requis) ou tout fournisseur compatible OpenAI
> **Note production :** En déploiement exposé à Internet, placer HAProxy devant Nginx pour la
> terminaison TLS. Voir `ARCHITECTURE.md` pour le schéma complet. Dans cette configuration,
> Nginx écoute sur le port 8080 (fichier `deploy/nginx.conf`) et HAProxy forward sur ce port.
> Pour un déploiement standalone sans HAProxy, changez `listen 8080;` en `listen 80;` dans
> `deploy/nginx.conf` et activez HTTPS avec certbot (section 12).
---
## Prérequis
```bash
# Mettre à jour le système
sudo dnf update -y
# Outils de base
sudo dnf install -y git curl wget epel-release
```
@@ -73,21 +76,22 @@ sudo systemctl enable nginx
## 2. Configurer PostgreSQL
```bash
# Connexion en tant que postgres
sudo -u postgres psql
```
-- Dans psql :
Dans psql :
```sql
CREATE USER voixdupeuple WITH PASSWORD 'CHANGEME_MOT_DE_PASSE_FORT';
CREATE DATABASE voixdupeuple OWNER voixdupeuple;
GRANT ALL PRIVILEGES ON DATABASE voixdupeuple TO voixdupeuple;
\q
```
Éditez `/var/lib/pgsql/15/data/pg_hba.conf` pour autoriser la connexion locale par mot de passe :
Éditez `/var/lib/pgsql/15/data/pg_hba.conf` — remplacez `ident` par `scram-sha-256` sur
la ligne `127.0.0.1/32` :
```
# Remplacez "ident" par "md5" sur la ligne 127.0.0.1/32 :
host all all 127.0.0.1/32 md5
host all all 127.0.0.1/32 scram-sha-256
```
```bash
@@ -109,13 +113,12 @@ sudo chown voixdupeuple:voixdupeuple /opt/voix-du-peuple
---
## 4. Cloner le dépôt depuis Gitea
## 4. Cloner le dépôt
```bash
sudo -u voixdupeuple bash -c "
cd /opt
git clone https://votre-gitea.example.com/utilisateur/voix-du-peuple.git voix-du-peuple
"
sudo -u voixdupeuple git clone \
https://votre-depot.example.com/voix-du-peuple.git \
/opt/voix-du-peuple
```
---
@@ -123,27 +126,24 @@ sudo -u voixdupeuple bash -c "
## 5. Configurer les variables d'environnement
```bash
sudo -u voixdupeuple bash -c "
cp /opt/voix-du-peuple/.env.example /opt/voix-du-peuple/.env
"
# Éditer le fichier .env
sudo -u voixdupeuple cp /opt/voix-du-peuple/.env.example /opt/voix-du-peuple/.env
sudo nano /opt/voix-du-peuple/.env
```
Remplissez au minimum :
Valeurs obligatoires à renseigner :
```env
# Base de données
DATABASE_URL=postgresql://voixdupeuple:CHANGEME_MOT_DE_PASSE_FORT@127.0.0.1:5432/voixdupeuple
OPENAI_API_KEY=sk-VOTRE_CLE_OPENAI
SESSION_SECRET=GENEREZ_UNE_VALEUR_ALEATOIRE_ICI
PORT=8000
FLASK_ENV=production
```
Générez le `SESSION_SECRET` :
```bash
python3.11 -c "import secrets; print(secrets.token_hex(32))"
# IA — Mistral AI (recommandé)
MISTRAL_API_KEY=votre-cle-mistral
# Sécurité — générez avec : python3 -c "import secrets; print(secrets.token_hex(32))"
SECRET_KEY=VALEUR_ALEATOIRE_LONGUE
# Panel d'administration
ADMIN_SECRET=MOT_DE_PASSE_ADMIN_FORT
```
Sécurisez le fichier :
@@ -169,18 +169,9 @@ sudo -u voixdupeuple bash -c "
## 7. Initialiser la base de données
```bash
sudo -u voixdupeuple bash -c "
cd /opt/voix-du-peuple/artifacts/flask-api
source /opt/voix-du-peuple/.env
export DATABASE_URL OPENAI_API_KEY SESSION_SECRET FLASK_ENV PORT
/opt/voix-du-peuple/.venv/bin/python3 -c '
from database import init_db
init_db()
print(\"Base de données initialisée.\")
'
"
```
La base de données est initialisée **automatiquement** au premier démarrage du service
(création des tables et migrations incrémentales idempotentes). Aucune action manuelle n'est
requise. Passez directement à l'étape suivante.
---
@@ -190,14 +181,17 @@ print(\"Base de données initialisée.\")
sudo -u voixdupeuple bash -c "
cd /opt/voix-du-peuple
pnpm install --frozen-lockfile
cd artifacts/voix-du-peuple
pnpm exec vite build --config vite.config.selfhost.ts
pnpm build
"
```
Les fichiers statiques sont générés dans `artifacts/voix-du-peuple/dist/public/`.
Si vous avez configuré `VITE_APP_URL` (URL publique du site, pour le QR code du flyer) ou
`VITE_HCAPTCHA_SITE_KEY`, ajoutez ces variables dans le `.env` **avant** le build — elles
sont intégrées statiquement dans le bundle.
---
## 9. Configurer le service systemd (Gunicorn)
@@ -229,14 +223,16 @@ curl http://127.0.0.1:8000/api/healthz
## 10. Configurer Nginx
Le fichier `deploy/nginx.conf` configure Nginx en écoute sur le port **8080** (mode
derrière HAProxy). Pour un déploiement standalone sans HAProxy, changez `listen 8080;`
en `listen 80;` avant de copier.
```bash
# Copier la config Nginx
sudo cp /opt/voix-du-peuple/deploy/nginx.conf \
/etc/nginx/conf.d/voix-du-peuple.conf
# Éditez le fichier pour mettre votre nom de domaine
sudo nano /etc/nginx/conf.d/voix-du-peuple.conf
# Remplacez voix-du-peuple.example.com par votre domaine
# Si déploiement standalone : changer le port d'écoute
sudo sed -i 's/listen 8080;/listen 80;/' /etc/nginx/conf.d/voix-du-peuple.conf
# Tester la configuration
sudo nginx -t
@@ -250,32 +246,35 @@ sudo systemctl reload nginx
## 11. Ouvrir le pare-feu
```bash
# Standalone (Nginx expose directement HTTP/HTTPS)
sudo firewall-cmd --permanent --add-service=http
sudo firewall-cmd --permanent --add-service=https
sudo firewall-cmd --reload
```
SELinux — autoriser Nginx à se connecter à Gunicorn :
SELinux — autoriser Nginx à proxyfier vers Gunicorn :
```bash
sudo setsebool -P httpd_can_network_connect 1
```
---
## 12. (Recommandé) HTTPS avec Let's Encrypt
## 12. (Recommandé) HTTPS avec Let's Encrypt — déploiement standalone
> Ne s'applique qu'en déploiement standalone (Nginx exposé directement). Si HAProxy gère
> le TLS, configurez Let's Encrypt sur HAProxy — voir `ARCHITECTURE.md`.
```bash
sudo dnf install -y certbot python3-certbot-nginx
# Remplacez voix-du-peuple.example.com par votre domaine
sudo certbot --nginx -d voix-du-peuple.example.com \
--email admin@example.com --agree-tos --no-eff-email
--email contact@example.com --agree-tos --no-eff-email
# Renouvellement automatique (déjà configuré par certbot)
# Vérifier le renouvellement automatique
sudo systemctl status certbot-renew.timer
```
Puis décommentez le bloc HTTPS dans `/etc/nginx/conf.d/voix-du-peuple.conf`.
---
## Structure du projet
@@ -283,28 +282,31 @@ Puis décommentez le bloc HTTPS dans `/etc/nginx/conf.d/voix-du-peuple.conf`.
```
voix-du-peuple/
├── artifacts/
│ ├── flask-api/ # Backend Python Flask
│ │ ├── app.py # Application principale + routes
│ │ ├── ai_agent.py # Agent IA (filtrage + synthèse)
│ │ ├── legal_framework.py # Prompts ancrés dans le droit international
│ │ ├── database.py # Accès PostgreSQL (psycopg2)
│ │ ├── requirements.txt # Dépendances Python
│ │ └── start.sh # Démarrage développement
│ └── voix-du-peuple/ # Frontend React + Vite
│ ├── flask-api/ # Backend Python Flask
│ │ ├── app.py # Application principale + routes API
│ │ ├── ai_agent.py # Agent IA (filtrage, synthèse, check-law)
│ │ ├── legal_framework.py # Prompts ancrés dans le droit (16 sources)
│ │ ├── database.py # Accès PostgreSQL (psycopg2, sans ORM)
│ │ ├── requirements.txt # Dépendances Python
│ │ └── start.sh # Démarrage dev direct (python app.py)
│ └── voix-du-peuple/ # Frontend React + Vite 7
│ ├── src/
│ │ ├── pages/ # home.tsx, about.tsx
│ │ ├── components/ # Composants UI (shadcn/ui + radix)
│ │ ── App.tsx # Routing principal
└── vite.config.ts # Config Vite (développement + production)
│ │ ├── pages/ # home, about, transparence, contributions-brutes,
│ │ │ # consultations, legal-notice, privacy-policy, admin…
│ │ ── components/ # UI (shadcn/ui + Radix), consent-dialog, navbar…
│ ├── lib/ # prompts.ts (verbatim IA), hooks, utils
│ │ └── App.tsx # Routing principal (Wouter)
│ └── vite.config.ts # Config Vite (dev + production)
├── lib/
│ ├── api-spec/ # Spécification OpenAPI
│ ├── api-client-react/ # Hooks React Query générés
│ └── db/ # Schéma Drizzle (référence)
│ ├── api-spec/ # Spécification OpenAPI + config Orval
│ ├── api-client-react/ # Hooks React Query générés
│ └── api-zod/ # Schémas Zod générés
├── deploy/
│ ├── voix-du-peuple-api.service # Service systemd Gunicorn
│ └── nginx.conf # Configuration Nginx
├── .env.example # Variables d'environnement (modèle)
── DEPLOIEMENT.md # Ce fichier
│ └── nginx.conf # Configuration Nginx (port 8080, derrière HAProxy)
├── docs/ # Documentation technique (RGPD, sécurité, prompts…)
── .env.example # Variables d'environnement (modèle commenté)
└── DEPLOIEMENT.md # Ce fichier
```
---
@@ -313,32 +315,36 @@ voix-du-peuple/
| Variable | Obligatoire | Description |
|----------|-------------|-------------|
| `DATABASE_URL` | Oui | URL PostgreSQL complète |
| `OPENAI_API_KEY` | Oui | Clé API OpenAI (sk-...) |
| `OPENAI_BASE_URL` | Non | Proxy OpenAI compatible (Ollama, Azure, etc.) |
| `OPENAI_FILTER_MODEL` | Non | Modèle de filtrage (défaut : `gpt-4o-mini`) |
| `OPENAI_SYNTHESIS_MODEL` | Non | Modèle de synthèse (défaut : `gpt-4o`) |
| `SESSION_SECRET` | Oui | Clé secrète Flask (min 32 caractères aléatoires) |
| `PORT` | Non | Port Gunicorn (défaut : 8000) |
| `FLASK_ENV` | Non | `production` ou `development` |
| `DATABASE_URL` | Oui | URL PostgreSQL `postgresql://user:pass@host:5432/db` |
| `MISTRAL_API_KEY` | Oui* | Clé API Mistral AI (fournisseur par défaut) |
| `OPENAI_API_KEY` | Oui* | Alternative si Mistral non utilisé (compatible OpenAI) |
| `OPENAI_BASE_URL` | Non | Base URL pour fournisseur tiers (Ollama, proxy…) |
| `FILTER_MODEL` | Non | Modèle de filtrage (défaut : `mistral-small-latest`) |
| `SYNTHESIS_MODEL` | Non | Modèle de synthèse (défaut : `mistral-large-latest`) |
| `SECRET_KEY` | Recommandé | Active le cooldown cookie et le consentement RGPD signé HMAC-SHA256 |
| `ADMIN_SECRET` | Recommandé | Mot de passe Bearer token pour le panel `/admin` |
| `REDIS_URL` | Non | Rate limiting persistant entre workers — `redis://localhost:6379/0` |
| `RATE_LIMIT_CONTRIBUTIONS` | Non | Seuil de soumissions (défaut : `5 per minute;3 per hour`) |
| `CONTRIBUTION_COOLDOWN_SECONDS` | Non | Délai inter-soumissions par session (défaut : `240`) |
| `FLOOD_THRESHOLD` | Non | Seuil d'alerte flood en 5 min (défaut : `10`) |
| `IP_BLACKLIST_DAYS` | Non | Durée du blacklist IP (défaut : `30`) |
| `HCAPTCHA_SECRET_KEY` | Non | Active la vérification hCaptcha côté serveur |
| `VITE_HCAPTCHA_SITE_KEY` | Non | Clé publique hCaptcha côté frontend (rebuild requis) |
| `VITE_APP_URL` | Non | URL publique du site — utilisée par le QR code du flyer (rebuild requis) |
*`MISTRAL_API_KEY` est prioritaire. Si absent, `OPENAI_API_KEY` est utilisé. L'un des deux est obligatoire.
---
## Modèles IA utilisés
## Modèles IA
| Fonction | Modèle par défaut | Variable d'environnement |
|----------|-------------------|--------------------------|
| Filtrage des idées | `mistral-small-latest` | `FILTER_MODEL` |
| Rôle | Modèle par défaut | Variable de contrôle |
|------|-------------------|----------------------|
| Filtrage des contributions | `mistral-small-latest` | `FILTER_MODEL` |
| Synthèse collective | `mistral-large-latest` | `SYNTHESIS_MODEL` |
| Vérification cadre légal | même que `FILTER_MODEL` | `FILTER_MODEL` |
Pour changer les modèles, éditez `artifacts/flask-api/ai_agent.py` :
```python
# Filtrage (ligne ~56)
model="gpt-4o-mini", # ou tout modèle OpenAI compatible
# Synthèse (ligne ~104)
model="gpt-4o", # ou tout modèle OpenAI compatible
```
Pour changer de modèle, définissez la variable dans `.env` — aucune modification du code n'est nécessaire.
---
@@ -349,16 +355,15 @@ sudo -u voixdupeuple bash -c "
cd /opt/voix-du-peuple
git pull origin main
# Mise à jour des dépendances Python si nécessaire
# Dépendances Python
.venv/bin/pip install -r artifacts/flask-api/requirements.txt
# Mise à jour des dépendances Node et rebuild frontend
# Rebuild frontend
pnpm install --frozen-lockfile
cd artifacts/voix-du-peuple
pnpm exec vite build --config vite.config.selfhost.ts
cd artifacts/voix-du-peuple && pnpm build
"
# Redémarrer le service
# Redémarrer le service (les migrations DB s'appliquent automatiquement)
sudo systemctl restart voix-du-peuple-api
sudo systemctl reload nginx
```
@@ -368,10 +373,10 @@ sudo systemctl reload nginx
## Logs et supervision
```bash
# Logs API Flask
# Logs API Flask (journald + fichiers)
sudo journalctl -u voix-du-peuple-api -f
tail -f /var/log/voix-du-peuple/api-error.log
tail -f /var/log/voix-du-peuple/api-access.log
sudo tail -f /var/log/voix-du-peuple/api-error.log
sudo tail -f /var/log/voix-du-peuple/api-access.log
# Logs Nginx
sudo tail -f /var/log/nginx/error.log
@@ -391,31 +396,44 @@ sudo systemctl status postgresql-15
```bash
# Vérifier que Gunicorn tourne
sudo systemctl status voix-du-peuple-api
# Vérifier SELinux
sudo setsebool -P httpd_can_network_connect 1
# Tester Gunicorn directement
curl http://127.0.0.1:8000/api/healthz
```
### Erreur "could not connect to server" (PostgreSQL)
```bash
# Vérifier que PostgreSQL tourne
sudo systemctl status postgresql-15
# Vérifier l'URL de connexion dans .env
psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple
psql -U voixdupeuple -h 127.0.0.1 -d voixdupeuple -c "\dt"
# Vérifier DATABASE_URL dans .env
```
### Erreur OpenAI API
### Erreur Mistral AI API
```bash
# Vérifier la clé dans .env
grep OPENAI_API_KEY /opt/voix-du-peuple/.env
# Tester directement
curl https://api.openai.com/v1/models \
-H "Authorization: Bearer $OPENAI_API_KEY"
grep MISTRAL_API_KEY /opt/voix-du-peuple/.env
# Tester l'accès à l'API Mistral
curl https://api.mistral.ai/v1/models \
-H "Authorization: Bearer $(grep MISTRAL_API_KEY /opt/voix-du-peuple/.env | cut -d= -f2)"
```
### Le frontend affiche une page blanche
```bash
# Vérifier que le build existe
ls /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/public/
# Vérifier les droits
sudo chown -R voixdupeuple:voixdupeuple /opt/voix-du-peuple/artifacts/voix-du-peuple/dist/
sudo chown -R voixdupeuple:voixdupeuple \
/opt/voix-du-peuple/artifacts/voix-du-peuple/dist/
```
### Les variables VITE_* ne sont pas prises en compte
Les variables préfixées `VITE_` sont intégrées au moment du build, pas à l'exécution.
Après toute modification de `VITE_APP_URL` ou `VITE_HCAPTCHA_SITE_KEY`, relancez le build :
```bash
cd /opt/voix-du-peuple/artifacts/voix-du-peuple && pnpm build
```