Un proxy inverse placé devant une application change surtout trois choses : l’entrée réseau, les en-têtes transmis et la façon dont on fait évoluer le service. Quand je le configure avec Nginx, je cherche à obtenir un point d’accès unique, des règles de routage lisibles et un comportement prévisible en cas de montée en charge ou de panne. C’est utile autant pour une API Node.js que pour un backend monolithique, une grappe de microservices ou une application avec WebSocket.
Dans cet article, je passe en revue la logique du montage, la configuration minimale, les en-têtes à ne pas oublier, les réglages pour HTTPS et les connexions longues, puis les options de répartition de charge et les erreurs qui reviennent le plus souvent en production.
Les points essentiels à garder en tête avant de le déployer
- Nginx devient un point d’entrée unique entre le client et vos services internes.
- Le comportement de `proxy_pass` dépend du slash final et peut modifier l’URI transmise au backend.
- Les en-têtes `Host`, `X-Real-IP` et `X-Forwarded-For` doivent être gérés explicitement si l’application en dépend.
- La répartition de charge par défaut est en round-robin, avec d’autres stratégies comme `least_conn` ou `ip_hash` selon le besoin.
- Les connexions longues, comme WebSocket ou certains flux SSE, exigent des timeouts adaptés.
- Les erreurs les plus coûteuses viennent souvent d’un mauvais routage, d’un redirect non réécrit ou d’un timeout trop court.
Pourquoi placer Nginx en frontal change l’architecture
Je vois souvent Nginx comme une couche de traduction entre le monde public et les services applicatifs. Au lieu d’exposer plusieurs ports, plusieurs certificats et plusieurs règles réseau, on centralise l’accès, on masque l’architecture interne et on garde la main sur la façon dont les requêtes sont distribuées.
Dans un contexte DevOps, l’intérêt n’est pas seulement esthétique. On peut terminer le TLS au même endroit, journaliser les requêtes de manière cohérente, absorber des changements de backend sans déplacer l’URL publique, et faire évoluer les services sans casser les clients. C’est particulièrement propre quand plusieurs équipes déploient des services distincts derrière le même domaine.
| Scénario | Ce que Nginx apporte | Point de vigilance |
|---|---|---|
| API unique derrière un domaine public | Un seul point d’entrée, TLS centralisé, journaux homogènes | Bien transmettre le bon `Host` pour éviter les routes cassées |
| Plusieurs services derrière le même domaine | Routage par chemin ou par nom d’hôte | Éviter les collisions de préfixes et les redirections absolues vers l’interne |
| Montée en charge | Répartition de charge et mise à l’écart des nœuds défaillants | Choisir une stratégie cohérente avec le type de session |
Une fois ce rôle posé, le vrai sujet devient la configuration concrète, parce qu’un montage propre tient souvent à quelques lignes bien choisies.

Construire une configuration de base qui reste lisible
Pour démarrer, je pars d’un bloc simple : un serveur public en face, un ou plusieurs upstreams derrière, et quelques en-têtes utiles pour préserver le contexte d’origine. Le but n’est pas d’empiler des directives, mais d’écrire quelque chose qu’on sait relire six mois plus tard.
upstream app_backend {
server 127.0.0.1:3000;
}
server {
listen 80;
server_name api.exemple.fr;
location / {
proxy_pass http://app_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Le point le plus sous-estimé ici, c’est la transmission du bon `Host`. Si vous laissez Nginx substituer sa propre valeur, certaines applications ne reconnaissent plus le vhost attendu, génèrent des URLs internes ou cassent la logique de génération de liens. En pratique, je préfère presque toujours `proxy_set_header Host $host;`.
Le second piège, c’est le comportement de `proxy_pass` avec ou sans URI. Si vous écrivez `proxy_pass http://backend/;`, Nginx remplace la partie de l’URI qui correspond au `location`. Si vous écrivez `proxy_pass http://backend;`, il transmet l’URI telle qu’elle arrive, sans cette réécriture implicite. C’est un détail minuscule sur le papier, mais c’est souvent lui qui provoque un `404` bizarre ou un endpoint qui perd son préfixe.
location /app/ {
proxy_pass http://127.0.0.1:3000/;
}
location /api/ {
proxy_pass http://127.0.0.1:4000;
}
Dans le premier cas, le préfixe `/app/` est réécrit vers `/`. Dans le second, l’URI est envoyée sans réécriture automatique. C’est précisément le genre de différence qui mérite un test explicite dans votre pipeline avant d’exposer quoi que ce soit au public.
Quand cette base est stable, il faut regarder ce qui se passe dès qu’on ajoute du HTTPS, du WebSocket ou des échanges plus longs que la moyenne.
Gérer les cas modernes sans casser le trafic
Dès qu’une application n’est plus un simple backend HTTP court, je surveille trois sujets : le TLS en amont, les connexions longues et le buffering des requêtes. C’est là que beaucoup d’installations “fonctionnent” en apparence, mais se dégradent au premier trafic réel.
Si votre backend est lui-même en HTTPS, activez `proxy_ssl_server_name on;` lorsque le certificat dépend du nom d’hôte. Sans SNI, certains backends multi-domaines répondent avec le mauvais certificat ou le mauvais site virtuel. C’est un détail de couche transport, mais en pratique il évite des erreurs pénibles à diagnostiquer.
location /chat/ {
proxy_pass http://backend;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
}
Pour WebSocket, il faut transmettre explicitement les en-têtes `Upgrade` et `Connection`. Nginx sait ensuite établir le tunnel quand le backend répond avec un statut `101`. Le timeout mérite aussi d’être ajusté : la valeur par défaut de lecture côté proxy est de 60 secondes, ce qui est souvent trop court pour un canal vivant mais silencieux entre deux messages.
Pour les uploads volumineux ou les APIs qui streament des données, le buffering demande un choix assumé. Par défaut, Nginx lit le corps complet de la requête avant de le relayer. C’est rassurant pour protéger un backend fragile, mais ce n’est pas idéal si vous voulez du flux quasi immédiat. Si vous désactivez le buffering, gardez en tête qu’une requête déjà envoyée ne se rejoue pas facilement vers un autre serveur si le premier tombe au mauvais moment.
Une fois ces cas gérés, la vraie question devient la distribution du trafic entre plusieurs instances et la manière d’absorber une panne sans faire tomber l’ensemble.
Répartir la charge et absorber les défaillances
Nginx ne sert pas seulement à faire passer des requêtes. Il peut aussi arbitrer entre plusieurs serveurs applicatifs. Le comportement par défaut est round-robin, ce qui convient à des backends homogènes et à des requêtes de coût comparable. Dès que ce n’est plus vrai, je change de méthode.
| Méthode | Quand je l’utilise | Limite principale |
|---|---|---|
| Round-robin | Backends similaires, trafic régulier | Ne tient pas compte de la charge réelle de chaque nœud |
| `least_conn` | Requêtes hétérogènes, certaines plus longues que d’autres | Moins pertinent si la latence dépend surtout du traitement métier interne |
| `ip_hash` | Besoin de “sticky sessions” temporaires | Fragile si les sessions vivent seulement en mémoire locale |
| Poids (`weight`) | Instances de capacité différente | Demande un suivi réel des capacités, sinon le réglage devient arbitraire |
La stratégie la plus saine dépend du type d’application. Pour une API sans état, je privilégie un équilibrage simple, puis je laisse le backend conserver les sessions côté base ou via un stockage partagé. Pour une application qui garde encore de l’état local, `ip_hash` peut dépanner, mais je le traite comme une béquille, pas comme une architecture finale.
Nginx ajoute aussi des vérifications passives de santé. Si un serveur renvoie une erreur ou ne répond plus, il est mis de côté pendant un certain temps. Par défaut, `proxy_next_upstream` tente déjà un relais sur `error` et `timeout`, ce qui aide à absorber un incident sans basculer immédiatement toute la requête côté client. Pour moi, cela reste très utile, à condition de garder un œil sur les requêtes non idempotentes : rejouer un `GET` n’a pas le même risque que rejouer un `POST` métier.
Ce mécanisme rend l’ensemble plus robuste, mais il ne pardonne pas les configurations approximatives. C’est précisément ce que je passe en revue juste après.
Les erreurs que je corrige le plus souvent avant la mise en production
Les déploiements cassent rarement sur une seule grosse faute. Ils cassent plus souvent sur une accumulation de petits écarts : un en-tête oublié, un redirect absolu mal réécrit, un timeout trop agressif ou une route qui ne correspond pas exactement au préfixe attendu.
- Oublier `Host` : l’application voit le backend au lieu du domaine public, ce qui casse parfois la génération d’URL ou le routage virtuel.
- Se tromper de slash dans `proxy_pass` : le préfixe est conservé ou supprimé au mauvais endroit, et l’API reçoit une route différente de celle que vous croyez envoyer.
- Ignorer `proxy_redirect` : si le backend répond avec un `Location` vers son hôte interne, le navigateur suit un chemin inaccessible depuis l’extérieur.
- Laisser le timeout par défaut pour un flux long : au-delà de 60 secondes sans activité, la connexion est fermée.
- Désactiver le buffering sans raison claire : vous gagnez en immédiateté, mais vous perdez une partie de la marge de sécurité côté proxy.
- Ne pas journaliser les timings amont : sans `upstream_response_time` et `upstream_status`, on sait qu’une requête a échoué, mais pas où elle a vraiment ralenti.
Je corrige aussi beaucoup de cas où les redirections d’un backend applicatif ne respectent pas l’URL publique. Dans ces situations, `proxy_redirect` sert à réécrire proprement les en-têtes `Location` et `Refresh` pour que le client reste sur la bonne adresse. C’est le genre de détail invisible dans un environnement de test, puis très visible dès qu’un utilisateur clique sur un lien de connexion ou de retour à l’application.
Quand ces points sont verrouillés, il reste la partie la plus sobre et la plus rentable du travail : vérifier que la configuration est testée, traçable et facile à faire évoluer.
Ce que je vérifie avant d’ouvrir l’accès au public
Avant une mise en production, je fais toujours le même contrôle de bon sens. Je valide la configuration, je teste le routage exact, je vérifie les comportements de reprise, puis je confirme que les journaux me donnent assez de contexte pour comprendre un incident en quelques minutes, pas en quelques heures.
- Je lance une validation de configuration avec la commande de test de Nginx avant chaque rechargement.
- Je teste au minimum un chemin racine, un chemin routé vers un backend, une redirection et, si besoin, un échange WebSocket.
- Je confirme que le backend reçoit bien le bon `Host`, la bonne IP client et le bon schéma public.
- Je contrôle le comportement en cas de backend indisponible pour éviter une panne silencieuse.
- Je garde un format de logs qui expose au moins le statut amont et le temps de réponse côté backend.