Traefik’s service discovery means you never edit a proxy config file to add an app. Traefik watches the Docker socket, and when a container starts with the right labels, it creates the route, picks the port and requests the certificate. When the container stops, the route goes away.
This guide sets up Traefik v3 on a VPS with Docker Compose, exposes a container with two labels, and covers the rules that decide whether discovery works: which port Traefik picks, which network it uses, and which containers it skips.
How Traefik discovers containers
Traefik’s Docker provider reads the Docker API through the socket. For every running container, it reads the labels and builds two things:
- A router, which matches requests, such as
Host(`app.example.com`), and sends them on. - A service, which is the container’s IP address on a shared network and a port.
Traefik listens for Docker events, so a new container gets a route within seconds, without a restart or a reload. With exposedbydefault=false, Traefik ignores every container that doesn’t set traefik.enable=true. Keep it off, or Traefik creates a route for every container on the server, databases included.
Set up Traefik
This Compose file runs Traefik with the Docker provider, redirects HTTP to HTTPS, and gets certificates from Let’s Encrypt with the HTTP challenge. Port 80 must be open for the challenge, and each domain must point at the server before its first request.
services: traefik: image: traefik:v3.7 command: - --providers.docker=true - --providers.docker.exposedbydefault=false - --providers.docker.network=proxy - --entrypoints.web.address=:80 - --entrypoints.web.http.redirections.entrypoint.to=websecure - --entrypoints.web.http.redirections.entrypoint.scheme=https - --entrypoints.websecure.address=:443 - --entrypoints.websecure.http.tls.certresolver=le - --certificatesresolvers.le.acme.email=you@example.com - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web ports: - '80:80' - '443:443' volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - letsencrypt:/letsencrypt networks: - proxy restart: unless-stopped
networks: proxy: name: proxy
volumes: letsencrypt:Start it with docker compose up -d. Each line does one job:
providers.docker.network=proxytells Traefik to reach containers on theproxynetwork. Thename: proxyline keeps that name fixed, so other Compose projects can join it.entrypoints.websecure.http.tls.certresolver=leturns on TLS and Let’s Encrypt for every router on port 443, so your app labels don’t have to.- The
letsencryptvolume keepsacme.jsonacross restarts. Without it, Traefik requests new certificates every time it starts and can hit Let’s Encrypt’s rate limits.
Use your real email address. Let’s Encrypt rejects example.com addresses.
Expose a container with labels
Any container on the proxy network gets a route with two labels:
services: whoami: image: traefik/whoami:v1.11 labels: - traefik.enable=true - traefik.http.routers.whoami.rule=Host(`whoami.example.com`) networks: - proxy restart: unless-stopped
networks: proxy: external: trueA request to http://whoami.example.com gets a 301 to HTTPS, and the HTTPS request reaches the container. You didn’t set a port or a service: Traefik read the port from the image and created a service with the same name as the router.
The app can live in a different Compose project from Traefik. external: true joins the proxy network that the Traefik project created.
Rules that decide whether discovery works
Each of these was tested against Traefik v3.7 with the setup above.
Traefik picks the lowest exposed port
If the image exposes one port, Traefik uses it. If it exposes more than one, Traefik uses the lowest, which is often not the one your app serves on. An image that exposes ports 70 and 80 gets routed to 70, and every request returns 502 Bad Gateway.
Set the port when the image exposes more than one, or none:
traefik.http.services.whoami.loadbalancer.server.port=80The port is the one inside the container, not a published host port. You don’t need ports: on the app at all. Traffic reaches it over the Docker network.
The container must share a network with Traefik
A container with labels but no shared network still gets a router, because Traefik reads labels from every container. Traefik can’t reach its IP address, so requests return 504 Gateway Timeout. Add the container to the proxy network.
A container on several networks, such as proxy and a database network, needs Traefik to know which one to use. The providers.docker.network flag sets it for every container. To set it on one container, add traefik.docker.network=proxy.
Unhealthy containers get no traffic
If the image or Compose file defines a Docker HEALTHCHECK and the container is unhealthy, Traefik drops it from the service. With one container, requests return 404. This protects users from a broken instance, but it also means a health check that is wrong takes the app offline. Check docker ps for (unhealthy) when a route that worked starts returning 404.
No traefik.enable, no route
With exposedbydefault=false, a container with router labels but without traefik.enable=true is ignored and returns 404.
Debug a route that doesn’t work
Turn on the API and dashboard while you debug. Add these flags to Traefik, and publish port 8080 on localhost only:
command: # ...the flags above - --api.insecure=trueports: # ...80 and 443 - '127.0.0.1:8080:8080'Then list what Traefik discovered:
curl -s http://127.0.0.1:8080/api/http/routers | jq '.[] | {name, rule, service, status}'curl -s http://127.0.0.1:8080/api/http/services | jq '.[] | {name, servers: .loadBalancer.servers}'The services list shows the exact URL Traefik sends traffic to, such as http://172.18.0.4:70. A wrong port or a missing server there explains most 502 and 404 responses. Remove --api.insecure when you’re done. For a dashboard that stays on, put it behind a router with authentication.
| Response | Usual cause |
|---|---|
404 page not found | No router matched: missing traefik.enable=true, a typo in the Host rule, or the container is unhealthy |
502 Bad Gateway | Wrong port: Traefik picked the lowest exposed port, or the app listens on 127.0.0.1 inside the container |
504 Gateway Timeout | The container isn’t on a network Traefik can reach |
| Browser certificate warning | The ACME request failed. Check Traefik’s logs for Unable to obtain ACME certificate, usually DNS not pointing at the server yet, or port 80 closed |
Is mounting the Docker socket safe?
Traefik needs the socket to read containers and events. The :ro flag stops Traefik from changing the socket file, but it doesn’t limit the Docker API behind it. Anything that can talk to the socket can start containers, which is root on the host.
To limit Traefik to read-only API calls, put a Docker socket proxy between them. The proxy allows GET requests for containers and events and blocks the rest, and Traefik connects to it with --providers.docker.endpoint=tcp://socket-proxy:2375.
Traefik on Serversinc
Serversinc installs Traefik on every server it sets up, with the Docker provider, the web and websecure entry points and a Let’s Encrypt resolver called le that uses the HTTP challenge. Every application joins the traefik network when it deploys, so the network rule above is handled.
Enter a domain on the application and Serversinc writes the router rule label for you. See Add a domain for the steps.
FAQ
What is noop@internal in Traefik?
A built-in service that does nothing. Traefik uses it for routers that never forward a request, such as the HTTP to HTTPS redirect router web-to-websecure@internal. Seeing it in the dashboard is normal.
Do I need to restart Traefik when I add a container?
No. Traefik reads Docker events and adds the route as soon as the container starts.
Can Traefik discover containers on other servers?
Not through the Docker provider. It reads one Docker daemon. For several servers, run Traefik on each one, or use a provider that reads a shared source such as Swarm or a key-value store.
Should I use traefik:latest?
Pin the major and minor version, such as traefik:v3.7, and upgrade on purpose. A new major version can change the configuration format, as v2 to v3 did.