Chapitre 7.4 — Proxy vers une app
⏱️ TL;DR — Le cœur du reverse proxy tient en une directive :
proxy_pass http://127.0.0.1:3000;dans unlocation. Mais nue, elle ne suffit pas : sans les en-têtesproxy_set_header, ton app croit que toutes les requêtes viennent de127.0.0.1en HTTP, et casse les redirections, la détection d’IP et les cookies sécurisés. On pose donc le quatuor —Host,X-Real-IP,X-Forwarded-For,X-Forwarded-Proto— pour que l’app connaisse le vrai hôte, la vraie IP et le vrai protocole. On ajouteproxy_http_version 1.1+Upgrade/Connectionpour les WebSockets, des timeouts, un blocupstreamnommé, et on sert le statique de Next (/_next/) directement. Et on relie le tout au fameux 502 : quand l’app derrière tombe, c’est Nginx qui te le dit.
🎯 Objectifs
- Écrire un
locationqui proxifie vers une app locale avecproxy_pass. - Poser les en-têtes indispensables et comprendre pourquoi chacun est là.
- Activer le support des WebSockets (
Upgrade/Connection). - Régler les timeouts et déclarer un
upstreamnommé. - Servir le statique de Next (
/_next/) par Nginx et proxifier le reste. - Faire le lien entre un 502 et une app tombée.
proxy_pass : relayer vers l’app locale
Ton app Next tourne en service (Partie 9) et écoute sur 127.0.0.1:3000 — localement uniquement, jamais exposée. Le rôle de Nginx : recevoir la requête publique et la transmettre à cette app. La directive centrale est proxy_pass.
server {
listen 80;
server_name formacampus.fr;
location / {
proxy_pass http://127.0.0.1:3000; # relaie tout vers l'app locale
}
}Traduit : « pour toute URL commençant par /, transmets la requête à l’app qui écoute sur le port 3000 en local, récupère sa réponse et renvoie-la au visiteur. » Le port 3000 reste fermé au pare-feu ; seul Nginx y accède, depuis la même machine.
Ça fonctionne… mais de façon trompeuse. Vue de ton app, toutes les requêtes semblent venir de 127.0.0.1, en http, sur localhost. Elle a perdu le contexte réel : quel domaine le visiteur a tapé, quelle est sa vraie IP, était-ce du HTTPS. Il faut le lui redonner avec des en-têtes.
Le quatuor d’en-têtes indispensables
proxy_set_header ajoute (ou réécrit) un en-tête dans la requête transmise à l’app. Quatre sont quasi obligatoires. Voici le bloc complet, puis le pourquoi de chacun.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host; # le vrai domaine demandé
proxy_set_header X-Real-IP $remote_addr; # la vraie IP du visiteur
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # chaîne des IP
proxy_set_header X-Forwarded-Proto $scheme; # http ou https d'origine
}Host $host;— sans lui, l’app reçoitHost: 127.0.0.1:3000au lieu deformacampus.fr. Or beaucoup de frameworks s’en servent pour construire des URL absolues (redirections, liens de mail, canonical), choisir un site en multi-tenant, ou valider l’hôte.$hosttransmet le domaine réellement demandé par le visiteur.X-Real-IP $remote_addr;—$remote_addrest l’IP de qui parle à Nginx : ici, le vrai visiteur. Sans cet en-tête, ton app ne verrait que127.0.0.1(l’IP de Nginx) et ne pourrait ni logger, ni géolocaliser, ni limiter par IP réelle.X-Forwarded-For $proxy_add_x_forwarded_for;— la variante « chaîne » : elle empile les IP successives si plusieurs proxys se suivent (CDN → Nginx → app). C’est l’en-tête standard que les frameworks lisent pour retrouver l’IP client d’origine.X-Forwarded-Proto $scheme;—$schemevauthttpouhttpscôté visiteur. Crucial : entre Nginx et l’app, c’est du HTTP clair (local), mais le visiteur, lui, est en HTTPS. Sans cet en-tête, l’app croit être en HTTP et peut casser les cookiesSecure, faire des redirections vershttp://, ou boucler en redirection. C’est la cause classique du « pourquoi mon app redirige-t-elle vers du HTTP ? » derrière un proxy.
💡 Réflexe — Retiens la logique plutôt que la liste : Nginx a déchiffré et masqué l’origine ; il doit la rendre à l’app. Le vrai hôte (
Host), la vraie IP (X-Real-IP/X-Forwarded-For), le vrai protocole (X-Forwarded-Proto). Une app derrière un proxy qui se comporte bizarrement (redirections HTTP, IP toutes identiques, cookies perdus) a presque toujours un en-têteX-Forwarded-*manquant.
⚠️ Piège — La combinaison la plus douloureuse :
proxy_passsansX-Forwarded-Proto. Ton app sert en HTTPS via Nginx, mais elle se croit en HTTP → elle émet des redirections vershttp://, que Nginx renvoie au navigateur, qui repasse en HTTPS… boucle de redirection (ERR_TOO_MANY_REDIRECTS) ou cookies de sessionSecurejamais posés. Le correctif n’est pas dans l’app : c’est l’en-têteX-Forwarded-Proto $scheme;(et, côté framework, la config « trust proxy » qui l’active).
WebSockets : Upgrade et Connection
Une connexion WebSocket commence par une requête HTTP qui demande à passer en connexion persistante bidirectionnelle — un protocol upgrade. Par défaut, Nginx proxifie en HTTP/1.0 et ne relaie pas les en-têtes Upgrade/Connection nécessaires. Pour que le temps réel (chat, notifications live, HMR de dev) fonctionne, il faut trois lignes.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1; # requis pour l'upgrade WebSocket
proxy_set_header Upgrade $http_upgrade; # relaie la demande d'upgrade
proxy_set_header Connection "upgrade"; # signale la connexion persistante
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_http_version 1.1;— l’upgrade WebSocket exige HTTP/1.1 côté proxy.proxy_set_header Upgrade $http_upgrade;— recopie l’en-têteUpgradedu client (vide pour une requête HTTP normale,websocketpour un handshake WS) : la même config marche pour les deux.proxy_set_header Connection "upgrade";— dit à l’app de maintenir la connexion pour le passage en WebSocket.
Timeouts : ne pas rester bloqué
Par défaut, Nginx attend une réponse de l’app pendant un temps borné, puis abandonne avec un 504 Gateway Timeout. Pour des requêtes longues légitimes (génération de rapport, gros calcul, streaming), tu peux rallonger — mais avec parcimonie.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_read_timeout 60s; # temps max d'attente d'une réponse de l'app
# proxy_connect_timeout, proxy_send_timeout existent aussi
}proxy_read_timeout borne l’attente entre deux paquets de la réponse de l’app. Trop court, tu coupes des requêtes longues légitimes (→ 504) ; trop long, tu laisses des connexions traîner. Ajuste selon tes vrais besoins, ne mets pas « très grand » par réflexe.
Un upstream nommé
Plutôt que de répéter http://127.0.0.1:3000 partout, on peut nommer la cible dans un bloc upstream (au niveau http) et y référer par son nom. C’est plus lisible, ça centralise l’adresse, et ça prépare la répartition de charge si un jour tu ajoutes une seconde instance.
upstream front_next {
server 127.0.0.1:3000;
# server 127.0.0.1:3001; # une 2e instance : Nginx répartirait entre les deux
}
server {
listen 80;
server_name formacampus.fr;
location / {
proxy_pass http://front_next; # référence le upstream nommé
proxy_set_header Host $host;
# ... les autres en-têtes ...
}
}Servir le statique de Next et proxifier le reste
Une app Next sert ses fichiers buildés sous /_next/static/…. Les proxifier vers Node fonctionne, mais fait travailler Node pour rien. Mieux : laisser Nginx servir directement ce statique depuis le disque (cache long), et ne proxifier que le dynamique. Voici un server block Next complet et réaliste.
upstream front_next {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name formacampus.fr;
# 1. Les assets buildés de Next : servis par Nginx, cache long
location /_next/static/ {
alias /var/www/formacampus/.next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off; # inutile de logger chaque asset
}
# 2. Tout le reste : proxifié vers l'app Next
location / {
proxy_pass http://front_next;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
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 60s;
}
}Ce que fait ce bloc : le premier location /_next/static/ intercepte les assets versionnés et les sert directement depuis le disque (avec alias qui mappe l’URL vers le dossier réel), cache un an. Tout le reste tombe dans location / et part vers l’app Next avec les bons en-têtes et le support WebSocket (utile pour le HMR en dev, les fonctionnalités temps réel en prod).
🔒 Sécurité — Le port de l’app (3000) doit rester fermé au monde : dans UFW, tu n’ouvres que 80/443, jamais 3000. Nginx y accède en local (
127.0.0.1), et c’est tout ce qu’il faut. Et fais bien écouter ton app sur127.0.0.1:3000, pas0.0.0.0:3000: sur0.0.0.0elle serait joignable directement depuis Internet (si le pare-feu laissait passer), court-circuitant ton proxy et son TLS. Le proxy ne protège que ce qu’on ne peut pas contourner.
Le 502 : quand l’app derrière tombe
Tu l’as croisé au chapitre 1.5 — voici le lien direct avec ce que tu configures ici. Un 502 Bad Gateway signifie : Nginx est debout, mais n’a pas réussi à joindre l’app vers laquelle il proxifie. Presque toujours, l’app est tombée, n’a pas (re)démarré, ou n’écoute pas sur le port attendu.
# le réflexe face à un 502 : vérifier l'app, PAS Nginx
systemctl status formacampus-front # l'app tourne-t-elle ?
journalctl -u formacampus-front -n 50 # ses derniers logs (crash ? erreur au boot ?)
ss -tulpn | grep 3000 # quelque chose écoute-t-il bien sur 3000 ?Distingue bien : 502 = Nginx ne joint pas l’app (app tombée / mauvais port) ; 504 = l’app répond trop lentement (timeout dépassé) ; 500 = l’app répond mais plante (bug applicatif). Le premier chiffre change le lieu du problème.
⚠️ Piège — Passer une heure à trafiquer la config du
proxy_passalors que le 502 crie « l’app est morte ». Le proxy est rarement en cause. Avant de toucher à Nginx, vérifie toujours l’état du service applicatif et ses logs. Cause fréquente aussi : Nginx pointe vers le mauvais port (l’app écoute sur 3001, leproxy_passdit 3000) — un coup dess -tulpnle révèle en une seconde.
🧭 Sur FormaCampus — La config finale du VPS combine les deux mondes : Nginx sert les médias de cours en statique (chapitre 7.3) et proxifie le dynamique. Sur
formacampus.fr,location /part vers le front Next (port 3000) etlocation /api/part vers l’API Symfony (port 8000), chacun avec son quatuor d’en-têtes. Symfony doit recevoirX-Forwarded-Protopour générer ses URL enhttps://et poser ses cookies de session enSecure— l’oublier avait causé, en préprod, une belle boucle de redirection que l’ajout de l’en-tête a réglée d’un coup.
📚 La doc — Le module
ngx_http_proxy_module(surnginx.org) documenteproxy_pass,proxy_set_header, lesproxy_*_timeoutet le fonctionnement desupstream. Côté framework, cherche « trust proxy » (Express), « TRUSTED_PROXIES » (Symfony), ou l’équivalent : l’app doit explicitement faire confiance aux en-têtesX-Forwarded-*pour les prendre en compte.
✏️ Exercices
Exercice 1 — Le proxy minimal correct. Écris un location / qui proxifie vers une app Node sur 127.0.0.1:4000, avec les quatre en-têtes indispensables. Nomme, pour chacun, ce qu’il apporte à l’app.
✅ Solution
location / {
proxy_pass http://127.0.0.1:4000;
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;
}Host → le vrai domaine demandé (URL absolues, multi-tenant). X-Real-IP → la vraie IP du visiteur (logs, géoloc). X-Forwarded-For → la chaîne des IP (standard lu par les frameworks). X-Forwarded-Proto → le protocole d’origine (http/https) pour ne pas casser cookies Secure et redirections.
Exercice 2 — Boucle de redirection. Une app Symfony derrière Nginx boucle en ERR_TOO_MANY_REDIRECTS dès qu’on passe en HTTPS. La config a proxy_pass et Host, mais rien d’autre. Quel en-tête manque, et pourquoi cause-t-il ça ?
✅ Solution
Il manque proxy_set_header X-Forwarded-Proto $scheme; (et, côté Symfony, la conf des trusted proxies). Sans lui, l’app se croit en HTTP alors que le visiteur est en HTTPS : elle émet une redirection vers HTTPS, que le navigateur suit… mais l’app continue de se croire en HTTP et redirige encore → boucle infinie. Transmettre X-Forwarded-Proto $scheme; fait comprendre à l’app qu’elle est déjà en HTTPS, et la boucle disparaît.
Exercice 3 — 502 après déploiement. Après un déploiement, formacampus.fr renvoie 502 Bad Gateway. Le certificat est valide, le site répond (avec la page d’erreur Nginx). Quelle est ta démarche, et qu’est-ce que ce n’est probablement pas ?
✅ Solution
Le 502 dit que Nginx est vivant mais ne joint pas l’app. Démarche : systemctl status <app> (tourne-t-elle ?), journalctl -u <app> -n 50 (a-t-elle crashé au démarrage — build cassé, variable d’env manquante, port déjà pris ?), ss -tulpn | grep 3000 (écoute-t-elle sur le bon port ?). Ce n’est probablement pas un problème de Nginx ni de la config proxy : l’app n’a pas redémarré ou plante au boot. On la répare/relance, le 502 disparaît.
🧠 Quiz de révision
1. Que fait proxy_pass http://127.0.0.1:3000; et pourquoi le port reste-t-il fermé au pare-feu ?
proxy_pass http://127.0.0.1:3000; et pourquoi le port reste-t-il fermé au pare-feu ?Il relaie la requête reçue vers l’app locale qui écoute sur le port 3000. Ce port reste fermé au monde (UFW n’ouvre que 80/443) : Nginx y accède en local sur la même machine, personne d’autre. L’app n’est jamais exposée directement.
2. À quoi sert proxy_set_header Host $host; ?
proxy_set_header Host $host; ?À transmettre à l’app le vrai domaine demandé par le visiteur (formacampus.fr) au lieu de 127.0.0.1:3000. Les frameworks s’en servent pour construire des URL absolues, gérer le multi-tenant, valider l’hôte.
3. Pourquoi X-Forwarded-Proto est-il critique derrière un proxy HTTPS ?
X-Forwarded-Proto est-il critique derrière un proxy HTTPS ?Parce qu’entre Nginx et l’app c’est du HTTP clair (local), mais le visiteur est en HTTPS. Sans cet en-tête, l’app se croit en HTTP : elle casse les cookies Secure, redirige vers http://, ou boucle en redirection. X-Forwarded-Proto $scheme; lui donne le vrai protocole.
4. Quelles directives faut-il pour proxifier des WebSockets ?
proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade; et proxy_set_header Connection "upgrade";. Elles permettent le protocol upgrade HTTP → WebSocket que Nginx ne relaie pas par défaut.
5. 502, 504, 500 : qui est fautif dans chaque cas ?
502 = Nginx ne joint pas l’app (app tombée / mauvais port). 504 = l’app répond trop lentement (timeout dépassé). 500 = l’app répond mais plante (bug applicatif). Le premier oriente vers l’état du service, le deuxième vers la durée/les timeouts, le troisième vers le code.
Chapitre suivant : Vhosts multi-sites — héberger plusieurs domaines et sous-domaines sur un seul VPS.