126 lines
7.6 KiB
Markdown
126 lines
7.6 KiB
Markdown
# Règle d'Or : Résolution DNS Dynamique dans les Proxies Nginx Docker
|
|
|
|
## 1. Contexte du Problème (Cause Racine de l'Erreur 502)
|
|
|
|
Dans un environnement Docker où les conteneurs sont reliés par un réseau virtuel ( ou réseau nommé partagé comme `n8n`), chaque conteneur peut obtenir une nouvelle adresse IP interne (ex. `172.27.0.x`) à chaque redémarrage, mise à jour ou recréation.
|
|
|
|
Par défaut dans Nginx, lorsqu'une directive `proxy_pass` utilise un nom d'hôte statique direct :
|
|
```nginx
|
|
# ❌ CONFIGURATION VULNÉRABLE (Résolution DNS statique au boot)
|
|
location / {
|
|
proxy_pass http://mon-backend:8080;
|
|
}
|
|
```
|
|
Nginx résout le nom `mon-backend` **une seule fois au démarrage de Nginx** et met l'adresse IP en cache de manière permanente en mémoire.
|
|
|
|
### Conséquence :
|
|
Dès que `mon-backend` redémarre ou est mis à jour :
|
|
1. `mon-backend` reçoit une nouvelle adresse IP Docker (ex. `172.27.0.43`).
|
|
2. Nginx continue d'envoyer les requêtes vers l'ancienne IP (ex. `172.27.0.37`).
|
|
3. Nginx reçoit un `Connection refused` ou `No route to host` et renvoie un **502 Bad Gateway**.
|
|
4. Le service reste en panne jusqu'à un redémarrage manuel du conteneur Nginx.
|
|
|
|
---
|
|
|
|
## 2. Le Pattern Requis : `resolver 127.0.0.11` + Variable Dynamique
|
|
|
|
Pour forcer Nginx à réévaluer la résolution DNS selon le TTL Docker sans dépendre d'un cache figé, **deux éléments sont indispensables** :
|
|
|
|
1. La déclaration explicite du DNS interne Docker (`127.0.0.11`) avec un TTL court (ex. `valid=10s`).
|
|
2. L'assignation de l'URL cible dans une **variable intermédiaire** (`set $upstream_var ...`). Nginx est conçu pour ne pas pré-résoudre les variables au boot, mais au moment du traitement de la requête.
|
|
|
|
### Configuration Standard Recommandée :
|
|
|
|
```nginx
|
|
server {
|
|
listen 80;
|
|
server_name service.bolbol.tn;
|
|
|
|
# 1. DNS interne Docker (127.0.0.11) avec re-résolution toutes les 10s
|
|
resolver 127.0.0.11 valid=10s ipv6=off;
|
|
|
|
# 2. Définition dynamique de l'amont via variable
|
|
location / {
|
|
set $backend_upstream http://mon-service:8080;
|
|
proxy_pass $backend_upstream;
|
|
|
|
proxy_http_version 1.1;
|
|
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_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection $connection_upgrade;
|
|
proxy_buffering off;
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
chunked_transfer_encoding on;
|
|
}
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## 3. Périmètre Appliqué sur le NAS (21/08/2026)
|
|
|
|
| Service Proxy | Fichier de Configuration | Statut |
|
|
|---------------|--------------------------|--------|
|
|
| `baserow-oauth-proxy` | `/volume1/docker/baserow-oauth-stub/nginx.conf` | ✅ Corrigé & Testé en direct |
|
|
| `baserow-schema-mcp-proxy` | `/volume1/docker/baserow-schema-mcp/nginx.conf` | ✅ Corrigé & Validé |
|
|
| `redaction-pro` | `/volume1/docker/redaction-pro/nginx.conf` | ✅ Corrigé & Validé |
|
|
| `formation-consultant` | `/volume1/docker/formation-consultant/nginx.conf` | N/A (fichiers statiques locaux, aucun upstream) |
|
|
|
|
---
|
|
|
|
## 4. Protocole de Test de Résilience Obligatoire
|
|
|
|
Lors de la mise en place ou modification d'un reverse proxy Nginx :
|
|
1. Valider la syntaxe : `docker exec <proxy> nginx -t`
|
|
2. Recharger la config : `docker exec <proxy> nginx -s reload`
|
|
3. **Test d'attribution dynamique** : Redémarrer le conteneur backend (`docker restart <backend>`) et vérifier avec `curl` l'accès via le proxy **sans toucher au conteneur proxy**.
|
|
|
|
## Piege additionnel confirme (02/09/2026) — le prefixe de location n'est PAS tronque avec proxy_pass + variable
|
|
|
|
**Symptome** : sur redaction-pro, TOUS les appels API echouaient en 405 Method Not Allowed depuis le navigateur, quel que soit le modele choisi — alors que les memes appels fonctionnaient parfaitement en tapant directement l'URL de Bifrost. Aucun rapport avec la whitelist de modeles (voir common/bifrost-vk-incidents.md, incident du meme jour).
|
|
|
|
**Cause** : le pattern documente plus haut (`set $upstream ...; proxy_pass $upstream;`) resout bien le probleme de cache DNS, MAIS il a un effet de bord non documente jusqu'ici : quand `proxy_pass` cible une **variable**, nginx ne tronque plus jamais le prefixe du `location` — il transmet l'URI ORIGINALE complete au backend, contrairement a un `proxy_pass` avec une URI litterale qui tronque automatiquement le prefixe du `location` matche.
|
|
|
|
Concretement sur redaction-pro :
|
|
```nginx
|
|
location /api/ {
|
|
set $bifrost_upstream http://bifrost:8080;
|
|
proxy_pass $bifrost_upstream/; # BUG : le "/" final est ignore avec une variable
|
|
}
|
|
```
|
|
Une requete navigateur vers `/api/v1/chat/completions` etait transmise a Bifrost telle quelle, soit `/api/v1/chat/completions` — un chemin que Bifrost ne reconnait pas comme route API (son router de secours sert alors sa propre page de dashboard en GET, et renvoie 405 sur tout le reste).
|
|
|
|
**Fix** : ajouter un `rewrite` explicite AVANT le `proxy_pass`, pour forcer la troncature que la variable empeche :
|
|
```nginx
|
|
location /api/ {
|
|
set $bifrost_upstream http://bifrost:8080;
|
|
rewrite ^/api/(.*)$ /$1 break;
|
|
proxy_pass $bifrost_upstream; # sans "/" final, inutile desormais
|
|
}
|
|
```
|
|
Valider avec `docker exec <proxy> nginx -t` puis `nginx -s reload` (pas besoin de redemarrer le conteneur).
|
|
|
|
**Regle generale** : des qu'un `location` a un PREFIXE non-racine (`/api/`, `/mcp/`, etc.) ET que le `proxy_pass` cible une variable (pattern DNS dynamique ci-dessus), un `rewrite ... break;` de troncature est **obligatoire**, sinon le backend recoit un chemin errone en silence (pas d'erreur nginx, juste un mauvais routage cote backend).
|
|
|
|
**A verifier** (non fait le 02/09, hors perimetre de la tache du jour) : `baserow-schema-mcp/nginx.conf` utilise `location /mcp/ { proxy_pass $mcp_upstream; }` — meme pattern a risque, jamais audite sous cet angle precis. A verifier si le backend MCP attend un chemin sans prefixe avant de considerer que c'est fonctionnel par coincidence ou reellement correct.
|
|
|
|
## Tableau perimetre — mise a jour
|
|
|
|
| Service Proxy | Statut prefixe non-racine |
|
|
|---|---|
|
|
| `redaction-pro` (`/api/`) | ✅ Bug trouve et corrige le 02/09/2026 (rewrite ajoute) |
|
|
| `baserow-schema-mcp` (`/mcp/`) | ✅ Verifie 03/09 — pas un bug, backend attend le prefixe complet (voir addendum) |
|
|
| `baserow-oauth-proxy` | OK — utilise `$upstream$request_uri` (chemin complet volontaire), pas concerne |
|
|
|
|
## Vérification 03/09/2026 — baserow-schema-mcp exclu du périmètre (faux positif)
|
|
|
|
Le pattern `location /mcp/ { proxy_pass $mcp_upstream; }` (sans rewrite ni `$request_uri`) ressemble au bug corrigé sur redaction-pro, mais **ce n'en est PAS un ici**. Vérifié dans `app/main.py` : le serveur mcp.server.fastmcp lui-même monte ses routes sous `Mount(f"/mcp/{MCP_SECRET_TOKEN}", app=mcp.sse_app())` — le backend attend explicitement le chemin complet avec le préfixe `/mcp/<token>/...`. Transmettre l'URI non tronquée (comportement par défaut de `proxy_pass` avec variable) est donc ici le comportement CORRECT et nécessaire, pas un bug.
|
|
|
|
Testé bout-en-bout via le domaine public (`GET /mcp/<token>/sse`) : 200 OK, event-stream valide avec endpoint de session retourné. Aucune action requise sur ce service.
|
|
|
|
**Règle affinée** : le pattern à risque décrit plus haut ne s'applique que si le backend attend un chemin SANS le préfixe du `location`. Toujours vérifier le montage réel du backend (code source ou test direct sur le port interne, avec et sans préfixe) avant de conclure à un bug — ne pas généraliser depuis la seule forme de la config nginx.
|