← Retour aux tutoriels

Corriger l’erreur Nginx 502 Bad Gateway avec Docker : causes et solutions

nginxdocker502-bad-gatewayreverse-proxydevopstroubleshootingdocker-composenetworking

Corriger l’erreur Nginx 502 Bad Gateway avec Docker : causes et solutions

L’erreur 502 Bad Gateway avec Nginx dans un environnement Docker est l’un des problèmes les plus fréquents dès qu’on met en place un reverse proxy (Nginx) devant une application (Node.js, PHP-FPM, Python, Go, etc.). Elle signifie, dans la majorité des cas, que Nginx n’arrive pas à obtenir une réponse valide de l’upstream (le service derrière), ou que la réponse est invalide/rompue.

Ce tutoriel explique en profondeur les causes réelles, comment diagnostiquer rapidement, et comment corriger durablement avec des exemples concrets (Docker, Docker Compose, commandes et configurations Nginx).


1) Comprendre ce que signifie “502 Bad Gateway” dans Nginx

Nginx agit souvent comme reverse proxy :

Une 502 indique que Nginx a bien reçu la requête du client, mais qu’il n’a pas pu obtenir une réponse correcte de l’upstream.

Différences utiles : 502 vs 504 vs 503


2) Les causes les plus courantes en Docker

Dans Docker, les 502 viennent très souvent de :

  1. Mauvais nom d’hôte upstream (Nginx pointe vers localhost au lieu du service Docker).
  2. Mauvais port (Nginx proxy vers 3000 alors que l’app écoute 8080).
  3. L’application écoute sur 127.0.0.1 au lieu de 0.0.0.0 dans le conteneur.
  4. Le service n’est pas prêt (Nginx démarre avant l’app, ou l’app met du temps à boot).
  5. Réseaux Docker mal configurés (conteneurs pas sur le même réseau).
  6. Protocole erroné (HTTP vs FastCGI vs HTTPS en amont).
  7. TLS en amont (proxy vers un upstream HTTPS sans proxy_ssl_*).
  8. Limites/ressources : OOMKill, crash, redémarrages, saturation, file descriptors.
  9. Résolution DNS interne (Nginx résout une fois au démarrage, IP change après restart).
  10. Timeouts/buffers (réponses trop grosses, headers trop gros, timeouts trop courts).

3) Méthode de diagnostic rapide (checklist)

L’objectif : déterminer si c’est un problème réseau, port, app, Nginx, ou timing.

3.1 Lire les logs Nginx (indispensable)

Dans un setup Docker Compose :

docker compose logs -f nginx

Si vous utilisez docker directement :

docker logs -f <container_nginx>

Cherchez des messages typiques :

3.2 Vérifier l’état des conteneurs

docker compose ps
docker compose top

Puis inspecter les redémarrages :

docker inspect -f '{{.Name}} RestartCount={{.RestartCount}} OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' $(docker compose ps -q)

Si l’app crash en boucle, Nginx aura des 502 intermittentes.

3.3 Tester la connectivité depuis le conteneur Nginx

Entrez dans Nginx :

docker compose exec nginx sh

Puis testez l’upstream (ex: service app sur port 3000) :

apk add --no-cache curl 2>/dev/null || true
curl -v http://app:3000/

Si curl échoue, le problème est avant Nginx (réseau, DNS, port, app non prête).

3.4 Vérifier la résolution DNS Docker

Toujours dans le conteneur Nginx :

getent hosts app
nslookup app 2>/dev/null || true

Si getent hosts ne renvoie rien, Nginx ne peut pas résoudre le service (réseau/nom).

3.5 Vérifier sur quel port l’application écoute réellement

Dans le conteneur de l’app :

docker compose exec app sh

Puis :

ss -lntp
# ou
netstat -lntp 2>/dev/null || true

Vous devez voir quelque chose comme LISTEN 0.0.0.0:3000 (ou le port attendu).


4) Erreur classique n°1 : utiliser localhost au lieu du nom de service Docker

Symptôme

Dans nginx.conf :

proxy_pass http://localhost:3000;

Dans un conteneur, localhost pointe vers le conteneur Nginx lui-même, pas vers votre application.

Correction

Utilisez le nom du service Docker Compose (ex: app) :

location / {
  proxy_pass http://app:3000;
}

Assurez-vous que Nginx et app sont sur le même réseau Docker (Compose le fait par défaut si dans le même fichier).


5) Erreur classique n°2 : l’application écoute sur 127.0.0.1 au lieu de 0.0.0.0

Pourquoi ça casse ?

Dans un conteneur, si l’app écoute uniquement sur 127.0.0.1, elle n’accepte que les connexions venant d’elle-même. Nginx, depuis un autre conteneur, ne pourra pas se connecter.

Exemple Node.js (Express)

Mauvais :

app.listen(3000, "127.0.0.1");

Bon :

app.listen(3000, "0.0.0.0");

Exemple Python (Flask)

Mauvais :

flask run --host=127.0.0.1 --port=5000

Bon :

flask run --host=0.0.0.0 --port=5000

Vérification

Dans le conteneur app :

ss -lntp | grep 3000

Vous voulez voir 0.0.0.0:3000 (ou :::3000 pour IPv6).


6) Erreur classique n°3 : mauvais port (EXPOSE ≠ port réellement écouté)

Dans Docker, EXPOSE est informatif. Le port qui compte, c’est celui sur lequel le processus écoute.

Diagnostic

Vérifiez :

docker compose exec app ss -lntp

Corrigez soit :


7) Erreur classique n°4 : Nginx démarre avant l’application (service pas prêt)

depends_on dans Docker Compose n’attend pas que l’app soit prête, seulement qu’elle soit démarrée.

Symptômes

Solutions

Option A : healthcheck + attente côté Nginx (recommandé)

Exemple docker-compose.yml (extrait) :

services:
  app:
    build: ./app
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 5s
      timeout: 2s
      retries: 20

  nginx:
    image: nginx:alpine
    depends_on:
      app:
        condition: service_healthy

Selon la version de Compose, condition: service_healthy peut ne pas être supporté partout. Si c’est votre cas, utilisez une attente dans l’entrypoint Nginx.

Option B : script d’attente (wait-for)

Dans l’image Nginx, vous pouvez lancer un script qui attend app:3000 :

#!/bin/sh
set -e

until nc -z app 3000; do
  echo "En attente de app:3000..."
  sleep 1
done

nginx -g 'daemon off;'

Puis Dockerfile Nginx (exemple) :

FROM nginx:alpine
RUN apk add --no-cache netcat-openbsd
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENTRYPOINT ["/docker-entrypoint.sh"]

8) Erreur classique n°5 : Nginx et l’app ne sont pas sur le même réseau

Symptôme

Diagnostic

Listez les réseaux :

docker network ls
docker compose ps

Inspectez le réseau :

docker network inspect <nom_du_reseau>

Correction (Compose)

Assurez-vous que les services partagent un réseau :

services:
  app:
    networks: [webnet]
  nginx:
    networks: [webnet]

networks:
  webnet:

9) Cas PHP-FPM : confusion entre HTTP et FastCGI

Si vous utilisez PHP-FPM, Nginx ne doit pas faire proxy_pass (HTTP), mais fastcgi_pass.

Mauvais (entraîne 502/invalid response)

location ~ \.php$ {
  proxy_pass http://php-fpm:9000;
}

Bon

location ~ \.php$ {
  include fastcgi_params;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  fastcgi_pass php-fpm:9000;
}

Diagnostic côté logs Nginx

Vous pouvez voir :


10) Cas HTTPS en amont : proxy vers un upstream TLS

Si votre upstream parle HTTPS (ex: https://app:8443), Nginx doit utiliser proxy_pass https://... et parfois ajuster la validation TLS.

Exemple

location / {
  proxy_pass https://app:8443;
  proxy_ssl_server_name on;
  proxy_ssl_verify off; # à éviter en prod, préférez un CA correct
}

Diagnostic

Testez depuis Nginx :

curl -vk https://app:8443/

Si le handshake échoue, Nginx aura des 502.


11) Problèmes de DNS Docker et Nginx : résolution au démarrage

Nginx résout souvent les noms DNS au démarrage et garde l’IP. Si le conteneur upstream redémarre et change d’IP, Nginx peut continuer à viser l’ancienne IP → 502 intermittentes.

Solution : utiliser resolver et variables (ou upstream + resolve)

Approche simple : définir un resolver DNS Docker (souvent 127.0.0.11) :

resolver 127.0.0.11 valid=10s;

set $upstream "app:3000";

location / {
  proxy_pass http://$upstream;
}

Autre approche avec upstream (selon version Nginx, resolve peut être disponible) :

resolver 127.0.0.11 valid=10s;

upstream backend {
  server app:3000;
}

location / {
  proxy_pass http://backend;
}

Vérification

Redémarrez l’app et observez si Nginx récupère correctement :

docker compose restart app
docker compose logs -f nginx

12) Timeouts : quand 502 ressemble à un 504 (et inversement)

Certaines erreurs côté upstream peuvent se manifester en 502 si la connexion est coupée brutalement.

Paramètres Nginx utiles

Dans location :

proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

proxy_buffering on;
proxy_buffers 16 16k;
proxy_busy_buffers_size 32k;

Si vous avez des requêtes longues (export, génération PDF, etc.), augmentez proxy_read_timeout.

Diagnostic

Dans les logs Nginx :


13) Réponses trop grosses / headers trop gros

Certaines apps renvoient des headers volumineux (cookies, JWT, etc.). Nginx peut échouer et produire des erreurs.

Indices dans les logs

Correction

Augmentez :

proxy_buffer_size 16k;
proxy_buffers 32 16k;
proxy_busy_buffers_size 64k;

Pour les en-têtes client :

large_client_header_buffers 4 16k;

14) Problèmes de ressources : OOMKill, CPU, limites de fichiers

Si l’upstream se fait tuer (OOM) ou redémarre, Nginx verra des resets et renverra 502.

Vérifier OOMKill

docker inspect <container_app> --format '{{.State.OOMKilled}}'
dmesg | tail -n 50 2>/dev/null || true

Surveiller la conso

docker stats

Augmenter les limites (exemple Compose)

services:
  app:
    deploy:
      resources:
        limits:
          memory: 512M

Note : deploy est surtout pour Swarm. En Compose classique, utilisez plutôt des options selon votre runtime, ou configurez Docker Desktop/daemon.


15) Exemple complet : Nginx + app Node.js avec Docker Compose

15.1 Arborescence

.
├─ docker-compose.yml
├─ nginx/
│  └─ default.conf
└─ app/
   ├─ Dockerfile
   ├─ package.json
   └─ server.js

15.2 Application Node.js (exemple)

app/server.js :

import http from "http";

const port = process.env.PORT || 3000;

const server = http.createServer((req, res) => {
  if (req.url === "/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ ok: true }));
    return;
  }

  res.writeHead(200, { "Content-Type": "text/plain" });
  res.end("Hello via Nginx + Docker\n");
});

server.listen(port, "0.0.0.0", () => {
  console.log(`Listening on 0.0.0.0:${port}`);
});

app/Dockerfile :

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci --omit=dev || npm install --omit=dev
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]

15.3 Configuration Nginx

nginx/default.conf :

server {
  listen 80;
  server_name _;

  access_log /var/log/nginx/access.log;
  error_log  /var/log/nginx/error.log warn;

  location / {
    proxy_pass http://app:3000;

    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_read_timeout 60s;
  }
}

15.4 Docker Compose

docker-compose.yml :

services:
  app:
    build: ./app
    restart: unless-stopped

  nginx:
    image: nginx:1.27-alpine
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      - ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - app

15.5 Lancement

docker compose up -d --build
curl -v http://localhost:8080/
curl -v http://localhost:8080/health

Si vous obtenez une 502, appliquez immédiatement :


16) Interpréter les messages d’erreur Nginx (table de correspondance)

connect() failed (111: Connection refused)

Commandes :

docker compose exec app ss -lntp
docker compose logs -f app

host not found in upstream

Commandes :

docker compose ps
docker compose exec nginx getent hosts app

upstream timed out

Commandes :

docker stats
docker compose exec nginx tail -n 200 /var/log/nginx/error.log

upstream sent invalid header

Commandes :

docker compose exec nginx curl -v http://app:3000/

17) Bonnes pratiques pour éviter les 502 en production

  1. Toujours proxy vers le nom de service Docker, jamais localhost.
  2. Rendre l’app accessible sur 0.0.0.0.
  3. Mettre un endpoint /health et l’utiliser pour des healthchecks.
  4. Configurer des timeouts adaptés (connexion, lecture).
  5. Stabiliser la résolution DNS (resolver Docker si nécessaire).
  6. Limiter les redémarrages en boucle : logs, monitoring, ressources.
  7. Activer des logs utiles et les centraliser (stdout/stderr, ou volumes).
  8. Tester depuis le conteneur Nginx (curl interne) avant d’accuser Nginx.

18) Procédure “anti-panique” en 60 secondes

Si vous avez une 502 maintenant, exécutez dans cet ordre :

  1. Logs Nginx :
docker compose logs --tail=200 nginx
  1. Voir si l’app tourne :
docker compose ps
docker compose logs --tail=200 app
  1. Tester l’upstream depuis Nginx :
docker compose exec nginx sh -c "curl -v http://app:3000/health"
  1. Vérifier l’écoute :
docker compose exec app sh -c "ss -lntp"
  1. Corriger :

Conclusion

L’erreur Nginx 502 Bad Gateway avec Docker n’est presque jamais “mystérieuse” : elle pointe vers un problème de connectivité, de résolution, de port, de protocole, ou de disponibilité de l’upstream. La clé est d’adopter une démarche systématique :

Si vous décrivez votre stack (Compose, default.conf, logs Nginx + logs app), je peux vous proposer une correction précise et minimale adaptée à votre cas.