← Terug naar tutorials

Reverse proxy-problemen oplossen met Nginx in Docker: 502/504 en SSL-valkuilen

nginxdockerreverse-proxydevops502-bad-gateway504-gateway-timeoutssl-tlstroubleshooting

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

  1. Context: wat doet Nginx als reverse proxy in Docker?
  2. Snelstart: een minimale werkende setup
  3. Begrijp 502 vs 504: wat betekent het echt?
  4. Diagnoseflow: van buiten naar binnen
  5. Nginx-logs en Docker: waar kijk je?
  6. Netwerkproblemen in Docker: DNS, networks, poorten
  7. Upstream-problemen: app luistert niet, verkeerde poort, crashloops
  8. Timeouts en buffering: 504 en “upstream timed out”
  9. HTTP/HTTPS-mismatches: “The plain HTTP request was sent to HTTPS port”
  10. SSL/TLS-valkuilen in reverse proxy setups
  11. Headers die je móét zetten: Host, X-Forwarded-*, en websockets
  12. Redirect loops en verkeerde scheme-detectie
  13. mTLS, self-signed upstreams en proxy_ssl_*
  14. Praktische debugcommando’s (curl, openssl, tcpdump)
  15. Hardening en best practices
  16. 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:

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:

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:

Typische Nginx error log regels:

504 Gateway Timeout

Nginx geeft 504 als het wel kan verbinden, maar geen response op tijd krijgt. Oorzaken:

Typische log:


Diagnoseflow: van buiten naar binnen

Werk altijd van buiten naar binnen, en verifieer elke laag.

  1. Client → Nginx: komt de request aan? (access log)
  2. Nginx → upstream: kan Nginx resolven en verbinden?
  3. Upstream: luistert de app op juiste interface/poort? is hij gezond?
  4. TLS: klopt terminatie/doorsturen? juiste headers?
  5. 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

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:

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

Verifieer chain:

openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null

Let op regels als:

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:

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):

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:

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?

  1. Reproduceer:
    curl -vk https://example.com/
  2. 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?

  1. Resolve upstream vanuit Nginx container:
    docker exec -it nginx-rp getent hosts app
  2. 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/"
  3. Check of backend luistert op 0.0.0.0:
    docker exec -it app sh -c "ss -tulpn || netstat -tulpn"

C) 504: timeouts?

  1. Kijk naar upstream timed out in logs.
  2. Meet latency:
    docker exec -it nginx-rp sh -c "time curl -s -o /dev/null -w '%{http_code}\n' http://app:8080/slow"
  3. Verhoog gericht:
    proxy_read_timeout 300s;

D) SSL issues?

  1. Cert chain/hostname:
    openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null
  2. 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:

Recept 2: 502 door verkeerde poort

Symptoom: connect() failed (111: Connection refused)

Fix:

Recept 3: 504 door lange requests

Symptoom: upstream timed out ... reading response header

Fix:

Recept 4: SSL handshake faalt naar upstream

Symptoom: SSL_do_handshake() failed

Fix:

Recept 5: Redirect loop

Symptoom: curl -IL toont herhaalde 301/302

Fix:


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:

  1. vanuit de Nginx container naar de upstream test,
  2. Nginx logs leest met de juiste context,
  3. scheme/headers correct doorgeeft,
  4. 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.