docker
nginx
reverse-proxy
502-bad-gateway
troubleshooting

Docker nginx reverse proxy gives 502 Bad Gateway

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

A 502 Bad Gateway from Nginx means Nginx received the client request but could not get a valid response from the upstream application. In Docker setups, that usually comes down to the reverse proxy pointing at the wrong host or port, the backend container not being reachable on the Docker network, or the app not being ready when Nginx starts forwarding traffic.

What Nginx Is Actually Trying to Reach

Inside a container, localhost means the container itself, not another service. That is the first thing to check.

If Nginx is in one container and your application is in another, this is wrong:

nginx
location / {
    proxy_pass http://127.0.0.1:3000;
}

That tells the Nginx container to connect back to itself. In Docker Compose, you usually want the service name instead:

nginx
1location / {
2    proxy_pass http://app:3000;
3    proxy_set_header Host $host;
4    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
5    proxy_set_header X-Forwarded-Proto $scheme;
6}

The app hostname works because Docker Compose provides service-name DNS on the shared network.

A Minimal Working Compose Setup

A simple example makes the networking model clearer:

yaml
1services:
2  nginx:
3    image: nginx:1.27-alpine
4    ports:
5      - "8080:80"
6    volumes:
7      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
8    depends_on:
9      - app
10
11  app:
12    image: node:22-alpine
13    working_dir: /usr/src/app
14    command: sh -c "node server.js"
15    volumes:
16      - ./server.js:/usr/src/app/server.js:ro

And the sample backend:

javascript
1const http = require('http');
2
3const server = http.createServer((req, res) => {
4  res.writeHead(200, { 'Content-Type': 'text/plain' });
5  res.end('hello from app\n');
6});
7
8server.listen(3000, '0.0.0.0', () => {
9  console.log('listening on port 3000');
10});

The important detail is that the app listens on 0.0.0.0, not only on 127.0.0.1. If it binds only to loopback inside its own container, other containers cannot reach it.

How to Debug the Failure Quickly

Start with the basics:

bash
docker compose ps
docker compose logs nginx
docker compose logs app

Then test connectivity from inside the Nginx container:

bash
docker compose exec nginx sh
apk add --no-cache curl
curl http://app:3000/

If that curl request fails, Nginx is not the problem. The issue is the app container, port, bind address, or shared network.

Also validate the Nginx config before restarting it:

bash
docker compose exec nginx nginx -t

That catches syntax errors and invalid upstream configuration immediately.

Readiness and Startup Order

depends_on only controls container start order. It does not guarantee the application is ready to accept traffic. A backend that takes ten seconds to boot can still produce temporary 502 responses while Nginx is already live.

In those cases, the fix is usually one of these:

  • add a health check and wait for readiness
  • make the app start faster
  • configure retries at the right layer

Do not assume that because Docker started the container, the service inside it is listening.

Common Pitfalls

The most common mistake is using localhost in proxy_pass. In a multi-container setup, use the backend service name instead.

Another common issue is pointing Nginx at the host-exposed port instead of the container's internal listening port. Nginx talks to the backend over the Docker network, so it needs the container port, not the published host port.

People also forget the backend bind address. A service listening only on 127.0.0.1 inside its own container will appear healthy from its own logs but remain unreachable from Nginx.

Finally, treat depends_on as ordering, not readiness. If the backend is booting, crashing, or restarting, Nginx can only report the upstream failure.

Summary

  • A Docker Nginx 502 usually means the reverse proxy cannot reach a healthy upstream.
  • Use the backend service name in proxy_pass, not localhost.
  • Make sure the application listens on 0.0.0.0 and on the expected internal port.
  • Debug from inside the Nginx container with curl and validate config with nginx -t.
  • Remember that container startup order is not the same as application readiness.

Course illustration
Course illustration

All Rights Reserved.