Tech Digest

Guide

Why does Traefik return 404 page not found for a Docker container?

Traefik answers 404 page not found when no router accepted the request. The app was never asked. We broke a Traefik 3.7 setup more than twenty ways to find which mistakes give that 404, and which ones people blame for it while they really give 502 or 504.

Last reviewed

Why does Traefik return 404 page not found for a Docker container?

Because no enabled router matched the request, so Traefik answered itself and never contacted the container. Open /api/http/routers. If the router is missing, Traefik skipped the container: it is unhealthy, still starting, stopped, not enabled while exposedByDefault is false, or has no port. If it shows disabled, read its error: a v2-style rule or a missing middleware. If it is enabled, the request does not fit it: wrong host, a TLS router reached over HTTP, or another entry point. A wrong network gives 504, a wrong port 502.

Traefik returns 404 page not found when no enabled router accepted the request. The 404 is Traefik's own answer. Your container was never contacted. So the fix is never inside the app. It is in how Traefik read the container's labels, or in how the request reached Traefik. One API call shows which:

bash
curl -s http://localhost:8080/api/http/routers | jq '.[] | {name, rule, status, error, entryPoints, tls}'

That needs --api.insecure=true or a router to the dashboard. If your router is missing from the list, Traefik skipped the container. If it is there with "status": "disabled", its error field names the reason. If it is enabled, the request does not fit it.

The terms this answer turns on#

A router is Traefik's rule for which requests go where. It has a rule (such as `Host(app.example.com)), a list of **entry points** (the ports Traefik listens on, often web on 80 and websecure` on 443) and a service. The service is the address and port of the container.

The Docker provider builds routers and services from the labels on your containers. It reads them through the Docker socket. exposedByDefault decides whether a container with no traefik.enable label is included. The install reference gives its default as true.

When a request arrives, Traefik checks the routers on that entry point. If none matches, it answers with a 19-byte body, 404 page not found. The access log then shows "-" where the router name would be.

What gave 404, and what did not#

We ran Traefik 3.7.17 on Docker Engine Engine 28.1.1, with whoami and small Alpine servers behind it, all on one Compose project. Each row is one broken setup:

SetupStatus/api/http/routersLog line
Request for a host no rule matches404Nothing for that hostNone
Container health starting or unhealthy404Router missingDEBUG only: Filtering unhealthy or starting container
Container stopped404Router missingNone
exposedByDefault=false, no traefik.enable=true404Router missingNone
No exposed port, no port label404Router missingservice "noport-tlab" error: port is missing
Docker socket not mounted404 for everythingOnly api@internal and dashboard@internalfailed to connect to the docker API at unix:///var/run/docker.sock
v2 rule `Host(a.test,b.test)`404disabled, unexpected number of parameters; got 2, expected one of [1]Same error
v2 matcher Headers(...)404disabled, unsupported function: HeadersSame error
Middleware that does not exist404disabled, middleware "auth@docker" does not existSame error
tls=true router, request over HTTP404 (HTTPS: 200)Enabled, tls setNone
Router on websecure only, request to web404Enabled, one entry pointNone
Wrong port502, 0 msEnabledDEBUG only: connect: connection refused
Container on a network Traefik is not on504, 30 sEnabledDEBUG only: i/o timeout

The log column assumes the default level. Without --log.level, our Traefik printed only ERR lines. Anything marked DEBUG appeared only after we set --log.level=DEBUG.

Two answers circulate, and both are half right#

Forum threads usually say a Traefik 404 means the containers are on different networks. They are not, in the sense that matters: a network mistake never gave us a 404. Traefik still builds the router and the service. It sends the request to an address it cannot reach and waits. After 30 seconds it answered 504 Gateway Timeout. If you see 404, stop changing networks.

The other answer is that you copied a v2 config into v3. That is one cause of several, and the API tells you in a second whether it is yours. Even then the result depends on the rule, as the v2 section below shows. Many rows in our table are simpler: a container still starting, or a router that the request did not fit.

A worked case: Uptime Kuma behind Traefik#

Take a Compose stack where Uptime Kuma has these labels, and https://status.example.com returns 404:

yaml
labels:
  - traefik.http.routers.kuma.rule=Host(`status.example.com`)
  - traefik.http.routers.kuma.entrypoints=websecure
  - traefik.http.routers.kuma.tls.certresolver=le

Start with the API. Suppose kuma@docker is missing. Then Traefik skipped the container, and the labels have not been read yet. Run docker ps. Suppose the status says (health: starting). Then Traefik is right to wait: until the healthcheck passes there is no router. Wait for healthy and try again.

Now suppose the router is there and enabled. Look at entryPoints. It says websecure, so a request to http://status.example.com will get 404 by design. The browser address must be the https:// one. If it already is, compare the rule with the address character by character. A www. or a missing subdomain is enough.

Now suppose the router is there and disabled. Read error. That line is the whole answer, and the sections below cover each one.

The router is missing: Traefik skipped the container#

Health. A container with a healthcheck is left out while Docker reports starting or unhealthy. Ours returned 404 while starting. Once Docker reported healthy, the next request returned 200, with no restart.

Stopped. A stopped container takes its router with it. Ours came back on docker start.

Not enabled. With --providers.docker.exposedByDefault=false, only containers labelled traefik.enable=true get routers. That is the safer setting, and Reverse proxy and TLS uses it. With the default true, our Traefik built a router for every container on the host: 39 from other projects, plus Traefik itself. Each got the default rule `Host({{ normalize .Name }}), which gave names like Host(traefik-tlab)`.

No port. An image that exposes no port and has no port label gets port is missing and no router. Add traefik.http.services.<name>.loadbalancer.server.port=3000.

No Docker socket. If /var/run/docker.sock is not mounted into the Traefik container, the provider cannot read any labels. Our instance logged failed to connect to the docker API and retried. Its API listed only api@internal and dashboard@internal, and every host got 404.

The router is disabled: read its error#

Traefik parsed the labels, built the router and then refused it. Every case we tested printed an ERR line at the default level and put the same text in the router's error field. One cause is a missing middleware: a middlewares=auth@docker label for a middleware defined nowhere, or defined with a typo. The others we found are v2 rules.

What a v2 rule actually does in v3#

The migration notes list the changes and say defaultRuleSyntax is set to v3 by default. They do not say what happens to an old rule. We tested four:

v2-style ruleRouterResult
`Host(a.test,b.test)`Disabled, unexpected number of parameters; got 2, expected one of [1]404
`Host(headers.test) && Headers(X-A,1)`Disabled, unsupported function: Headers404
`HostRegexp({sub:[a-z]+}.rx.test)`Enabled, no error404 for x.rx.test
`PathPrefix(/api/{id:[0-9]+})`Enabled, no error404 for /api/12

The first two fail loudly. The last two are the trap. v3 reads HostRegexp as a Go regular expression, so {sub:...} is taken as literal text that no real hostname contains. PathPrefix no longer takes patterns at all. Both routers look healthy on the dashboard and match nothing. Rewritten as `HostRegexp(^[a-z]+\.rx\.test$) and PathRegexp(^/api/[0-9]+)`, both returned 200 in our test.

Adding traefik.http.routers.<name>.ruleSyntax=v2 fixed the two-host rule in our test. Caddy vs Traefik covers why that is a stopgap, not a fix.

The router is enabled, but the request does not fit#

The router reference says a router with a tls section "will only handle HTTPS requests". Ours returned 404 over HTTP and 200 over HTTPS. The same reference says a router with no entryPoints is attached to all of them. Name only websecure and plain HTTP gets 404. That is often what you want, with a redirect on web. Name an entry point that does not exist, and the log says EntryPoint doesn't exist and No valid entryPoint for this router.

Lowest or first port, settled#

Traefik's two Docker reference pages disagree. The install page says that with several ports "Traefik uses the lowest port". The routing page says "the first exposed port". The v3.7 source settles it: getPort sorts the ports by number and returns the smallest. We checked it live. A container exposing 10000 and 9000, with docker inspect listing 10000 first, got traffic on 9000.

That catches images that already expose a port. traefik/whoami exposes 80. We started it on 8080 and added expose: ["8080", "9000"]. Traefik chose 80, and the request got 502. Set the port label for every image that exposes more than one port. A wrong port gives 502, the same refusal that Why Nginx Proxy Manager returns 502 Bad Gateway explains for NPM.

Several networks: Traefik picks one at random#

For a container on two networks, the routing page says Traefik "will randomly pick one". The source loops over a Go map, whose order changes each time. Across eight Traefik restarts, our two-network container got its other network six times and proxy twice. The wrong pick gave 504, not 404.

Name the network. The catch is the Compose prefix. A label traefik.docker.network=proxy on project tlab logged Could not find network named "proxy"... Maybe you're missing the project's prefix in the label?, then Defaulting to first available network. tlab_proxy worked. So did --providers.docker.network=tlab_proxy, 3 of 3. Neither helps a container that is not on that network at all. That one still gave 504. Why a Docker container cannot resolve DNS covers how Compose names and joins networks.

When this answer stops applying#

We tested the Docker provider on Engine 28.1.1 with label configuration only. Swarm mode, Kubernetes, the file provider, Podman and network_mode: host were not tested. Neither were allowEmptyServices or the allownonrunning label, which the docs say change how unhealthy and stopped containers are handled. A 404 that carries a router name in the access log came from the app, not from Traefik. It has its own causes, such as a path prefix the app does not expect. The v2 results above are for 3.7.17. The migration notes say v2 syntax support will be removed in the next major version.

What to do next#

Read /api/http/routers first and sort the problem into missing, disabled or enabled. Then match it to the table. If you see 502 or 504, the router worked and the port or network is wrong. If debugging labels has become the main cost of your proxy, Caddy vs Traefik weighs that against a config file. The Traefik profile lists its other upgrade traps.

Questions#

Does Traefik use the first or the lowest exposed port?

The lowest. Traefik's two reference pages disagree: the install page says lowest, the routing page says first. The v3.7 source sorts the container's ports by number and takes the smallest. We exposed 10000 and 9000, with docker inspect listing 10000 first, and Traefik sent traffic to 9000. Set traefik.http.services.<name>.loadbalancer.server.port whenever an image exposes more than one port.

Why does Traefik show 404 when the container is running?

Running is not enough if the container has a healthcheck. Traefik leaves out containers whose health is starting or unhealthy. Our test app returned 404 while Docker said starting, and 200 as soon as it turned healthy. Nothing appears in the log unless the level is DEBUG, where the line reads Filtering unhealthy or starting container. Check docker ps for the health status first.

Do Traefik v2 rules work in Traefik v3?

Some fail loudly, some fail silently, and some still parse. Host(a.test,b.test) disables the router with unexpected number of parameters; got 2, expected one of [1]. Headers(...) disables it with unsupported function: Headers. A v2 placeholder such as HostRegexp({sub:[a-z]+}.example.com) parses as a Go regexp, shows as enabled, and never matches. All three give 404. Adding the label ruleSyntax=v2 to the router made the two-host rule work again.

How do I see why Traefik ignores my container?

Enable the API and read /api/http/routers and /api/http/services. A router with status: disabled carries its reason in error. A router that is missing means the provider skipped the container. Then raise the log to --log.level=DEBUG: skipped containers, network warnings and the real cause of a 502 or 504 are logged below the default level. At the default, our log showed only errors.

Why does Traefik return 504 instead of 404?

Because a router matched and Traefik could not reach the container. In our tests that was always a network problem: the container sat on a Docker network Traefik was not on, or on two networks where Traefik picked the wrong one. The connection hung and Traefik gave up after 30 seconds. Put both on one network and name it with traefik.docker.network, including the Compose project prefix.

Sources#

Published . Last reviewed . Found something out of date? Tell us and we will fix it and log the change.