This post is mostly for myself: I find the Traefik documentation hard to navigate, so having figured this out in response to a question on Stack Overflow, I’m putting it here to help it stick in my head.

The question asks essentially how to perform port-based routing of requests to containers, so that a request for http://example.com goes to one container while a request for http://example.com:9090 goes to a different container.

Creating entrypoints#

A default Traefik configuration will already have a listener on port 80, but if we want to accept connections on port 9090 we need to create a new listener: what Traefik calls an entrypoint. We do this using the --entrypoints.<name>.address option. For example, --entrypoints.ep1.address=80 creates an entrypoint named ep1 on port 80, while --entrypoints.ep2.address=9090 creates an entrypoint named ep2 on port 9090. Those names are important because we’ll use them for mapping containers to the appropriate listener later on.

This gives us a Traefik configuration that looks something like:

  proxy:
    image: traefik:latest
    command:
      - --api.insecure=true
      - --providers.docker
      - --entrypoints.ep1.address=:80
      - --entrypoints.ep2.address=:9090
    ports:
      - "80:80"
      - "127.0.0.1:8080:8080"
      - "9090:9090"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

We need to publish ports 80 and 9090 on the host in order to accept connections. Port 8080 is by default the Traefik dashboard; in this configuration I have it bound to localhost because I don’t want to provide external access to the dashboard.

Routing services#

Now we need to configure our services so that connections on ports 80 and 9090 will get routed to the appropriate containers. We do this using the traefik.http.routers.<name>.entrypoints label. Here’s a simple example:

app1:
  image: docker.io/alpinelinux/darkhttpd:latest
  labels:
    - traefik.http.routers.app1.entrypoints=ep1
    - traefik.http.routers.app1.rule=Host(`example.com`)

In the above configuration, we’re using the following labels:

  • traefik.http.routers.app1.entrypoints=ep1

    This binds our app1 container to the ep1 entrypoint.

  • traefik.http.routers.app1.rule=Host(`example.com`)

    This matches requests with Host: example.com.

So in combination, these two rules say that any request on port 80 for Host: example.com will be routed to the app1 container.

To get port 9090 routed to a second container, we add:

app2:
  image: docker.io/alpinelinux/darkhttpd:latest
  labels:
    - traefik.http.routers.app2.rule=Host(`example.com`)
    - traefik.http.routers.app2.entrypoints=ep2

This is the same thing, except we use entrypoint ep2.

With everything running, we can watch the logs from docker-compose up and see that a request on port 80:

curl -H 'host: example.com' localhost

Is serviced by app1:

app1_1   | 172.20.0.2 - - [21/Jun/2022:02:44:11 +0000] "GET / HTTP/1.1" 200 354 "" "curl/7.76.1"

And that request on port 9090:

curl -H 'host: example.com' localhost:9090

Is serviced by app2:

app2_1   | 172.20.0.2 - - [21/Jun/2022:02:44:39 +0000] "GET / HTTP/1.1" 200 354 "" "curl/7.76.1"

The complete docker-compose.yaml file from this post looks like:

version: "3"

services:
  proxy:
    image: traefik:latest
    command:
      - --api.insecure=true
      - --providers.docker
      - --entrypoints.ep1.address=:80
      - --entrypoints.ep2.address=:9090
    ports:
      - "80:80"
      - "8080:8080"
      - "9090:9090"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock

  app1:
    image: docker.io/alpinelinux/darkhttpd:latest
    labels:
      - traefik.http.routers.app1.rule=Host(`example.com`)
      - traefik.http.routers.app1.entrypoints=ep1

  app2:
    image: docker.io/alpinelinux/darkhttpd:latest
    labels:
      - traefik.http.routers.app2.rule=Host(`example.com`)
      - traefik.http.routers.app2.entrypoints=ep2