Résoudre les problèmes de reverse proxy Nginx dans Docker : 502, 504 et pièges SSL
Ce tutoriel explique en profondeur comment diagnostiquer et corriger les erreurs typiques d’un reverse proxy Nginx exécuté dans Docker, en particulier les 502 Bad Gateway, 504 Gateway Timeout, et les problèmes SSL/TLS (certificats, SNI, chaînes incomplètes, boucles HTTP↔HTTPS, WebSocket, HTTP/2, etc.). L’objectif est de vous donner une méthode reproductible avec des commandes réelles, des configs Nginx et des tests côté réseau, Docker et applicatif.
1) Comprendre ce que signifient 502 et 504 dans un reverse proxy
1.1 502 Bad Gateway : Nginx n’arrive pas à parler à l’upstream
Un 502 côté Nginx indique généralement :
- Nginx a tenté de se connecter à l’upstream (votre application) et a échoué (connexion refusée, reset, DNS, mauvais port, protocole incorrect).
- Ou Nginx a reçu une réponse invalide (ex : parler HTTP à un upstream qui attend HTTPS, ou inversement).
- Ou l’upstream a crashé / n’écoute pas.
En Docker, les causes courantes sont :
- mauvais nom de service (DNS Docker),
- mauvais port (port interne vs port publié),
- application qui écoute sur
127.0.0.1au lieu de0.0.0.0, - réseau Docker non partagé entre Nginx et l’app,
- Nginx qui tente d’atteindre
localhost(qui est le conteneur Nginx, pas l’hôte).
1.2 504 Gateway Timeout : Nginx a parlé à l’upstream, mais ça a trop duré
Un 504 signifie que Nginx a bien initié la connexion, mais :
- l’upstream met trop de temps à répondre,
- ou Nginx attend des données (headers/body) trop longtemps,
- ou le réseau est instable / saturé,
- ou l’application est bloquée (deadlock, requête DB lente, pool saturé).
Dans Docker, cela peut être aggravé par :
- limites CPU/RAM,
- DNS lent,
- MTU/overlay réseau,
- timeouts Nginx trop faibles par rapport aux temps de réponse réels,
- proxy buffer / streaming mal réglé (SSE, uploads).
2) Cartographier l’architecture : ports, réseaux, flux
Avant de toucher à Nginx, clarifiez :
- Quel conteneur écoute où ?
- Quel réseau Docker relie Nginx à l’app ?
- Quel port interne l’app expose ?
- L’app écoute sur 0.0.0.0 ?
Commandes utiles :
docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Ports}}\t{{.Status}}'
Inspecter un conteneur (réseaux, IP, variables) :
docker inspect nginx-proxy | less
docker inspect mon-app | less
Lister les réseaux :
docker network ls
docker network inspect bridge | less
Vérifier que les deux conteneurs sont sur le même réseau (ex : app-net) :
docker network inspect app-net | grep -A3 -B3 '"Name":'
3) Erreur classique : utiliser localhost au lieu du nom Docker
3.1 Pourquoi localhost casse tout dans Docker
Si Nginx est dans un conteneur, proxy_pass http://localhost:3000; pointe vers le conteneur Nginx, pas vers votre application.
✅ Solution : utiliser le nom du service (DNS Docker) ou l’IP du conteneur sur le même réseau.
Exemple correct :
location / {
proxy_pass http://mon-app:3000;
}
3.2 Tester la résolution DNS depuis le conteneur Nginx
Entrez dans le conteneur :
docker exec -it nginx-proxy sh
Puis testez :
getent hosts mon-app
Si getent n’existe pas, essayez :
cat /etc/resolv.conf
nslookup mon-app 2>/dev/null || true
Test de connectivité TCP :
nc -vz mon-app 3000
Test HTTP :
wget -S -O- http://mon-app:3000/ 2>&1 | head -n 40
Si ça échoue, ce n’est pas (encore) un problème Nginx : c’est réseau/port/app.
4) L’application écoute sur 127.0.0.1 : invisible depuis l’extérieur du conteneur
Beaucoup de serveurs dev (Node, Python, etc.) écoutent par défaut sur 127.0.0.1. Dans un conteneur, cela signifie : accessible seulement depuis le même conteneur.
4.1 Symptôme
curl http://mon-app:3000depuis Nginx → connexion refusée- mais
curl http://127.0.0.1:3000depuis le conteneur mon-app fonctionne.
4.2 Correction
Configurer l’app pour écouter sur 0.0.0.0.
Exemples :
Node/Express :
app.listen(3000, "0.0.0.0");
Uvicorn (FastAPI) :
uvicorn main:app --host 0.0.0.0 --port 8000
Gunicorn :
gunicorn -b 0.0.0.0:8000 wsgi:app
Validez dans le conteneur app :
docker exec -it mon-app sh -c "ss -lntp || netstat -lntp"
Vous devez voir 0.0.0.0:PORT (ou :::PORT).
5) Réseaux Docker : bridge par défaut vs réseau user-defined
5.1 Le piège du réseau bridge par défaut
Sur le bridge par défaut, la résolution DNS par nom n’est pas toujours disponible comme sur un réseau user-defined (selon votre usage). La bonne pratique est de créer un réseau dédié.
Créer un réseau :
docker network create app-net
Lancer Nginx et l’app dessus :
docker run -d --name mon-app --network app-net monimage-app
docker run -d --name nginx-proxy --network app-net -p 80:80 -p 443:443 nginx:alpine
Connecter un conteneur existant :
docker network connect app-net nginx-proxy
docker network connect app-net mon-app
6) Config Nginx reverse proxy robuste (HTTP)
Voici une base solide pour proxifier une app HTTP (ex : mon-app:3000) :
server {
listen 80;
server_name exemple.com;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log warn;
location / {
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_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
proxy_pass http://mon-app:3000;
}
}
6.1 Vérifier la config et recharger
Dans le conteneur Nginx :
nginx -t
nginx -s reload
Si Nginx est PID 1 dans Docker, un reload peut aussi se faire via signal :
docker kill -s HUP nginx-proxy
7) Diagnostiquer un 502 avec les logs Nginx (et les bons messages)
Les logs d’erreur Nginx donnent souvent la cause exacte.
Afficher les logs :
docker logs -f nginx-proxy
Ou directement dans le conteneur :
docker exec -it nginx-proxy sh -c "tail -n 200 /var/log/nginx/error.log"
Messages typiques et interprétation :
connect() failed (111: Connection refused) while connecting to upstream- l’upstream n’écoute pas, mauvais port, app down, app écoute sur 127.0.0.1
host not found in upstream "mon-app"- DNS Docker : conteneurs pas sur le même réseau, nom incorrect
upstream prematurely closed connection while reading response header from upstream- app crash, timeout côté app, taille headers, bug applicatif, proxy buffers
SSL_do_handshake() failed ...(si upstream en HTTPS)- problème TLS entre Nginx et upstream (cert, SNI, protocole)
8) 504 : comprendre les timeouts Nginx vs timeouts applicatifs
8.1 Les principaux timeouts Nginx à connaître
proxy_connect_timeout: temps pour établir la connexion TCP vers l’upstreamproxy_read_timeout: temps max entre deux lectures de réponseproxy_send_timeout: temps max entre deux envois vers l’upstreamsend_timeout: côté client
Un 504 survient souvent quand proxy_read_timeout est trop bas pour une requête longue.
Exemple d’augmentation raisonnable :
location /api/long {
proxy_connect_timeout 10s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_pass http://mon-app:3000;
}
8.2 Attention : augmenter les timeouts ne “répare” pas une app bloquée
Si l’app est lente à cause de :
- requêtes DB non indexées,
- pool de connexions saturé,
- CPU bridé,
- GC,
- deadlocks,
alors augmenter les timeouts masque le symptôme. Il faut aussi profiler l’app et sa DB.
8.3 Vérifier si l’app répond vite depuis Nginx
Depuis le conteneur Nginx :
docker exec -it nginx-proxy sh -c "time wget -qO- http://mon-app:3000/health"
Ou avec curl (si présent) :
docker exec -it nginx-proxy sh -c "curl -sS -o /dev/null -w '%{http_code} %{time_total}\n' http://mon-app:3000/health"
9) Pièges SSL/TLS côté client (Nginx termine TLS)
9.1 Terminaison TLS : Nginx en 443, upstream en HTTP
Cas le plus courant : Nginx gère les certificats, et parle en HTTP interne à mon-app.
Exemple complet (redirection HTTP → HTTPS + serveur TLS) :
server {
listen 80;
server_name exemple.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name exemple.com;
ssl_certificate /etc/nginx/certs/exemple.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/exemple.com/privkey.pem;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://mon-app:3000;
}
}
9.2 Chaîne de certificats incomplète : erreurs navigateur
Si vous servez uniquement cert.pem au lieu de fullchain.pem, certains clients échoueront.
Tester depuis votre machine :
openssl s_client -connect exemple.com:443 -servername exemple.com -showcerts </dev/null
Vous devez voir une chaîne cohérente. Si le navigateur indique “certificat non valide” ou “chaîne incomplète”, utilisez le fichier fullchain.pem.
9.3 Permissions et montage de volumes
Si Nginx ne peut pas lire les fichiers :
- Dans les logs :
cannot load certificate ... BIO_new_file() failed - Vérifiez le montage Docker et les permissions.
Exemple de test dans le conteneur :
docker exec -it nginx-proxy sh -c "ls -l /etc/nginx/certs && head -n 2 /etc/nginx/certs/exemple.com/fullchain.pem"
10) Pièges SSL/TLS côté upstream (Nginx parle en HTTPS à l’app)
Parfois l’upstream est lui-même en HTTPS (ex : service interne avec TLS). Vous faites alors :
proxy_pass https://mon-app:8443;
10.1 Problème de SNI (Server Name Indication)
Si le certificat de l’upstream dépend du nom, Nginx doit envoyer SNI :
location / {
proxy_ssl_server_name on;
proxy_ssl_name mon-app;
proxy_pass https://mon-app:8443;
}
10.2 Certificat auto-signé : validation échoue
Par défaut, Nginx valide le certificat upstream si vous activez certaines options, mais selon config, vous pouvez rencontrer :
SSL certificate verify failed
Solutions :
- Fournir une CA interne et configurer
proxy_ssl_trusted_certificate - Ou désactiver la vérification (à éviter en prod) :
proxy_ssl_verify off;
Approche plus propre (CA interne) :
proxy_ssl_trusted_certificate /etc/nginx/ca/internal-ca.pem;
proxy_ssl_verify on;
proxy_ssl_verify_depth 2;
Tester la connexion TLS depuis le conteneur Nginx :
docker exec -it nginx-proxy sh -c "openssl s_client -connect mon-app:8443 -servername mon-app </dev/null | head -n 40"
11) Boucles de redirection HTTP↔HTTPS (souvent liées à X-Forwarded-Proto)
11.1 Symptôme
Vous accédez à https://exemple.com et le navigateur boucle (trop de redirections). Souvent l’app redirige vers HTTPS parce qu’elle croit être en HTTP, ou inversement.
11.2 Cause
L’app est derrière Nginx (TLS terminé à Nginx), mais elle ne sait pas que le client est en HTTPS. Il faut transmettre :
X-Forwarded-Proto- parfois
X-Forwarded-Host - et configurer l’app pour faire confiance au proxy (selon framework)
Config Nginx :
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Côté app :
- Express :
app.set('trust proxy', true) - Django :
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - Rails : config reverse proxy /
config.force_ssl+ trusted proxies
12) WebSocket : 502/400 si headers Upgrade manquants
Les WebSockets nécessitent Upgrade et Connection.
Exemple Nginx :
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name exemple.com;
ssl_certificate /etc/nginx/certs/exemple.com/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/exemple.com/privkey.pem;
location /ws/ {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_pass http://mon-app:3000;
}
location / {
proxy_pass http://mon-app:3000;
}
}
Tester côté client :
curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" https://exemple.com/ws/
Ou utilisez un outil WS (wscat) depuis une machine de test.
13) Gros uploads, streaming, SSE : buffers et timeouts
13.1 Uploads : client_max_body_size
Si vous voyez 413 Request Entity Too Large, ce n’est pas 502/504, mais c’est fréquent en reverse proxy.
server {
client_max_body_size 50m;
}
13.2 Streaming / SSE : bufferisation indésirable
Pour SSE, désactivez le buffering :
location /events/ {
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 3600s;
proxy_pass http://mon-app:3000;
}
14) Erreurs liées aux ports : port exposé vs port interne
14.1 Rappel
-p 8080:80publie un port hôte → conteneur- Entre conteneurs sur un même réseau, on utilise le port interne du conteneur.
Exemple : votre app écoute sur 3000 dans le conteneur.
Même si vous publiez -p 13000:3000, Nginx doit utiliser 3000 via le réseau Docker :
proxy_pass http://mon-app:3000;
14.2 Vérifier le port interne
Dans le conteneur app :
docker exec -it mon-app sh -c "ss -lntp | head -n 20"
15) Méthode de diagnostic systématique (checklist)
Étape A : vérifier que l’upstream est “vivant”
Depuis l’hôte :
docker ps
docker logs --tail=200 mon-app
Cherchez crash, redémarrages, erreurs de binding.
Étape B : vérifier le réseau Docker
- même réseau ?
- nom correct ?
docker network inspect app-net | sed -n '1,200p'
Étape C : tester depuis le conteneur Nginx vers l’upstream
docker exec -it nginx-proxy sh
# puis :
getent hosts mon-app
nc -vz mon-app 3000
wget -S -O- http://mon-app:3000/ 2>&1 | head -n 50
Étape D : lire les logs Nginx
docker logs --tail=200 nginx-proxy
Étape E : valider la config Nginx
docker exec -it nginx-proxy nginx -t
Étape F : valider TLS depuis l’extérieur
openssl s_client -connect exemple.com:443 -servername exemple.com </dev/null | head -n 60
Étape G : si 504, mesurer les temps
- temps Nginx,
- temps app,
- latence DB.
Test rapide :
curl -sS -o /dev/null -w 'code=%{http_code} t=%{time_total}\n' https://exemple.com/api/health
16) Exemple complet : Nginx dans Docker + app + réseau dédié
16.1 Lancer une app de test (ex : http-echo)
Pour simuler un upstream simple :
docker network create app-net
docker run -d --name echo --network app-net --rm hashicorp/http-echo \
-listen=:5678 -text="OK depuis upstream"
16.2 Lancer Nginx avec une config montée
Créez un fichier default.conf sur l’hôte :
server {
listen 80;
server_name _;
location / {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://echo:5678;
}
}
Lancez Nginx :
docker run -d --name nginx-proxy --network app-net -p 8080:80 \
-v "$PWD/default.conf":/etc/nginx/conf.d/default.conf:ro \
nginx:alpine
Test :
curl -i http://localhost:8080/
Si vous obtenez OK depuis upstream, votre chaîne Docker DNS + Nginx fonctionne.
17) Cas réels et corrections rapides
Cas 1 : host not found in upstream "mon-app"
- conteneurs pas sur le même réseau
- nom de service incorrect
Correctifs :
docker network connect app-net nginx-proxy
docker network connect app-net mon-app
Puis :
docker exec -it nginx-proxy getent hosts mon-app
Cas 2 : connect() failed (111: Connection refused)
- app down
- mauvais port
- app écoute sur 127.0.0.1
Correctifs :
- vérifier port interne :
ss -lntp - écouter sur
0.0.0.0 - corriger
proxy_pass
Cas 3 : 504 sur endpoints longs
- augmenter
proxy_read_timeout - optimiser l’app
- vérifier DB
Correctif Nginx ciblé sur un chemin :
location /export/ {
proxy_read_timeout 600s;
proxy_send_timeout 600s;
proxy_pass http://mon-app:3000;
}
Cas 4 : boucle HTTPS
- ajouter
X-Forwarded-Proto - configurer l’app pour faire confiance au proxy
Nginx :
proxy_set_header X-Forwarded-Proto $scheme;
Express :
app.set('trust proxy', true);
Cas 5 : WebSocket KO
- ajouter headers Upgrade/Connection
proxy_http_version 1.1
18) Bonnes pratiques de durcissement et observabilité
18.1 Définir des logs utiles
Ajoutez un format de log enrichi :
log_format main_ext '$remote_addr - $host [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'ua="$http_user_agent" '
'up="$upstream_addr"';
access_log /var/log/nginx/access.log main_ext;
$upstream_response_time est très utile pour distinguer :
- latence Nginx ↔ upstream
- latence applicative
18.2 Healthchecks
Ajoutez un endpoint /health côté app et testez-le depuis Nginx (ou depuis un job externe). Cela réduit les “502 surprises” après déploiement.
18.3 Limiter les effets de panne upstream
Utilisez une définition upstream et éventuellement plusieurs backends :
upstream app_backend {
server mon-app:3000 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
listen 80;
server_name _;
location / {
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://app_backend;
}
}
Conclusion
Les 502 et 504 avec Nginx en reverse proxy dans Docker ne sont pas des “mystères” : ce sont presque toujours des problèmes de résolution DNS, de réseau Docker, de port interne, d’application qui n’écoute pas sur 0.0.0.0, ou de timeouts mal dimensionnés. Les pièges SSL/TLS ajoutent une couche : chaîne de certificats, SNI, headers X-Forwarded-*, WebSockets, et redirections.
La clé est de diagnostiquer par couches :
- Réseau Docker (même réseau, DNS OK)
- Connectivité TCP (nc)
- HTTP direct depuis Nginx vers l’upstream (wget/curl)
- Logs Nginx (messages explicites)
- Timeouts et buffering
- TLS (openssl s_client, fullchain, SNI)
Si vous décrivez votre topologie (services, ports internes, extrait de config Nginx, logs d’erreur), je peux vous proposer une correction précise et minimale adaptée à votre cas.