Debugging 502 and 404 Errors Behind a Reverse Proxy on a VPS
- Author
- Vishal Maurya
- Published on
- Reading time
- 4 min read
Overview
You put Nginx in front of your app on a VPS, and the browser shows 502 Bad Gateway or a 404 that does not look like it came from your application. The app works when you test it directly, so the problem sits between the proxy and the app. Work from the inside out and the cause usually appears in a few commands.
1. Know What Each Error Means
502 Bad Gateway: Nginx reached for the upstream and got a bad answer or no connection at all. Typically the app is down, listening elsewhere, or unreachable from Nginx.504 Gateway Timeout: the upstream accepted the connection but did not reply in time.404 Not Found: either your app returned it (route mismatch, path prefix) or Nginx did (wrongserver_name, missinglocation, wrongroot).
Check which one produced the 404. Nginx's own page says nginx in the footer; your framework's page usually looks different. Then read the Nginx error log, which states the real reason.
sudo tail -n 50 /var/log/nginx/error.log
Messages like connect() failed (111: Connection refused) while connecting to upstream point straight at the upstream.
2. Test the App Directly
Bypass Nginx and call the app from the host.
docker compose ps
curl -i http://127.0.0.1:8000/health
If this fails, fix the app first. Check docker compose logs api for crashes or failed migrations.
3. The App Must Listen on 0.0.0.0 Inside the Container
A frequent cause: the server binds to 127.0.0.1 inside the container. Published ports and other containers reach the container through its network interface, not its loopback, so the connection is refused.
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
For Gunicorn use --bind 0.0.0.0:8000; for Django's development server runserver 0.0.0.0:8000. Verify with docker compose exec api ss -ltn if the tool exists in the image.
4. Match the Port Mapping
In ports: "8000:3000", the left number is the host port and the right is the container port. The app must really listen on the right-hand number. A mismatch gives connection refused or resets.
services:
api:
ports:
- "127.0.0.1:8000:8000"
Verify with docker compose port api 8000 or ss -ltnp | grep 8000 on the host.
5. Use the Right Address in proxy_pass
Where Nginx runs decides the address:
- Nginx on the host, app published to the host:
proxy_pass http://127.0.0.1:8000; - Nginx in the same Compose project: use the service name and the container port:
proxy_pass http://api:8000;
Inside a container, 127.0.0.1 means that container, not the host. Services only resolve each other by name when they share a Docker network.
server {
listen 80;
server_name api.example.com;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Mind trailing slashes. proxy_pass http://api:8000/; with a location /app/ strips the prefix, while omitting the slash passes the full path. This difference commonly causes unexplained 404 responses from the app. Also confirm server_name matches the requested hostname, otherwise Nginx serves a different default server. After edits run sudo nginx -t and reload.
6. If Nginx Started Before the App
Nginx resolves hostnames in proxy_pass at startup. If the container name did not resolve then, Nginx fails to start or keeps a stale address after the container is recreated with a new IP. Restarting or reloading Nginx after a deploy fixes it; for a robust setup, check the Nginx documentation about resolvers and variables in proxy_pass.
7. Firewalls and Docker
Docker publishes ports by editing iptables rules itself. Rules added with UFW may not block a port published with ports: "8000:8000", so the app can be reachable from the internet even if UFW says it is denied. Bind to 127.0.0.1 for anything that should be private. The reverse problem exists too: a cloud provider security group can block 80 or 443 while everything works locally, so test from outside with curl -I http://your-domain.
8. Quick Checklist
- Read the Nginx error log.
curlthe app directly on the host.- Is the app bound to
0.0.0.0inside the container? - Do the published port and the app port match?
- Does
proxy_passuse the correct host or service name and port? - Does
server_namematch, and did you reload after changes? - Are DNS and cloud firewall rules correct?
Conclusion
Start with the error log, test the app without the proxy, then check the bind address, ports and proxy_pass target. Most 502 and 404 problems come from one of those.
If you have a deployment that still returns errors after these checks, I can help trace it. Contact me.