Reverse proxy-problemen oplossen met Nginx in Docker: 502/504 en SSL-valkuilen
Deze tutorial is een praktische, diepgaande gids om reverse proxy-problemen met Nginx in Docker te diagnosticeren en op te lossen. De focus ligt op de meest voorkomende fouten in productie: 502 Bad Gateway, 504 Gateway Timeout, en SSL/TLS-valkuilen (certificaten, SNI, chain issues, redirect loops, en “mixed content”-achtige symptomen). Je krijgt concrete commando’s, voorbeeldconfiguraties, en een systematische aanpak die je kunt herhalen bij elk incident.
Inhoud
- Context: wat doet Nginx als reverse proxy in Docker?
- Snelstart: een minimale werkende setup
- Begrijp 502 vs 504: wat betekent het echt?
- Diagnoseflow: van buiten naar binnen
- Nginx-logs en Docker: waar kijk je?
- Netwerkproblemen in Docker: DNS, networks, poorten
- Upstream-problemen: app luistert niet, verkeerde poort, crashloops
- Timeouts en buffering: 504 en “upstream timed out”
- HTTP/HTTPS-mismatches: “The plain HTTP request was sent to HTTPS port”
- SSL/TLS-valkuilen in reverse proxy setups
- Headers die je móét zetten: Host, X-Forwarded-*, en websockets
- Redirect loops en verkeerde scheme-detectie
- mTLS, self-signed upstreams en
proxy_ssl_* - Praktische debugcommando’s (curl, openssl, tcpdump)
- Hardening en best practices
- Checklist: 502/504/SSL incident in 10 minuten
Context: wat doet Nginx als reverse proxy in Docker?
In een typische Docker-setup staat Nginx “vooraan” en routeert verkeer naar één of meerdere backend services (upstreams). Nginx kan:
- TLS termineren (HTTPS aan de buitenkant, HTTP naar binnen)
- load balancing doen
- caching en compressie toepassen
- request size limits afdwingen
- websockets doorzetten
- headers normaliseren (belangrijk voor apps die achter een proxy draaien)
In Docker komt daar een extra laag bij: container networking. Nginx praat niet met localhost van je host, maar met het Docker-netwerk. Veel 502/504’s zijn uiteindelijk “gewoon” netwerk- of naamresolutieproblemen in Docker.
Snelstart: een minimale werkende setup
We bouwen een klein voorbeeld met:
nginxals reverse proxy- een simpele
http-echobackend (of je eigen app) - één Docker network
1) Docker network
docker network create rpnet
2) Backend starten (voorbeeld)
Gebruik een simpele HTTP service. Bijvoorbeeld hashicorp/http-echo:
docker run -d --name echo --network rpnet \
-p 18080:5678 \
hashicorp/http-echo -listen=:5678 -text="ok via echo"
Test vanaf je host:
curl -i http://localhost:18080/
3) Nginx config maken
Maak een map:
mkdir -p nginx/conf.d
Maak nginx/conf.d/default.conf:
server {
listen 80;
server_name _;
location / {
proxy_pass http://echo:5678;
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;
}
}
Start Nginx:
docker run -d --name nginx-rp --network rpnet \
-p 8080:80 \
-v "$PWD/nginx/conf.d:/etc/nginx/conf.d:ro" \
nginx:alpine
Test:
curl -i http://localhost:8080/
Als dit werkt, heb je een baseline. Alles wat hierna misgaat, kun je vergelijken met deze “known good”.
Begrijp 502 vs 504: wat betekent het echt?
502 Bad Gateway
Nginx geeft 502 als het geen geldig antwoord krijgt van de upstream. Veelvoorkomende oorzaken:
- upstream hostnaam niet te resolven (DNS)
- connectie geweigerd (app luistert niet / verkeerde poort)
- upstream sluit verbinding direct (crash, misconfig)
- TLS handshake faalt tussen Nginx en upstream (bij HTTPS upstream)
- upstream stuurt geen geldige HTTP response (bijv. praat gRPC/HTTPS op een HTTP-poort)
Typische Nginx error log regels:
connect() failed (111: Connection refused) while connecting to upstreamhost not found in upstreamupstream prematurely closed connection
504 Gateway Timeout
Nginx geeft 504 als het wel kan verbinden, maar geen response op tijd krijgt. Oorzaken:
- backend is traag of hangt
- timeouts te laag (
proxy_read_timeout,proxy_connect_timeout) - upstream verwerkt grote uploads of lange requests zonder data terug te sturen
- netwerkproblemen / packet loss
- DNS-resolutie die hangt (minder vaak, maar kan)
Typische log:
upstream timed out (110: Connection timed out) while reading response header from upstream
Diagnoseflow: van buiten naar binnen
Werk altijd van buiten naar binnen, en verifieer elke laag.
- Client → Nginx: komt de request aan? (access log)
- Nginx → upstream: kan Nginx resolven en verbinden?
- Upstream: luistert de app op juiste interface/poort? is hij gezond?
- TLS: klopt terminatie/doorsturen? juiste headers?
- Timeouts: passen Nginx timeouts bij je workload?
Nginx-logs en Docker: waar kijk je?
Container logs
Als je Nginx image zo is gebouwd dat logs naar stdout/stderr gaan (vaak bij Docker images), dan:
docker logs -f nginx-rp
Bij standaard Nginx in Alpine staan logs vaak in /var/log/nginx/. Kijk in de container:
docker exec -it nginx-rp sh
ls -l /var/log/nginx
tail -n 200 /var/log/nginx/error.log
tail -n 200 /var/log/nginx/access.log
Nginx config dumpen en testen
Altijd doen bij twijfel:
docker exec -it nginx-rp nginx -t
docker exec -it nginx-rp nginx -T | sed -n '1,200p'
nginx -T print de volledige effectieve configuratie (handig bij includes).
Netwerkproblemen in Docker: DNS, networks, poorten
1) Zitten containers op hetzelfde network?
docker inspect nginx-rp --format '{{json .NetworkSettings.Networks}}' | jq
docker inspect echo --format '{{json .NetworkSettings.Networks}}' | jq
Als ze niet hetzelfde network delen, kan proxy_pass http://echo:5678; nooit werken.
Koppel een container aan een network:
docker network connect rpnet nginx-rp
2) DNS-resolutie in Docker
In een Docker user-defined network werkt service discovery via containernaam. Test in Nginx container:
docker exec -it nginx-rp sh -c "getent hosts echo && nslookup echo || true"
Als getent hosts echo faalt: naam bestaat niet in dat network of container heet anders.
3) Verkeerde poort of interface
Veel apps luisteren in containers op 127.0.0.1 in plaats van 0.0.0.0. Dan zijn ze niet bereikbaar vanaf andere containers.
Check in de backend container:
docker exec -it echo sh -c "netstat -tulpn 2>/dev/null || ss -tulpn"
Je wil iets zien als 0.0.0.0:5678 (of :::5678 voor IPv6).
Upstream-problemen: app luistert niet, verkeerde poort, crashloops
Symptoom: 502 + Connection refused
Nginx kan de host resolven, maar de TCP connect faalt.
Check backend status:
docker ps --filter name=echo
docker logs --tail=200 echo
Als de container steeds herstart:
docker inspect echo --format '{{.State.Status}} {{.State.Restarting}} {{.State.ExitCode}}'
Test verbinding vanaf Nginx container
Dit is cruciaal: test vanuit dezelfde netwerknamespace als Nginx.
docker exec -it nginx-rp sh -c "apk add --no-cache curl >/dev/null 2>&1 || true; curl -i http://echo:5678/"
Als dit niet werkt, is het geen “Nginx-probleem” maar een netwerk/upstream probleem.
Veelgemaakte fout: proxy_pass http://localhost:...
In een container verwijst localhost naar de container zelf. Dus:
proxy_pass http://localhost:3000;
werkt alleen als je app in dezelfde container draait (meestal niet). Gebruik containernaam of service DNS (app:3000) of een Docker network alias.
Timeouts en buffering: 504 en “upstream timed out”
Belangrijke Nginx timeouts
proxy_connect_timeout: tijd om TCP connect op te zettenproxy_send_timeout: tijd om request naar upstream te sturenproxy_read_timeout: tijd om response te lezen (meest relevant bij 504)send_timeout: tijd om response naar client te sturen
Voor trage endpoints (rapportages, exports, grote queries) kan proxy_read_timeout te laag zijn.
Voorbeeld (server of location):
location /reports/ {
proxy_pass http://app:8080;
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 300s;
proxy_buffering off; # nuttig bij streaming of SSE
}
Buffering en grote responses
Nginx buffert responses standaard. Bij streaming (SSE), websockets, of “progressive” responses wil je vaak buffering uit:
proxy_buffering off;
proxy_request_buffering off;
Let op: buffering uitzetten kan performance-impact hebben; gebruik het gericht per endpoint.
Grote uploads: 413 vs timeouts
Uploads falen soms met 502/504-achtige symptomen, maar de echte oorzaak kan client_max_body_size zijn (meestal 413). Toch kan een upload ook “hangen” als upstream traag leest.
Zet expliciet:
client_max_body_size 100m;
proxy_request_buffering off; # bij grote uploads naar upstream
HTTP/HTTPS-mismatches: “The plain HTTP request was sent to HTTPS port”
Dit gebeurt wanneer Nginx HTTP praat tegen een upstream die HTTPS verwacht, of andersom.
Scenario A: upstream is HTTPS, maar proxy_pass http://...
Je upstream luistert op 443 met TLS, maar je proxy gebruikt http.
Fix:
location / {
proxy_pass https://app:443;
}
En vaak ook (als upstream SNI nodig heeft):
proxy_ssl_server_name on;
proxy_ssl_name app; # of de echte hostname in het certificaat
Scenario B: upstream is HTTP, maar je gebruikt https://
Dan faalt de TLS handshake en krijg je 502 met SSL errors in de log.
Check Nginx error log:
SSL_do_handshake() failedwrong version number
Fix: gebruik http:// naar de upstream.
SSL/TLS-valkuilen in reverse proxy setups
Hier zit een groot deel van de “mysterieuze” storingen. We splitsen het op in TLS aan de buitenkant (client ↔ Nginx) en TLS naar upstream (Nginx ↔ backend).
1) TLS terminatie (client ↔ Nginx)
Een solide basisconfig:
server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
# Moderne defaults (afhankelijk van je compliance)
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
location / {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Valkuil: verkeerd certificaatbestand
fullchain.pembevat doorgaans servercert + intermediates.- Als je alleen
cert.pemgebruikt, kunnen clients “unable to get local issuer certificate” krijgen.
Verifieer chain:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
Let op regels als:
Verify return code: 0 (ok)is goed- anders: chain ontbreekt of hostname mismatch
2) SNI en meerdere domeinen
Als je meerdere hosts op één IP hebt, moet server_name goed staan en je certificaten moeten matchen. Test:
openssl s_client -connect <ip-of-host>:443 -servername example.com </dev/null | openssl x509 -noout -subject -issuer
Zonder -servername kun je het “default” certificaat zien, wat misleidend is.
3) SSL redirect loops door verkeerde X-Forwarded-Proto
Veel apps (Django, Rails, Laravel, Next.js, Spring) detecteren of de request “secure” is via headers. Als Nginx TLS terminiert maar naar upstream HTTP praat, dan ziet de app http tenzij je X-Forwarded-Proto zet.
Symptoom: oneindige 301/302 redirects naar https of terug naar http.
Correct:
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
Als je in de HTTPS server block zit, is $scheme al https.
4) HSTS en testomgevingen
Als je Strict-Transport-Security zet, kan je browser “vast” op HTTPS blijven. Dat kan debugging frustreren.
Voor productie:
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Voor staging liever niet, of met lage max-age.
Headers die je móét zetten: Host, X-Forwarded-*, en websockets
Basisset headers
Gebruik dit bijna altijd:
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;
Waarom Host belangrijk is:
- apps gebruiken het voor routing/virtual hosts
- upstream frameworks genereren absolute URLs op basis van Host
- security checks (allowed hosts) falen zonder juiste Host
Websockets
Voor websockets moet je connection upgrade doorgeven:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
# ...
location /ws/ {
proxy_pass http://app:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
}
}
Symptoom bij ontbreken: websockets verbinden niet of vallen direct weg (soms als 502/504, soms als 400/426).
Redirect loops en verkeerde scheme-detectie
Diagnose met curl
Gebruik -I en -L slim:
curl -I http://example.com/
curl -I https://example.com/
curl -IL http://example.com/
Als je Location: https://... en daarna weer Location: http://... ziet, heb je een loop tussen Nginx en app.
Oplossing: app “vertrouwt proxy” configuratie
Nginx kan headers zetten, maar je app moet ze ook vertrouwen.
Voorbeelden (indicatief, check je framework):
- Django:
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') - Express/Node:
app.set('trust proxy', 1) - Spring Boot:
server.forward-headers-strategy=nativeofframework - Laravel:
TrustProxiesmiddleware correct instellen
Zonder dit blijft de app denken dat het HTTP is en blijft hij redirecten.
mTLS, self-signed upstreams en proxy_ssl_*
Als Nginx naar een HTTPS upstream praat met self-signed of private CA, faalt verificatie.
Snelle (maar vaak onwenselijke) workaround
proxy_ssl_verify off;
Gebruik dit alleen tijdelijk voor isolatie. Beter: installeer de CA.
Correct: CA certificaat toevoegen
Plaats je CA in de container (bijv. /etc/nginx/ca.pem) en:
location / {
proxy_pass https://internal-api:8443;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/ca.pem;
proxy_ssl_server_name on;
}
mTLS (client cert naar upstream)
location / {
proxy_pass https://secure-upstream:9443;
proxy_ssl_certificate /etc/nginx/client-certs/client.crt;
proxy_ssl_certificate_key /etc/nginx/client-certs/client.key;
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/nginx/ca.pem;
}
Diagnose upstream TLS:
docker exec -it nginx-rp sh -c "apk add --no-cache openssl >/dev/null 2>&1 || true; \
openssl s_client -connect secure-upstream:9443 -servername secure-upstream </dev/null"
Praktische debugcommando’s (curl, openssl, tcpdump)
1) Curl vanaf host vs vanaf Nginx container
Host:
curl -vk https://example.com/
Vanaf Nginx container naar upstream:
docker exec -it nginx-rp sh -c "apk add --no-cache curl >/dev/null 2>&1 || true; curl -v http://app:8080/health"
Verschil in resultaat = netwerk/DNS/route issue.
2) Check welke upstream Nginx probeert te bereiken
Zoek in nginx -T:
docker exec -it nginx-rp nginx -T | grep -R "proxy_pass" -n /etc/nginx 2>/dev/null || true
docker exec -it nginx-rp nginx -T | sed -n '1,200p'
Let op variabelen in proxy_pass (bijv. proxy_pass http://$upstream;). DNS-resolutie werkt dan anders; je hebt soms een resolver directive nodig.
3) resolver bij dynamische upstreams
Als je proxy_pass met variabelen gebruikt:
resolver 127.0.0.11 valid=10s ipv6=off;
set $backend app;
proxy_pass http://$backend:8080;
127.0.0.11 is de Docker embedded DNS in user-defined networks.
4) TCP dump (zwaar, maar soms goud)
Installeer tools in Nginx container (Alpine):
docker exec -it nginx-rp sh -c "apk add --no-cache tcpdump >/dev/null 2>&1 || true"
Capture verkeer naar upstream:
docker exec -it nginx-rp sh -c "tcpdump -i any -nn host app and port 8080"
Je ziet SYN/SYN-ACK (connect), retransmits (packet loss), etc.
Hardening en best practices
1) Health endpoints en upstream checks
Nginx OSS heeft geen actieve health checks zoals Nginx Plus, maar je kunt:
- een
/healthendpoint in je app hebben - monitoring op response codes/latency doen
max_failsenfail_timeoutgebruiken
Voorbeeld upstream block:
upstream app_upstream {
server app1:8080 max_fails=3 fail_timeout=10s;
server app2:8080 max_fails=3 fail_timeout=10s;
keepalive 32;
}
server {
location / {
proxy_pass http://app_upstream;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
2) Keepalive naar upstream
Zonder keepalive kan elke request een nieuwe TCP connect opzetten. Bij hoge load kan dat timeouts veroorzaken.
Belangrijk: als je keepalive gebruikt, zet proxy_http_version 1.1 en Connection "".
3) Limieten en bescherming
client_body_timeout 30s;
client_header_timeout 10s;
keepalive_timeout 65s;
limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
server {
location /login {
limit_req zone=perip burst=20 nodelay;
proxy_pass http://app:8080;
}
}
4) Duidelijke error logging tijdens incidenten
Zet tijdelijk error log op info of debug (debug vereist vaak build flag, maar info helpt al):
error_log /var/log/nginx/error.log info;
Herlaad Nginx:
docker exec -it nginx-rp nginx -s reload
Checklist: 502/504/SSL incident in 10 minuten
Gebruik dit als runbook.
A) Basis: wat is de fout?
- Reproduceer:
curl -vk https://example.com/ - Check Nginx error log:
docker logs --tail=200 nginx-rp # of: docker exec -it nginx-rp tail -n 200 /var/log/nginx/error.log
B) 502: upstream bereiken?
- Resolve upstream vanuit Nginx container:
docker exec -it nginx-rp getent hosts app - Curl upstream vanuit Nginx container:
docker exec -it nginx-rp sh -c "apk add --no-cache curl >/dev/null 2>&1 || true; curl -i http://app:8080/" - Check of backend luistert op 0.0.0.0:
docker exec -it app sh -c "ss -tulpn || netstat -tulpn"
C) 504: timeouts?
- Kijk naar
upstream timed outin logs. - Meet latency:
docker exec -it nginx-rp sh -c "time curl -s -o /dev/null -w '%{http_code}\n' http://app:8080/slow" - Verhoog gericht:
proxy_read_timeout 300s;
D) SSL issues?
- Cert chain/hostname:
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null - Redirect loop check:
curl -IL http://example.com/
Veelvoorkomende “recepten” voor fixes
Recept 1: 502 door verkeerde upstream naam
Symptoom in log: host not found in upstream "app"
Fix:
- zet containers op hetzelfde Docker network
- gebruik correcte servicenaam/containernaam
- test met
getent hosts app
Recept 2: 502 door verkeerde poort
Symptoom: connect() failed (111: Connection refused)
Fix:
- check
ss -tulpnin backend - pas
proxy_pass http://app:<poort>;aan
Recept 3: 504 door lange requests
Symptoom: upstream timed out ... reading response header
Fix:
- verhoog
proxy_read_timeout - check backend performance (DB, locks)
- overweeg async jobs
Recept 4: SSL handshake faalt naar upstream
Symptoom: SSL_do_handshake() failed
Fix:
- klopt
https://inproxy_pass? - zet
proxy_ssl_server_name on; - configureer
proxy_ssl_trusted_certificatevoor private CA
Recept 5: Redirect loop
Symptoom: curl -IL toont herhaalde 301/302
Fix:
- zet
X-Forwarded-Proto - configureer app “trust proxy”
- voorkom dubbele redirects (app en Nginx tegelijk)
Afsluiting
Reverse proxy-problemen met Nginx in Docker lijken vaak complex, maar vallen bijna altijd uiteen in een beperkt aantal categorieën: DNS/network, upstream health/poort/interface, timeouts, of TLS/headers. Als je consequent:
- vanuit de Nginx container naar de upstream test,
- Nginx logs leest met de juiste context,
- scheme/headers correct doorgeeft,
- TLS stap voor stap valideert met
openssl s_client,
dan kun je 502/504 en SSL-gerelateerde issues snel isoleren en duurzaam oplossen.
Als je wilt, kun je je default.conf, de output van nginx -T, en een relevante error.log snippet plakken; dan kan ik gericht aanwijzen waar de misconfiguratie zit en een aangepaste config voorstellen.