Traefik v2 as a Reverse Proxy for Docker services
1675 words, 8 minutes

Træfik - pronounced traffic - is a popular open-source reverse proxy that’s also capable of load-balancing incoming requests, while integrating easily with Docker containers. This is a guide to configuring Traefik v2.x to proxy Docker containers, which assumes familiarity with running Docker and its containerisation concepts.

A reverse proxy is a server that sits in front of other services, and is responsible for forwarding a client request received on a WAN-side open socket, to the correct socket on the internal LAN side.

Version 2 is a major breaking change, in return for some attractive new features. The headline feature - and the one that many were waiting for - is support for arbitrary TCP and UDP protocols, not limited to just HTTP/S web traffic. This allows Traefik to proxy other types of connection like SSH or MQTT without having to directly open ports.

How it works

There are three key components of Traefik - entrypoints, routers, and services. These components are software-defined in Traefik’s configuration.

Traefik’s site has a nice diagram which shows a good overview of how they sit together:

Traefik Architecture

In summary, each incoming request is:

  1. Received by an entrypoint which is listening for requests on a port,
  2. Matched to a router based on pre-defined rules, where the request is analysed and optionally processed by middleware components,
  3. Passed to a service where the request is forwarded to the actual backend - in this case, a single Docker container.

The last concept to be aware of is a provider - these are modules within Traefik that provide external integrations. The Docker provider is the magic glue that allows Traefik to be configured through Docker labels.

Once these four components are defined, Traefik will automatically create the routes to allow incoming requests to reach your Docker services.

Installing Traefik

The quickest way to ‘install’ is to use the Docker image. This means receiving all requests into one container, and using it to route to others (also known as Traefik-ception).

With Docker running on the host machine, a basic docker run command will give you some clues as to what inputs it expects:

docker run -d -p 8080:8080 -p 80:80 -p 443:443 \ -v $PWD/traefik.toml:/etc/traefik/traefik.toml \ traefik:v2.2

There are three port mappings:

  • 8080 for the Traefik dashboard UI (useful for debugging but eventually won’t be necessary since we’ll access it through Traefik itself)
  • 80 for listening to HTTP requests
  • 443 for listening to HTTPS requests

There is one file passed in, called traefik.toml - this is our configuration file.

The image is based on Traefik version 2.2. These configurations are not compatible with v1.x. Configuration entries and Docker labels are different for v1.x - see docs.

Static & Dynamic Configuration

Traefik uses a mix of static and dynamic configuration methods.

It’s a little confusing at first, but the general rule is that infrequently changing parameters are defined in static configuration, while frequently changing parameters are defined in dynamic configuration; as you’d expect.

Static configurations are defined in one or more .toml formatted configuration files. Traefik is given a single ‘master’ configuration file, which can refer to others. Below, traefik.toml is the master configuration file, that refers to a second file (rules.toml) containing more definitions.

Dynamic Configuration - in the context of Docker containers - are simply Docker labels that are attached to your containers. A useful way of defining the same information, but neatly organised with each container.

Static Config - entrypoints

Entrypoints are defined in static configuration, since the listening port(s) are unlikely to change.

Let’s create a traefik.toml file with two entrypoints. One for HTTP, and one for HTTPS. We’ll call the HTTP one web, and the HTTPS one websecure.

[entryPoints]
	[entryPoints.web]
		address = ":80"
		[entryPoints.web.http]
			[entryPoints.web.http.redirections]
				[entryPoints.web.http.redirections.entryPoint]
					to = "websecure"
					scheme = "https"
					permanent = true
					priority = 1000
	[entryPoints.websecure]
		address = ":443"
		[entryPoints.websecure.http.tls]
			[[entryPoints.websecure.http.tls.domains]]
				main = "vince.id"
			[[entryPoints.websecure.http.tls.domains]]
				main = "*.vince.id"

The HTTP entrypoint is configured to 301 redirect to the HTTPS endpoint. (The priority flag is a temporary fix for v2.2 for this ‘global’ redirect method.) The TLS domains define the SANs that Traefik handles, and should include all the host URLs that you plan on proxying behind the endpoint, including the root domain.

Static Config - providers

The Docker provider that is built-in to Traefik is something to statically configure - which means adding it to our traefik.toml. It is this provider that subsequently allows us to dynamically configure routers and services that we will define with Docker labels.

After the entrypoints, add a new provider section in the traefik.toml file:

[providers]
  [providers.docker]
    endpoint = "unix:///var/run/docker.sock"
    watch = true
    exposedbydefault = false
    network = "vince"

The provider needs the Docker daemon socket to monitor Docker events such as containers stopping and starting; and of course to read the labels that it will use to create the Traefik routes.

  • The watch flag tells Traefik to continue to monitor Docker events.
  • exposedbydefault means that every container needs an extra label to enable Traefik to route. This is up to you, but I prefer a guard against accidentally exposing a container you didn’t intend.
  • network is the name of the default network you want Traefik to use to communicate with containers. This can be overridden with a label on a container.

Exposing the Docker daemon socket to any third party service, including Traefik, has security implications. Do not do this in production, unless you’ve accounted for it in your threat models, and are satisfied with Traefik’s source code, image build, and distribution mechanisms.

Completing the Static Config

There’s a few extra things to add to our traefik.toml before it’s finished:

[api]
	dashboard = true

[log]
	level = "INFO"


[certificatesResolvers]
	[certificatesResolvers.letsencrypt]
		[certificatesResolvers.letsencrypt.acme]
			email = "[email protected]"
			storage="/etc/traefik/acme/acme.json"
			[certificatesResolvers.letsencrypt.acme.dnsChallenge]
				provider = "cloudflare"
				delayBeforeCheck = 300

The api entry is where we specify that the Traefik dashboard should be enabled.

We’ll set the log level to INFO. Note that to see any useful output for debugging you will need to switch to DEBUG.

Finally, the certificateResolvers section allows Traefik to automatically handle TLS certificate generation with an ACME provider. In my case, I’m using Let’s Encrypt and providing details of my DNS (with Cloudflare) to automatically handle DNS verification with Let’s Encrypt.

Depending on your DNS provider, you’ll need to provide credentials through environment variables - see the supported DNS providers here.

Dynamic Config - routers and services

At this point, the Traefik container will start - but with no other containers running that have the relevant labels, there are no routers, no services, and so nothing to proxy.

There are three labels to define on each container that should be proxied (or four if you include the enable).

This simple example shows the Docker labels from a docker-compose file, relevant to Traefik:

labels:
	- "traefik.enable=true"
	- "traefik.http.routers.whoami.rule=Host(`whoami.vince.id`)"
	- "traefik.http.routers.whoami.tls.certresolver=letsencrypt"
	- "traefik.http.services.whoami.loadbalancer.server.port=80"

The traefik.http.routers.whoami.rule label defines a new HTTP router, called whoami - there should be a router for each Docker service, so it makes sense to name it the same.

The router matches on a rule. In this case, it’s the SNI of the request. If a user tries to access whoami.vince.id, it reaches the HTTPS entrypoint, then Traefik will pass it to on to this router, since this rule was triggered.

The TLS certResolver option tells Traefik that it should use the certificate resolver called ’letsencrypt’ which was defined above in the toml config file.

After reaching the router, additional middleware can be defined if the request needs extra processing or redirection (to authenticate the user, for example). There’s none here in this case, but this is where something like Authelia SSO can integrate with Docker containers.

The final label "traefik.http.services.whoami.loadbalancer.server.port=80" is the definition of a service. Like routers, in most cases there should be one Traefik service definition for each Docker service. The port (defined under the server loadbalancer) tells Traefik which port to forward the request to, inside the container.

Traefik actually has port detection built into the Docker provider, so specifying the port isn’t necessary if the container only exposes one port. Rather than a port expose entry in the compose file, I prefer the explicitness of a port definition on the Traefik service.

In summary - each container will need these three labels, given that exposedbydefault is set to false, and there’s no extra middleware or other desired configuration.

Dashboard Access in Docker with docker-compose

The Traefik image will need some extra attention to expose the web dashboard, if it’s running alongside the containers its proxying within Docker. In Traefik v2, the dashboard is a service that can be defined within the router with the new @ syntax. Along with the port 8080 definition where the dashboard is, and an HTTP basic auth challenge in front to protect it the labels section for Traefik looks like:

labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.rule=Host(`traefik.${DOMAINNAME}`)"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
      - "traefik.http.routers.api.middlewares=auth"
      - "traefik.http.routers.api.tls"
      - "traefik.http.middlewares.auth.basicauth.users=${HTTP_USERNAME}:${HTTP_PASSWORD}"

docker-compose and toml files

Putting all this together, we end up a traefik.toml static configuration file, and a docker-compose file to bring up Traefik and an example whoami container, with their respective dynamic configuration through Docker labels.

traefik.toml:

[entryPoints]
  [entryPoints.web]
    address = ":80"
    [entryPoints.web.http]
      [entryPoints.web.http.redirections]
        [entryPoints.web.http.redirections.entryPoint]
          to = "websecure"
          scheme = "https"
          permanent = true
          priority = 1000
  [entryPoints.websecure]
    address = ":443"
    [entryPoints.websecure.http.tls]
      [[entryPoints.websecure.http.tls.domains]]
        main = "vince.id"
      [[entryPoints.websecure.http.tls.domains]]
        main = "*.vince.id"

[providers]
  [providers.docker]
    endpoint = "unix:///var/run/docker.sock"
    watch = true
    exposedbydefault = false
    network = "vince"
  [providers.file]
    watch = true
    filename = "/etc/traefik/rules.toml"

[api]
  dashboard = true

[log]
  level = "INFO"

[certificatesResolvers]
  [certificatesResolvers.letsencrypt]
    [certificatesResolvers.letsencrypt.acme]
      email = "[email protected]"
      storage="/etc/traefik/acme/acme.json"
      [certificatesResolvers.letsencrypt.acme.dnsChallenge]
        provider = "cloudflare"
        delayBeforeCheck = 300
   

docker-compose.yml:

version: '3'

services:
  traefik:
    hostname: traefik
    image: traefik:v2.2
    container_name: traefik
    restart: unless-stopped
    domainname: ${DOMAINNAME}
    networks: 
      - vince
    ports:
      - "80:80"
      - "443:443"
    environment:
      - CF_API_EMAIL=${CLOUDFLARE_EMAIL}
      - CF_API_KEY=${CLOUDFLARE_API_KEY}
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.entrypoints=websecure"
      - "traefik.http.routers.api.rule=Host(`traefik.${DOMAINNAME}`)"
      - "traefik.http.routers.api.service=api@internal"
      - "traefik.http.services.traefik.loadbalancer.server.port=8080"
      - "traefik.http.routers.api.middlewares=auth"
      - "traefik.http.routers.api.tls"
      - "traefik.http.middlewares.auth.basicauth.users=${HTTP_USERNAME}:${HTTP_PASSWORD}"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ${USERDIR}/mina/infrastructure/traefik.toml:/etc/traefik/traefik.toml
      - ${USERDIR}/mina/infrastructure/rules.toml:/etc/traefik/rules.toml
      - ${USERDIR}/docker/traefik/acme/:/etc/traefik/acme/
      - ${USERDIR}/docker/shared:/shared
			
	whoami:
    image: containous/whoami
    restart: unless-stopped
    container_name: whoami
    networks:
      - "vince"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.whoami.rule=Host(`whoami.${DOMAINNAME}`)"
			# An example of a label for router middleware - e.g. Authelia
      - 'traefik.http.routers.whoami.middlewares=authelia@docker'