Skip to content

Lab: Nginx 502/504 Troubleshooting

Pairs with: Nginx Reverse Proxy and 502/504 Troubleshooting

Environment: A single disposable VM with nginx installed and Python 3 available (for a trivial backend). If you prefer containers over a VM, the files below reproduce the same lab.

Docker Compose version of this lab (optional)
# docker-compose.yml
services:
  nginx:
    image: nginx:1.27-alpine
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    depends_on:
      - backend

  backend:
    image: python:3.12-alpine
    command: python3 -m http.server 8000
# nginx.conf (next to docker-compose.yml)
upstream app_backend {
    server backend:8000;
}

server {
    listen 80;
    server_name _;

    location / {
        proxy_pass http://app_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_read_timeout 5s;
    }
}

To reproduce the 502 scenario run docker compose stop backend; restore with docker compose start backend.

Starting conditions

  1. nginx installed and running.
  2. The reverse proxy config from the parent article, pointing at 127.0.0.1:8000.
  3. A trivial backend running and confirmed reachable through nginx:
    python3 -m http.server 8000 &
    curl -i http://localhost/
    
    This should return HTTP/1.1 200 OK.

Instructions

Part 1 — Produce a 502

  1. Stop the backend: kill %1 (or find its PID with jobs -l / pgrep -f http.server and kill it).
  2. curl -i http://localhost/ and observe the response.
  3. Check /var/log/nginx/error.log's newest entry.

Part 2 — Produce a 504

  1. Restart the backend, but this time make it artificially slow (simulate a hung request):
    python3 -c "
    import http.server, time
    class Handler(http.server.BaseHTTPRequestHandler):
        def do_GET(self):
            time.sleep(90)
            self.send_response(200)
            self.end_headers()
    http.server.HTTPServer(('', 8000), Handler).serve_forever()
    " &
    
  2. Set a short proxy_read_timeout 5s; in the nginx config (temporarily, for this test) and reload nginx.
  3. curl -i http://localhost/ and time how long it takes to receive a response.

Expected results

  • Part 1: curl receives HTTP/1.1 502 Bad Gateway essentially instantly (no waiting — nginx knows immediately that the connection was refused). The error log shows connect() failed (111: Connection refused) referencing 127.0.0.1:8000.
  • Part 2: curl waits approximately 5 seconds (matching proxy_read_timeout) and then receives HTTP/1.1 504 Gateway Timeout — notably not instant, and not the full 90 seconds the backend would eventually take to respond.

Diagnosis checklist

  • [ ] Did you confirm the 502 was instant (connection-level failure) vs. the 504 being delayed by roughly the configured timeout (a response that just never arrived in time)?
  • [ ] Did you check error.log for Part 1, and correctly interpret Connection refused as "nothing is listening," distinct from a reset or a timeout?
  • [ ] For Part 2: did you confirm, with a direct curl to 127.0.0.1:8000 bypassing nginx entirely, that the backend itself really is the slow part (as opposed to something being wrong specifically in the nginx-to-backend hop)?

Success criteria

You can explain, for any 502 or 504 your own production nginx throws in the future, which of these two categories it falls into and what your very next diagnostic command should be — without needing to re-read the parent article.

Cleanup / rollback

# Stop any lingering test backend:
pkill -f http.server

# Revert the temporary proxy_read_timeout if you changed it:
sudo sed -i 's/proxy_read_timeout 5s;/proxy_read_timeout 60s;/' /etc/nginx/sites-available/app.conf
sudo nginx -t && sudo systemctl reload nginx

Confirm curl -i http://localhost/ returns a normal 200 OK again (with a real backend running) before considering the environment restored.