## TL;DR

Set the container-internal port explicitly: ``yaml labels: - "traefik.http.services.myapp.loadbalancer.server.port=80" ` Rules: - The value is the port the process listens on inside the container. Use loadbalancer.server.port (step 2). - Confirm the provider is actually watching Docker: --providers.docker=true and the socket mounted (/var/run/docker.sock:/var/run/docker.sock:ro).

## When to use

You are seeing this: The router usually looks healthy in the dashboard, the failure is in the last hop, between Traefik and the container. Use this skill when you run into "Diagnose Traefik 502/504 Bad Gateway with the Docker provider".

## When not to use

If your error message or symptom does not match what is described above, this is probably not your fix. Search for your exact error text instead of forcing this one to fit.

## Versions

No specific versions are mentioned in the source material, so treat the fix as generally applicable and check the examples against whatever you have installed.

Use this when requests reach Traefik but come back `502 Bad Gateway` or
`504 Gateway Timeout` for a service discovered via the Docker provider. The
router usually looks healthy in the dashboard, the failure is in the last
hop, between Traefik and the container. Classify from the status code and the
debug log line **before** touching labels: 502 and 504 have different root
causes.

## 1. Read the status code and the debug line first

Turn on debug (`--log.level=DEBUG` or `log.level: DEBUG` in static config) and
reproduce. Traefik logs the exact backend address it tried:

```
msg="Creating server 0 http://CONTAINER_IP:80" serverName=0 routerName=my-service@docker
```

and on failure:

```
msg="'502 Bad Gateway' caused by: dial tcp CONTAINER_IP:1980: connect: connection refused"
```

Classify:

- **502 + `connect: connection refused`**, Traefik dialed the right host but
 the **wrong port** (or nothing listens there). Go to step 2.
- **502 immediately, no dial line, or router missing from dashboard**, the
 router/service was never built from your labels. Go to step 4.
- **504 / `dial tcp ... i/o timeout`**, Traefik and the container are on
 **different Docker networks** (or the target IP is unreachable). Go to
 step 3.
- **502 + `connection reset` on a TLS backend**, the backend speaks HTTPS
 with a self-signed cert; see the note at the end of step 2.

## 2. Wrong port: Traefik picks the first exposed port

By default Traefik uses the **first exposed port of the container**, not the
published port, not the port you think the app uses. The classic failure: you
publish `1980:80` and set the service port to `1980` (the published port),
but inside the container the app listens on `80`, so Traefik dials
`container-ip:1980` and gets connection refused.

Set the **container-internal** port explicitly:

```yaml
labels:
  - "traefik.http.services.myapp.loadbalancer.server.port=80"
```

Rules:

- The value is the port the process listens on **inside the container**.
 Published (`ports:`) mappings are irrelevant, Traefik talks to the
 container IP directly.
- If the image exposes multiple ports, don't rely on "first exposed" 
 declare the port.
- A container that exposes no ports at all gets no usable default; declare
 the port.
- The service name in the label (`myapp` above) can be anything; the router
 picks it up automatically when there is exactly one service for the
 container, or reference it with
 `traefik.http.routers.myapp.service=myapp@docker`.

Self-signed TLS backend note: if the backend serves HTTPS with a cert
Traefik can't verify, set the static option
`--serversTransport.insecureSkipVerify=true` (static config
`serversTransport.insecureSkipVerify: true`). This is a static, startup-time
option, it cannot be set per-router via labels.

## 3. Wrong network: Traefik guessed which network to use

When a container is attached to **more than one network**, Traefik picks one
 and it may pick the wrong one, or flip between them. Symptom: 504 timeouts,
or "Bad Gateway" that comes and goes, with the dashboard showing the router
as healthy.

Pin the network with the label:

```yaml
labels:
  - "traefik.docker.network=frontend"
```

Gotchas:

- The value must be the **real Docker network name**, not the compose
 shorthand. Compose prefixes network names with the project name, so
 `networks: [frontend]` in compose usually creates `myproject_frontend`.
 Check with `docker network ls`. To keep the literal name, set the
 `name:` property on the network in compose.
- Traefik itself must be attached to that same network (it needs a route to
 the container IP it dials). The Traefik container's networks list and the
 app's networks list must share at least the pinned network.
- The static alternative `--providers.docker.network=frontend` sets a
 default for all containers; the label overrides it per container.

## 4. Router never built: the labels were ignored

If the router doesn't appear in the dashboard at all (or appears disabled):

- `traefik.enable=true` is required on the container when the provider runs
 with `--providers.docker.exposedByDefault=false`. Without it, Traefik
 silently skips the container.
- Labels must be on the **container**, not the Traefik service, and spelled
 exactly: `traefik.http.routers.[name].rule=Host(`app.example.com`)` uses
 backticks, not quotes, around the hostname.
- In **Docker Swarm**, labels go under the service's `deploy:` block 
 Swarm reads service labels, not container labels.
- The v1 label `traefik.port` does not exist in v2/v3; it is silently
 ignored. Use `loadbalancer.server.port` (step 2).
- Confirm the provider is actually watching Docker:
 `--providers.docker=true` and the socket mounted
 (`/var/run/docker.sock:/var/run/docker.sock:ro`).

## 5. Checklist

1. Reproduce with `--log.level=DEBUG`; capture the `Creating server 0 ...`
 line and any `dial tcp` error.
2. `connect: connection refused` → fix `loadbalancer.server.port` to the
 container-internal port (step 2).
3. `i/o timeout` / 504 → pin `traefik.docker.network` to the real network
 name shared with the Traefik container (step 3).
4. Router missing/disabled in dashboard → `traefik.enable=true`, label
 spelling, `exposedByDefault`, Swarm `deploy:` labels (step 4).
5. Self-signed TLS backend → `serversTransport.insecureSkipVerify` in
 static config (restart required).
