← Retour aux tutoriels

Résoudre les problèmes de reverse proxy Nginx dans Docker : 502, 504 et pièges SSL

nginxdockerreverse-proxydevops502504ssl-tlstroubleshooting

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 :

En Docker, les causes courantes sont :

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 :

Dans Docker, cela peut être aggravé par :


2) Cartographier l’architecture : ports, réseaux, flux

Avant de toucher à Nginx, clarifiez :

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

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 :


8) 504 : comprendre les timeouts Nginx vs timeouts applicatifs

8.1 Les principaux timeouts Nginx à connaître

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 :

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 :

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 :

Solutions :

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 :

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 :


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

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

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

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"

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)

Correctifs :

Cas 3 : 504 sur endpoints longs

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

Nginx :

proxy_set_header X-Forwarded-Proto $scheme;

Express :

app.set('trust proxy', true);

Cas 5 : WebSocket KO


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 :

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 :

  1. Réseau Docker (même réseau, DNS OK)
  2. Connectivité TCP (nc)
  3. HTTP direct depuis Nginx vers l’upstream (wget/curl)
  4. Logs Nginx (messages explicites)
  5. Timeouts et buffering
  6. 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.