Nginx 502 Bad Gateway oplossen met Docker (DevOps gids)
Een 502 Bad Gateway van Nginx betekent vrijwel altijd: Nginx (reverse proxy) kan geen geldige response krijgen van de upstream (je app, API, PHP-FPM, Node, Gunicorn, uWSGI, enz.). In een Docker-setup komt daar extra complexiteit bij: netwerken, DNS-resolutie binnen Compose, healthchecks, container lifecycle, poortmappings, timeouts, en misconfiguraties die lokaal “toevallig” werken maar in containers falen.
Deze gids is een praktische DevOps-handleiding om systematisch 502’s te debuggen en op te lossen. Je krijgt diepere uitleg, concrete commando’s, en voorbeeldconfiguraties voor Docker + Nginx.
Inhoud
- Wat betekent 502 in Nginx precies?
- De snelste triage: waar komt de 502 vandaan?
- Basis: Nginx als reverse proxy in Docker
- Loganalyse: Nginx error log, access log, upstream details
- Netwerk en DNS in Docker: service names, networks, resolvers
- Veelvoorkomende oorzaken + oplossingen (met echte fixes)
- Timeouts, buffering en grote responses
- HTTP vs HTTPS upstream, headers en websockets
- Healthchecks, depends_on en “app is nog niet klaar”
- Debug-tooling in containers: curl, dig, ss, tcpdump
- Productie-hardening: retries, observability, best practices
- Checklist: 502 in Docker in 5 minuten isoleren
Wat betekent 502 in Nginx precies?
Nginx treedt in veel setups op als reverse proxy: hij ontvangt HTTP(S)-requests van clients en stuurt ze door naar een upstream (bijv. app:3000, php-fpm:9000, gunicorn:8000). Een 502 Bad Gateway betekent dat Nginx wel een upstream probeerde te bereiken, maar:
- geen verbinding kon maken (connection refused / no route / DNS fail),
- een verbinding had maar geen geldige HTTP-response kreeg,
- de upstream de verbinding voortijdig sloot,
- of Nginx een protocol mismatch zag (bijv. HTTPS naar HTTP upstream).
Belangrijk: 502 is niet hetzelfde als 504.
- 502: upstream is “kapot” of antwoord is ongeldig.
- 504 Gateway Timeout: upstream reageert te laat (timeout).
In Docker zie je 502 vaak door:
- verkeerde hostnaam (service name mismatch),
- verkeerde poort (container luistert intern op andere poort),
- app luistert alleen op
127.0.0.1in plaats van0.0.0.0, - Nginx in ander netwerk dan de app,
- upstream crasht of is nog aan het starten,
- DNS caching issues in Nginx bij dynamische containers.
De snelste triage: waar komt de 502 vandaan?
Voordat je config gaat aanpassen: bepaal of Nginx zelf faalt of dat je upstream faalt.
1) Check Nginx container status
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
Als Nginx steeds herstart, zit je probleem mogelijk in Nginx config (dan is het vaak 500/503 of geen response, maar kan ook 502 veroorzaken via load balancer).
2) Bekijk Nginx logs
docker logs --tail=200 -f nginx
Zoek naar regels zoals:
connect() failed (111: Connection refused) while connecting to upstreamhost not found in upstreamupstream prematurely closed connectionno live upstreamsSSL_do_handshake() failed
Deze tekst is vaak direct de oorzaak.
3) Test upstream vanaf Nginx container
Ga in de Nginx container en probeer de upstream te bereiken:
docker exec -it nginx sh
# of bash als beschikbaar
Installeer eventueel tools (afhankelijk van image):
Alpine (nginx:alpine):
apk add --no-cache curl bind-tools iproute2
Debian/Ubuntu:
apt-get update && apt-get install -y curl dnsutils iproute2
Test DNS en HTTP:
getent hosts app
curl -v http://app:3000/health
Als curl faalt, is het upstream-probleem of netwerk/DNS. Als curl werkt maar Nginx geeft 502, is het Nginx-config/protocol/header/timeout.
Basis: Nginx als reverse proxy in Docker
Een veelgebruikte Compose-setup:
nginxpubliceert poort 80/443 naar buitenappdraait intern op 3000- Nginx praat met
http://app:3000
Voorbeeld: docker-compose.yml (conceptueel)
Je vroeg om Markdown-only; hieronder staat een voorbeeldblok dat je direct kunt gebruiken. Pas namen/poorten aan.
services:
nginx:
image: nginx:1.25-alpine
container_name: nginx
ports:
- "8080:80"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d:ro
depends_on:
- app
networks:
- web
app:
image: node:20-alpine
container_name: app
working_dir: /app
volumes:
- ./app:/app
command: sh -c "npm install && node server.js"
expose:
- "3000"
networks:
- web
networks:
web:
driver: bridge
Voorbeeld: Nginx config ./nginx/conf.d/default.conf
server {
listen 80;
server_name _;
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;
proxy_send_timeout 60s;
}
}
Kernprincipe: in Docker Compose is app een DNS-naam binnen het netwerk web. Nginx moet in hetzelfde netwerk zitten om app te resolven en te bereiken.
Loganalyse: Nginx error log, access log, upstream details
Nginx error log live volgen
In veel images logt Nginx naar stdout/stderr, dus:
docker logs -f --tail=200 nginx
Als je een custom config gebruikt met logfiles in de container:
docker exec -it nginx sh -c "tail -n 200 /var/log/nginx/error.log"
docker exec -it nginx sh -c "tail -n 200 /var/log/nginx/access.log"
Extra upstream logging toevoegen
Je kunt meer inzicht krijgen door een custom log_format te gebruiken:
log_format upstreamlog '$remote_addr - $host [$time_local] '
'"$request" $status $body_bytes_sent '
'rt=$request_time urt=$upstream_response_time '
'ua="$http_user_agent" '
'upstream="$upstream_addr" ustatus="$upstream_status"';
access_log /var/log/nginx/access.log upstreamlog;
Dit helpt bij vragen als:
- Welke upstream_addr werd gebruikt?
- Was upstream_response_time leeg (connectie faalde) of heel lang (timeout)?
- Kreeg je upstream_status 502/503/504?
Netwerk en DNS in Docker: service names, networks, resolvers
Service name vs container_name
In Compose is de service name (bijv. app) de stabiele DNS-naam. container_name kan werken, maar is minder flexibel en kan conflicts geven. Gebruik bij voorkeur:
proxy_pass http://app:3000;(service name)- Niet
proxy_pass http://my-hardcoded-container-name:3000;tenzij je weet wat je doet.
Controleer of containers in hetzelfde netwerk zitten
docker network ls
docker network inspect <project>_web
Je wil in de inspect-output beide containers zien onder Containers.
DNS-resolutie testen vanuit Nginx
docker exec -it nginx sh -c "getent hosts app || true"
docker exec -it nginx sh -c "nslookup app || true"
Als getent hosts app niets teruggeeft: Nginx zit niet in hetzelfde netwerk, of de service bestaat niet, of Compose projectnaam klopt niet.
Nginx en DNS caching (belangrijk bij dynamische upstreams)
Nginx resolve’t hostnames vaak eenmalig bij start (afhankelijk van context). Als je upstream IP verandert (container herstart, scaling), kan Nginx blijven wijzen naar een oud IP.
Oplossing: gebruik een resolver en variabele in proxy_pass:
resolver 127.0.0.11 valid=10s ipv6=off;
set $upstream_app app;
proxy_pass http://$upstream_app:3000;
127.0.0.11 is de Docker embedded DNS server binnen containers.
Veelvoorkomende oorzaken + oplossingen (met echte fixes)
Hieronder de meest voorkomende 502-oorzaken in Docker, met diagnose en concrete oplossing.
1) Verkeerde upstream hostnaam (host not found in upstream)
Symptoom in Nginx log:
host not found in upstream "app:3000"
Diagnose:
- Bestaat de service
appin Compose? - Zit Nginx in hetzelfde network?
Fix:
- Gebruik de juiste service name.
- Voeg beide services toe aan hetzelfde
networks:blok.
Commands:
docker exec -it nginx sh -c "getent hosts app"
docker compose ps
2) Verkeerde poort: app luistert intern niet op 3000
Veel mensen verwarren:
ports: "8081:3000"(host:container)- met de interne poort die Nginx moet gebruiken.
Nginx moet altijd naar de containerpoort (rechts) praten, niet naar de hostpoort.
Voorbeeldfout:
- App:
ports: "8081:3000" - Nginx:
proxy_pass http://app:8081;❌ fout
Correct:
proxy_pass http://app:3000;✅
Diagnose: Check in de app-container welke poorten luisteren:
docker exec -it app sh -c "ss -lntp || netstat -lntp"
Test HTTP direct:
docker exec -it nginx sh -c "curl -v http://app:3000/"
3) App luistert alleen op 127.0.0.1 (localhost) in de container
Dit is een klassieke Docker-valkuil. Als je app bindt op 127.0.0.1, is hij alleen bereikbaar binnen dezelfde container, niet vanaf Nginx.
Diagnose: In de app-container:
docker exec -it app sh -c "ss -lntp | grep 3000 || true"
Als je iets ziet als 127.0.0.1:3000, dan is dit het probleem.
Fix (Node/Express voorbeeld):
Zorg dat je luistert op 0.0.0.0:
app.listen(3000, "0.0.0.0");
Fix (Python/uvicorn):
uvicorn main:app --host 0.0.0.0 --port 8000
Fix (Django runserver, alleen dev):
python manage.py runserver 0.0.0.0:8000
4) Upstream crasht of is niet “ready” (race condition bij startup)
Nginx start snel, je app heeft migraties, warming caches, dependency checks. Nginx probeert te proxy’en en krijgt connection refused.
Symptoom:
connect() failed (111: Connection refused) while connecting to upstream
Diagnose: Bekijk app logs:
docker logs --tail=200 -f app
Fix-opties:
- Voeg een health endpoint toe aan je app (
/health). - Gebruik
healthcheckin Compose en laat Nginx pas verkeer krijgen als app healthy is (of gebruik een entrypoint script).
Compose healthcheck voorbeeld:
services:
app:
# ...
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 5s
timeout: 2s
retries: 10
Let op: depends_on met health conditions is in moderne Compose varianten beperkt/anders; vaak is een robuuste oplossing:
- Nginx laten retry’en (zie verderop),
- of een init/wait script.
Nginx retry gedrag verbeteren:
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_next_upstream_tries 5;
proxy_next_upstream_timeout 10s;
5) Protocol mismatch: Nginx praat HTTP met een HTTPS upstream (of andersom)
Symptomen:
SSL_do_handshake() failed- of upstream geeft “400 Bad Request” en Nginx vertaalt naar 502 afhankelijk van situatie
Diagnose: Test met curl vanaf Nginx container:
docker exec -it nginx sh -c "curl -v http://app:3000/"
docker exec -it nginx sh -c "curl -vk https://app:3000/"
Fix:
- Als upstream HTTPS is:
proxy_pass https://app:3000; - Als upstream HTTP is:
proxy_pass http://app:3000;
Bij self-signed certs upstream (intern) kun je tijdelijk:
proxy_ssl_verify off;
Beter is een interne CA of plain HTTP op het interne Docker-netwerk.
6) Nginx config reload/start faalt of verwijst naar niet-bestaande upstream
Soms lijkt het een 502, maar Nginx draait met oude config of start niet goed.
Check config syntax:
docker exec -it nginx nginx -t
Reload:
docker exec -it nginx nginx -s reload
Als nginx -t faalt, los eerst config errors op.
7) PHP-FPM: verkeerd type upstream (FastCGI vs HTTP)
Als je PHP via PHP-FPM draait, gebruik je fastcgi_pass, niet proxy_pass.
Fout (HTTP proxy naar FPM):
location ~ \.php$ {
proxy_pass http://php-fpm:9000; # ❌ fout
}
Correct (FastCGI):
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass php-fpm:9000;
}
Diagnose: Nginx error log toont vaak “invalid response” omdat FPM geen HTTP spreekt.
Timeouts, buffering en grote responses
Soms is upstream “gezond”, maar te traag of stuurt grote headers/bodies waardoor Nginx faalt.
Relevante timeouts
proxy_connect_timeout: tijd om TCP connectie te makenproxy_read_timeout: tijd tussen reads (upstream response)proxy_send_timeout: tijd om request naar upstream te sturen
Voorbeeld:
location / {
proxy_pass http://app:3000;
proxy_connect_timeout 5s;
proxy_read_timeout 120s;
proxy_send_timeout 120s;
}
Grote headers: “upstream sent too big header”
Symptoom in error log:
upstream sent too big header while reading response header from upstream
Fix:
proxy_buffer_size 16k;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
Bij grote cookies/headers (bijv. SSO) kan dit cruciaal zijn.
Streaming / SSE / websockets en buffering
Voor Server-Sent Events of streaming wil je buffering uit:
proxy_buffering off;
proxy_cache off;
HTTP vs HTTPS upstream, headers en websockets
Host en X-Forwarded headers
Veel apps genereren redirects op basis van Host en X-Forwarded-Proto. Zonder die headers kan je app rare redirects doen (bijv. naar http) of verkeerde absolute URLs genereren.
Standaard set:
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
WebSockets (vaak oorzaak van 502 bij upgrade)
Voor websockets moet je Upgrade en Connection correct doorgeven:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 80;
location /ws/ {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}
Zonder dit kan de upstream de verbinding sluiten of Nginx een 502 geven bij upgrade.
Healthchecks, depends_on en “app is nog niet klaar”
Waarom depends_on niet genoeg is
depends_on zorgt in de basis voor startvolgorde, niet voor “ready to serve”. Je app kan draaien maar nog niet luisteren, of nog migraties uitvoeren.
Praktische aanpak
- Health endpoint in de app (
/healthof/ready). - Healthcheck in Compose.
- Nginx configureren met redelijke timeouts + retries.
- Eventueel een “wait-for” script in Nginx entrypoint (minder elegant, soms effectief).
Wait-for voorbeeld (simpel):
# in nginx container entrypoint script
until nc -z app 3000; do
echo "Wachten op app:3000..."
sleep 1
done
nginx -g 'daemon off;'
Hiervoor heb je nc nodig (apk add --no-cache netcat-openbsd).
Debug-tooling in containers: curl, dig, ss, tcpdump
Als je echt vastzit, ga laag in de stack debuggen.
1) DNS: getent, nslookup, dig
docker exec -it nginx sh -c "getent hosts app"
docker exec -it nginx sh -c "nslookup app 127.0.0.11"
2) TCP connectiviteit: nc of /dev/tcp
docker exec -it nginx sh -c "nc -vz app 3000"
Zonder nc (in bash):
docker exec -it nginx bash -lc "echo > /dev/tcp/app/3000"
3) Luisterende sockets: ss
docker exec -it app sh -c "ss -lntp"
docker exec -it nginx sh -c "ss -lntp"
4) Packet capture (alleen als je weet wat je doet)
Soms wil je zien of SYN/SYN-ACK gebeurt:
docker exec -it nginx sh -c "apk add --no-cache tcpdump"
docker exec -it nginx sh -c "tcpdump -i any -nn host app and port 3000"
Let op: in minimal images is tcpdump niet altijd beschikbaar of gewenst in productie.
Productie-hardening: retries, observability, best practices
1) Gebruik upstream blocks en keepalive
upstream app_upstream {
server app:3000;
keepalive 32;
}
server {
listen 80;
location / {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
Keepalive kan performance verbeteren en minder connect-fouten geven bij hoge load.
2) Rate limiting en bescherming tegen overload
Als je upstream onder druk omvalt, kan Nginx 502 geven door crashes. Overweeg rate limiting:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
server {
location /api/ {
limit_req zone=api burst=20 nodelay;
proxy_pass http://app:3000;
}
}
3) Metrics en tracing (praktisch)
- Log upstream response times (
$upstream_response_time) - Voeg request IDs toe:
proxy_set_header X-Request-ID $request_id;
add_header X-Request-ID $request_id;
In je app log je X-Request-ID mee om correlatie te krijgen.
Checklist: 502 in Docker in 5 minuten isoleren
Gebruik deze volgorde om snel te pinpointen waar het misgaat:
-
Logs Nginx
docker logs --tail=200 nginxZoek:
host not found,connection refused,prematurely closed,handshake failed. -
Upstream bereikbaar vanaf Nginx?
docker exec -it nginx sh -c "apk add --no-cache curl >/dev/null 2>&1 || true; curl -v http://app:3000/health" -
DNS klopt?
docker exec -it nginx sh -c "getent hosts app" -
Netwerk klopt?
docker network inspect <project>_web | sed -n '1,200p' -
Luistert de app op 0.0.0.0 en juiste poort?
docker exec -it app sh -c "ss -lntp" -
Protocol mismatch uitsluiten
docker exec -it nginx sh -c "curl -v http://app:3000/ || true; curl -vk https://app:3000/ || true" -
Nginx config validatie
docker exec -it nginx nginx -t docker exec -it nginx nginx -s reload
Concreet scenario: van 502 naar opgelost (voorbeeld)
Stel: je Nginx log toont:
connect() failed (111: Connection refused) while connecting to upstream, upstream: "http://app:3000/"
Stap 1: Test vanuit Nginx container
docker exec -it nginx sh -c "curl -v http://app:3000/"
Output faalt met Connection refused.
Stap 2: Kijk of app luistert op 127.0.0.1
docker exec -it app sh -c "ss -lntp | grep 3000 || true"
Je ziet:
LISTEN 0 511 127.0.0.1:3000 ...
Stap 3: Fix app bind address
Pas je app aan naar 0.0.0.0. Herstart:
docker compose restart app
Stap 4: Retest
docker exec -it nginx sh -c "curl -v http://app:3000/"
curl -v http://localhost:8080/
De 502 is weg.
Veelgemaakte misverstanden (kort maar belangrijk)
-
“Ik kan
localhost:3000bereiken op mijn host, dus Nginx moet ook kunnen.”
Nginx in een container heeft zijn eigen network namespace.localhostin Nginx is Nginx zelf, niet je host en niet je app-container. -
“Ik heb
ports:gezet op de app, dus intern is hij bereikbaar op die hostpoort.”
Intern gebruik je de containerpoort.portsis voor host-to-container, niet container-to-container. -
“Nginx resolve’t service names altijd dynamisch.”
Niet per se. Overweegresolver 127.0.0.11+ variabeleproxy_passals containers vaak wisselen.
Afsluiting
Een Nginx 502 in Docker is zelden “mysterieus” als je het terugbrengt tot drie vragen:
- Kan Nginx de upstream DNS-resolven?
- Kan Nginx een TCP-verbinding maken naar de juiste host:poort?
- Spreken Nginx en upstream hetzelfde protocol en voldoet de upstream aan de verwachtingen (headers/timeouts/upgrade)?
Met de commando’s en fixes uit deze gids kun je in de meeste omgevingen binnen minuten de root cause vinden en structureel oplossen.
Als je je Nginx default.conf, je Compose file, en de relevante error logregel(s) deelt (geanonimiseerd), kan ik de diagnose nog specifieker maken en exact aanwijzen welke regel aangepast moet worden.