Why 502 Errors in Docker Are Actually Two Different Problems
A 502 Bad Gateway means Nginx received your request but couldn't get a usable response from the upstream. That part is simple. What makes this error confusing inside Docker is that there are two completely separate networking layers that can cause it, and most troubleshooting guides only walk you through one of them.
The first layer is host-level networking: published ports, the host firewall, and whether a process is actually bound to an interface. The second layer is container-level networking: Docker's internal bridge networks, DNS resolution between containers, and whether your reverse proxy and your backend can even see each other at all.
Mixing these two up wastes time. You'll find people editing proxy_pass directives for twenty minutes when the real issue is that Nginx and the backend container aren't even on the same Docker network. You'll also find people running docker network connect in circles when the backend service simply crashed and isn't listening on any port at all. The fix is to ask the right question first, then follow one branch, not both at once.
The First Question: Is This Host-Level or Container-Level?
Before touching any config file, run one test that tells you which branch you're in.
# From the Docker host itself
curl -v http://localhost:9000
# From inside the nginx container, using the Docker network name
docker exec nginx curl -v http://portainer:9000
If the host-level curl fails, stop. Your problem is not Nginx configuration, it's the backend container or the host's published port. If the host-level curl works but the container-to-container curl fails, your problem is Docker network isolation, and no amount of tweaking proxy_pass syntax will fix it.
This single test is the fork in the decision tree. Everything below builds off which branch you land on.
Branch 1: Host-Level Networking Failures
This branch covers everything that happens before Docker's internal networking even gets involved. It's usually the simpler branch to fix, but it's also where people waste time assuming the problem is more complex than it is.
Step 1: Confirm the container is actually running
docker ps | grep portainer
docker inspect portainer | grep '"Status"'
A container in a restart loop will intermittently accept connections and then drop them, which produces 502s that come and go unpredictably. Check the logs before assuming anything about networking.
docker logs portainer --tail=50
Step 2: Confirm the port is published and something is listening
docker port portainer
ss -tlnp | grep -E ':9000|:9443'
If docker port shows nothing, the container never published the port to the host, meaning your host-level curl was never going to work no matter what you did to Nginx. This is a host-level configuration issue in your docker run or compose file, not a proxy issue.
Step 3: Check the host firewall
Even with a published port and a listening process, a host firewall (iptables, ufw, or a cloud security group) can silently drop the connection before it reaches Docker's proxy. A connection that hangs and times out rather than getting refused outright often points here.
Common host-level mistakes
- Assuming a container is up because
docker psshows it, without checkingStatusfor restart loops. - Confusing the container's internal port with the host's published port; Portainer's HTTPS UI listens on 9443 by default, and 9000 is the legacy HTTP port that may not even be published unless you explicitly configured it.
- Forgetting that binding to
127.0.0.1on the host restricts access to local connections only, which breaks things if Nginx is running on a different host or in a different network namespace than expected.
Branch 2: Container-Level Docker Network Isolation
If the host-level test worked but the container-to-container test failed, you're dealing with Docker's internal networking model, which is a different beast entirely. Containers can only resolve each other by name if they share a user-defined network, and this is the single most common cause of Nginx 502s in Docker setups that most people never check first.
Step 1: List the networks both containers are on
docker network ls
docker inspect portainer | grep Networks -A10
docker inspect nginx | grep Networks -A10
If Nginx is on a network called proxy and Portainer is only on the default bridge network, they cannot resolve each other by container name. Nginx's proxy_pass http://portainer:9000; will fail DNS resolution entirely, and you'll see it in the logs.
Step 2: Read the actual error, not just the status code
docker logs nginx --tail=100
These three messages tell you very different things:
connect() failed (111: Connection refused)means Nginx reached the network but nothing answered on that port. That's often a host-level clue leaking into a container-level test, or the backend crashed after your last check.no live upstreams while connecting to upstreamusually means DNS resolution failed inside the Docker network, which is a strong signal of network isolation.upstream timed outsuggests the connection got through but the backend never responded in time, which points toward application-level slowness rather than networking at all.
Step 3: Fix the isolation by joining a shared network
docker network connect proxy portainer
# or, if nginx is the one missing the network
docker network connect proxy nginx
Do this rather than hardcoding IP addresses into your Nginx config. Container IPs change on restart, and a config that worked yesterday will silently break the next time Docker reassigns addresses. Names on a shared user-defined network are stable; IPs are not.
Common container-level mistakes
- Using the default bridge network instead of a user-defined one. The default bridge doesn't support automatic DNS resolution between containers by name, so
proxy_pass http://portainer:9000;fails even though everything looks fine. - Assuming that because two containers are started from the same
docker-compose.yml, they're automatically networked together. They are, but only if they're in the same compose project and you haven't overridden the network settings. - Forgetting that connecting a container to a new network with
docker network connectdoesn't restart it, but some applications cache DNS results and need a restart to pick up new peers reliably.
The Special Case: WebSockets Making It Worse
Portainer, like many management UIs, relies on WebSockets for live updates. A reverse proxy that works fine for standard HTTP requests can still produce 502s or connection drops specifically on WebSocket upgrade requests if the config is missing the right headers.
location / {
proxy_pass http://portainer:9000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s;
}
This is worth mentioning here because it's neither a host-level nor a pure network-isolation issue. It's a protocol-level mismatch that looks like a 502 but only shows up once you're past both networking layers, which is why it belongs at the end of your decision tree, not the start.
Building the Decision Tree Into Your Habits
Next time you hit a 502 in a Dockerized Nginx setup, resist the urge to open the config file first. Run the host curl, then the container curl. That one comparison tells you whether you're chasing a host binding problem, a firewall rule, or a Docker network that never got joined. Only after you know which side of that fork you're on should you start reading logs or editing proxy_pass lines.
The reason most guides feel incomplete is that they hand you every possible fix without telling you which symptom maps to which fix. Treat the host-vs-container test as your triage step, and the rest of the troubleshooting becomes a lot faster because you're not guessing anymore.
FAQ
Why does curl work from the host but not from inside the Nginx container?
This almost always means Docker network isolation. The host has a direct route to the published port, but Nginx's container only has visibility into whatever Docker networks it's attached to. Run docker network inspect on both containers and confirm they share at least one user-defined network with DNS resolution enabled.
Why do I get intermittent 502s instead of consistent ones?
Intermittent errors usually point to a backend container that's restarting under load, or a WebSocket connection timing out and getting reopened. Check docker logs for restart events around the same timestamps as the 502s, and separately check your proxy_read_timeout settings if the errors line up with long-idle sessions.
Should I just hardcode the backend container's IP address to avoid all this?
No, avoid that. Container IPs on Docker networks aren't guaranteed to stay the same across restarts or redeployments, so a hardcoded IP will work today and break silently after the next docker-compose up. Use the container name or a network alias on a shared user-defined network instead, since Docker's embedded DNS keeps that resolution current automatically.