Blog

Traefik Service Discovery with Docker Labels

Set up Traefik v3 on a VPS so every Docker container with the right labels gets a route and HTTPS. Covers ports, networks, health checks and debugging.

Max Diamond 7 min read

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.

compose.yml
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=proxy tells Traefik to reach containers on the proxy network. The name: proxy line keeps that name fixed, so other Compose projects can join it.
  • entrypoints.websecure.http.tls.certresolver=le turns on TLS and Let’s Encrypt for every router on port 443, so your app labels don’t have to.
  • The letsencrypt volume keeps acme.json across 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:

compose.yml
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: true

A 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=80

The 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:

compose.yml
command:
# ...the flags above
- --api.insecure=true
ports:
# ...80 and 443
- '127.0.0.1:8080:8080'

Then list what Traefik discovered:

Terminal window
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.

ResponseUsual cause
404 page not foundNo router matched: missing traefik.enable=true, a typo in the Host rule, or the container is unhealthy
502 Bad GatewayWrong port: Traefik picked the lowest exposed port, or the app listens on 127.0.0.1 inside the container
504 Gateway TimeoutThe container isn’t on a network Traefik can reach
Browser certificate warningThe 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.

Run your apps on servers you own.

Connect a server you already run, or provision one on your cloud account. £5 a month covers the first three.