Nginx 502 Bad Gateway in Docker? 4 Fixes That Actually Work (2026)

Nginx 502 Bad Gateway error in Docker reverse proxy setup — 4 fixes that work

You set up an nginx reverse proxy in front of your Docker containers, opened the page, and got slapped with 502 Bad Gateway. The containers are running. The app works when you hit it directly. But through nginx — nothing.

I've been there, and for this guide I did something most write-ups don't: I triggered this exact error on a test server and captured the real log output — the connect() failed (111: Connection refused) lines below are from those runs, not illustrations. Docker adds its own twists on top (each container's private localhost, container-name DNS, IP changes on recreate), which I verified against the official Docker and nginx documentation and call out as such.

A 502 means nginx itself is fine, but it cannot complete the connection to your upstream container. The fix is always on the nginx-to-container path — and 90% of the time it's one of these four.

Diagnose Your 502 in 3 Commands

Before changing anything, find out which cause is yours. Run these from your Docker host:

docker logs your-nginx-container --tail 30
docker exec your-nginx-container getent hosts your-app-container
docker exec your-nginx-container wget -qO- http://your-app-container:3000

The first command is the money one. If you see connect() failed (111: Connection refused) while connecting to upstream, nginx reached something but nothing was listening. Here's what I got when I reproduced it:

2026/10/17 15:47:04 [error] 9502#9502: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 127.0.0.1, server: , request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:3000/", host: "127.0.0.1:8080"

See upstream: "http://127.0.0.1:3000/"? That's the smoking gun for Cause 1 — the classic nginx 502 Bad Gateway Docker misconfiguration. If your log looks like this, skip straight to Fix 1.

Fix 1: Stop Pointing proxy_pass at localhost

This is the trap. In your nginx config you wrote something like:

location / {
    proxy_pass http://localhost:3000;
}

On your laptop, localhost means your laptop. Inside the nginx container, localhost means the nginx container itself — and nothing is listening on port 3000 in there. Connection refused. 502. Every time.

This is the #1 cause of nginx 502s in Docker homelabs, and it's the one most guides skip: each container has its own loopback interface. localhost never leaves the container.

The fix: point nginx at the container name (or Compose service name), and put both containers on the same user-defined Docker network:

location / {
    proxy_pass http://my-app:3000;
}

In my lab, proxying to an address where nothing was listening produced exactly this 502 with the log signature above. In Docker, localhost inside the nginx container is precisely that — an address where your app isn't listening. Same error, same log, same one-word fix.

Fix 2: Make Your App Listen on 0.0.0.0, Not 127.0.0.1

So you fixed the container name, but the 502 is still there. Next suspect: your app is bound to localhost inside its own container.

I reproduced this on my test server by starting a web server bound to 127.0.0.1 and having nginx arrive via a different interface — instant 502 with the same connection refused signature. Rebinding the app to 0.0.0.0 flipped it to HTTP 200 with zero config changes on the nginx side.

The fix depends on your app:

# Python http.server / Flask-style apps: bind all interfaces
python3 -m http.server 3000 --bind 0.0.0.0

# Node.js / Express
app.listen(3000, '0.0.0.0');

# Uvicorn / FastAPI
uvicorn main:app --host 0.0.0.0 --port 8000
Rule of thumb: inside a container, your app must listen on 0.0.0.0 (all interfaces). 127.0.0.1 inside a container is only reachable from within that same container — nginx can never reach it.

Verify from the nginx container before touching the config again:

docker exec your-nginx-container wget -qO- http://my-app:3000 && echo "REACHABLE"

If that prints your app's response, the network path is good and nginx will work too.

Fix 3: Put Both Containers on the Same Docker Network

Container-name resolution only works on user-defined bridge networks. Docker's default bridge network does not provide automatic DNS by container name — this is documented Docker behavior, and it bites everyone once.

If your nginx container and app container were started with plain docker run (no --network), they're on the default bridge and my-app won't resolve. Test it:

docker exec your-nginx-container getent hosts my-app
# no output = DNS can't resolve the name = 502 incoming

The fix — create a shared network and attach both containers:

docker network create proxy-net
docker network connect proxy-net your-nginx-container
docker network connect proxy-net your-app-container

Or do it properly in Compose (recommended — this is also how you avoid the problem permanently):

services:
  nginx:
    image: nginx:alpine
    ports:
      - "8080:80"
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
    networks:
      - proxy-net
  my-app:
    image: python:3-alpine
    command: python3 -m http.server 3000 --bind 0.0.0.0
    networks:
      - proxy-net

networks:
  proxy-net:
    driver: bridge
Note: no version: key at the top — it's been obsolete since Compose v2 and current Compose versions warn about it. Plenty of guides (including the current top-ranking one) still include it. Leave it out.

This is the standard layout for any nginx reverse proxy in Docker: one shared network, container names

Running Pi-hole in Docker too? Our Pi-hole v6.5 update guide: 6 breaking changes and how to fix them covers the safe-update workflow for the 2026.10.0 image.


as upstreams, and no published ports on the backend containers.

Fix 4: Handle the Stale-IP Trap After Container Recreates

This one is sneaky and hits you days later: everything works, then you redeploy your app (docker compose up -d), and the nginx 502 Bad Gateway Docker error is back. You changed nothing in nginx.

What happened: nginx resolves upstream hostnames once at startup — documented nginx behavior. When Docker recreates your app container, it gets a new IP address — documented Docker behavior. But nginx keeps sending traffic to the old one, and every request dies with 502 until something forces a re-resolution.

Quick relief: docker exec your-nginx-container nginx -s reload — forces re-resolution.

Permanent fix: use a variable in proxy_pass with Docker's embedded DNS (127.0.0.11), so nginx re-resolves every few seconds:

server {
    listen 80;

    # Docker's embedded DNS server — re-resolve container IPs dynamically
    resolver 127.0.0.11 valid=5s ipv6=off;
    resolver_timeout 3s;

    # A variable forces nginx to resolve at request time, not just at startup
    set $upstream_app http://my-app:3000;

    location / {
        proxy_pass $upstream_app;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
The resolver 127.0.0.11 trick only works when nginx itself runs inside Docker on a user-defined network. If your nginx runs on the host, use your LAN DNS or the Docker bridge gateway IP instead.

While you're hardening the stack: if you run Nextcloud with the Collabora built-in CODE app, check it against CVE-2026-110447 — the backdoor that disabling the app won't fix.

Quick-Reference: Symptom to Fix

Match what you see in your logs to the right fix for the nginx 502 Bad Gateway Docker error:

What the log / test showsCauseFix
upstream: "http://127.0.0.1:3000/"proxy_pass points at localhost = the nginx container itselfUse the container/service name
getent hosts resolves, but connection refusedApp bound to 127.0.0.1 inside its containerBind the app to 0.0.0.0
getent hosts my-app returns nothingContainers on different networks (or default bridge)Shared user-defined network
Worked, then 502s after redeploying the appNginx cached the old container IPVariable proxy_pass + resolver 127.0.0.11

FAQ

Why does nginx return 502 Bad Gateway only in Docker, but works on the host?

Because networking semantics change inside containers. On the host, localhost is your machine and DNS resolves via your system. In Docker, each container has its own loopback and its own DNS view — so configs that work on bare metal break silently in containers.

How do I check the nginx error log inside a Docker container?

Run docker logs your-nginx-container --tail 50 from the host. Nginx in Docker logs to stdout/stderr by default, so the error log (including the connect() failed (111: Connection refused) line with the exact upstream) shows up in the container logs.

Is 502 different from 504 Gateway Timeout in nginx?

Yes. A 502 means nginx couldn't complete the connection to upstream at all (refused, DNS failure, invalid response). A 504 means the connection worked but upstream took too long to respond — that's a timeout problem, fixed with proxy_read_timeout and friends, not the fixes in this guide.

Do I need to publish my app container's ports for nginx to reach it?

No. Containers on the same Docker network reach each other by container name and the container's internal port — no published ports needed. Only nginx needs its ports (80/443) published to the host.

Why does my 502 come back every time I redeploy with docker compose?

Nginx resolves the upstream hostname once at startup. A redeploy gives your app container a new IP, and nginx keeps using the stale one. Use the variable + resolver 127.0.0.11 pattern from Fix 4, or reload nginx after every redeploy.

Can I use Nginx Proxy Manager instead of writing nginx configs?

Yes — it's a solid GUI option for homelabs. But the same rules apply: in a Proxy Host entry, never use localhost as the forward hostname (that's the NPM container itself). Use the container name and internal port, on a shared Docker network.

Running a fediverse server behind that reverse proxy? Our Mastodon 5.0 beta self-hosted upgrade guide covers the rebuilt UI, the Messages rename, and the safe upgrade path for source and Docker installs.


Failure mechanics tested October 17, 2026 on a Linux server running nginx 1.24 — the 502 log lines in this nginx 502 Bad Gateway Docker troubleshooting guide are real output from those runs. Docker-specific behaviors (per-container loopback, 127.0.0.11 embedded DNS, no name resolution on the default  network, new container IPs on recreate) verified against the official Docker and nginx documentation.

Running n8n behind that proxy? n8n 3.0 goes Docker-only and kills npm installs — our pre-upgrade triage covers the npm-to-Docker migration and every breaking change to fix before it lands.




Running n8n behind that proxy? n8n 3.0 goes Docker-only and kills npm installs — our pre-upgrade triage covers the npm-to-Docker migration and every breaking change to fix before it lands.


Comments

Popular posts from this blog

GitLab Update: Patch the CVSS-10 RCE Tonight — Self-Hosted Fix Guide (CVE-2026-110264)

php2shell: Patch Your Self-Hosted PHP Stack Tonight — Emergency RCE Playbook (CVE-2026-109618, CVE-2026-109619)