7.6 KiB
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 :
# ❌ 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 :
mon-backendreçoit une nouvelle adresse IP Docker (ex.172.27.0.43).- Nginx continue d'envoyer les requêtes vers l'ancienne IP (ex.
172.27.0.37). - Nginx reçoit un
Connection refusedouNo route to hostet renvoie un 502 Bad Gateway. - 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 :
- La déclaration explicite du DNS interne Docker (
127.0.0.11) avec un TTL court (ex.valid=10s). - 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 :
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 :
- Valider la syntaxe :
docker exec <proxy> nginx -t - Recharger la config :
docker exec <proxy> nginx -s reload - Test d'attribution dynamique : Redémarrer le conteneur backend (
docker restart <backend>) et vérifier aveccurll'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 :
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 :
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.