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 :
- Le client (navigateur) parle à Nginx (port 80/443).
- Nginx transmet la requête à un service “upstream” (ex:
app:3000,php-fpm:9000, etc.). - Nginx renvoie la réponse au client.
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
- 502 Bad Gateway : upstream injoignable, reset de connexion, réponse invalide, mauvaise config, protocole incorrect, etc.
- 504 Gateway Timeout : l’upstream est joignable mais ne répond pas assez vite (timeout).
- 503 Service Unavailable : souvent lié à la surcharge, maintenance, ou upstream explicitement indisponible.
2) Les causes les plus courantes en Docker
Dans Docker, les 502 viennent très souvent de :
- Mauvais nom d’hôte upstream (Nginx pointe vers
localhostau lieu du service Docker). - Mauvais port (Nginx proxy vers
3000alors que l’app écoute8080). - L’application écoute sur
127.0.0.1au lieu de0.0.0.0dans le conteneur. - Le service n’est pas prêt (Nginx démarre avant l’app, ou l’app met du temps à boot).
- Réseaux Docker mal configurés (conteneurs pas sur le même réseau).
- Protocole erroné (HTTP vs FastCGI vs HTTPS en amont).
- TLS en amont (proxy vers un upstream HTTPS sans
proxy_ssl_*). - Limites/ressources : OOMKill, crash, redémarrages, saturation, file descriptors.
- Résolution DNS interne (Nginx résout une fois au démarrage, IP change après restart).
- 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 :
connect() failed (111: Connection refused) while connecting to upstreamno live upstreamsupstream timed out (110: Connection timed out)host not found in upstreamrecv() failed (104: Connection reset by peer)upstream sent invalid header
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
- Dans Nginx :
proxy_pass http://app:3000; - Mais l’app écoute sur
8080→ 502.
Vérifiez :
docker compose exec app ss -lntp
Corrigez soit :
- la config Nginx (
proxy_pass http://app:8080;) - soit la config de l’app pour écouter sur
3000.
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
- 502 au démarrage, puis ça marche après quelques secondes/minutes.
- Logs Nginx :
connect() failed (111: Connection refused).
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
host not found in upstream "app"ouno route to host.
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 :
upstream sent no valid HTTP/1.0 header- ou
invalid header.
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 :
upstream timed out (110: Connection timed out)→ plutôt 504, mais selon contexte peut produire 502.
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
upstream sent too big header while reading response header from upstream
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 :
docker compose logs -f nginxdocker compose exec nginx sh -c "curl -v http://app:3000/health"
16) Interpréter les messages d’erreur Nginx (table de correspondance)
connect() failed (111: Connection refused)
- L’IP/port répond, mais aucun service n’écoute.
- Causes : app down, mauvais port, app écoute sur 127.0.0.1, app pas prête.
Commandes :
docker compose exec app ss -lntp
docker compose logs -f app
host not found in upstream
- Nom DNS introuvable.
- Causes : mauvais nom de service, réseaux séparés, conteneur absent.
Commandes :
docker compose ps
docker compose exec nginx getent hosts app
upstream timed out
- L’upstream ne répond pas assez vite.
- Causes : requête longue, deadlock, DB lente, CPU saturé, timeouts trop courts.
Commandes :
docker stats
docker compose exec nginx tail -n 200 /var/log/nginx/error.log
upstream sent invalid header
- Protocole incorrect (ex: FastCGI vs HTTP), ou application qui écrit une réponse malformée.
Commandes :
docker compose exec nginx curl -v http://app:3000/
17) Bonnes pratiques pour éviter les 502 en production
- Toujours proxy vers le nom de service Docker, jamais
localhost. - Rendre l’app accessible sur
0.0.0.0. - Mettre un endpoint
/healthet l’utiliser pour des healthchecks. - Configurer des timeouts adaptés (connexion, lecture).
- Stabiliser la résolution DNS (resolver Docker si nécessaire).
- Limiter les redémarrages en boucle : logs, monitoring, ressources.
- Activer des logs utiles et les centraliser (stdout/stderr, ou volumes).
- 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 :
- Logs Nginx :
docker compose logs --tail=200 nginx
- Voir si l’app tourne :
docker compose ps
docker compose logs --tail=200 app
- Tester l’upstream depuis Nginx :
docker compose exec nginx sh -c "curl -v http://app:3000/health"
- Vérifier l’écoute :
docker compose exec app sh -c "ss -lntp"
- Corriger :
proxy_pass http://app:PORT;- app sur
0.0.0.0 - même réseau
- healthcheck/attente si démarrage lent
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 :
- lire les logs Nginx,
- tester l’upstream depuis le conteneur Nginx,
- vérifier l’écoute réelle de l’application,
- corriger la configuration (nom de service, port, réseau, protocole),
- rendre le démarrage robuste (healthchecks, timeouts, DNS).
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.