tl;dr: A working Traefik v2 config implementing everything in the post can be found here.

Migration from Traefik v1 → v2

The official documentation on the differences between Traefik v1 and v2 can be found here, but I’ll go through the major points that’ll apply in this post.

Routers, Services and Middleware are the new black

Traefik v1 used the concepts of frontends and backends to represent how containers should be routed. Traefik v2 does away with this, and utilises routers, services and middleware to conduct routing. Entrypoints still exist in v2 configurations but they’re severely limited; you can no longer attach properties such as redirects or tls restrictions anymore. The official documentation describes the paradigm shift thusly:

Typically, a router replaces a frontend, and a service assumes the role of a backend, with each router referring to a service. However, even though a backend was in charge of applying any desired modification on the fly to the incoming request, the router defers that responsibility to another component. Instead, a dedicated middleware is now defined for each kind of such modification. Then any router can refer to an instance of the wanted middleware.

TLS configuration is now per router

Traefik v1 used to have a dedicated config section for ACME-based certificate acquisition and automagically applied these certificates to TLS entrypoints as required. With the shift to Traefik v2, this section is obselete and we now have certificateResolvers.

These resolvers can’t be applied to entryPoints like we’re used to in Traefik v1, we now need to apply these to routers instead. The official documentation on certificateResolvers can be found here.

Redirections are now per router

Traefik v1 allowed us to apply a blanket redirect upon an entrypoint to redirect all traffic somewhere else, i.e. redirecting all HTTP to HTTPS. Traefik v2 no longer allows this and instead requires us to specify any redirections we want as middleware upon routers. This gives us greater control on when we want to apply redirects but is pretty confusing when coming across from a Traefik v1 mindset.

Setting up Traefik

The examples in this post will operate on a docker-compose setup like the one below.

traefik2-demo
|── .env
├── README.md
├── config
│   └── traefik
│       ├── dynamic_conf.toml
│       ├── forward.ini
│       └── traefik.toml
└── docker-compose.yml

2 directories, 5 files

Our Traefik service is defined within our docker-compose.yml as such:

version: "3.7"
services:
  traefik:
    image: traefik
    ports:
      - "0.0.0.0:80:80"
      - "0.0.0.0:443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./config/traefik:/etc/traefik
    environment:
      - TZ=Australia/Sydney
      - DO_AUTH_TOKEN=${DO_AUTH_TOKEN}
    networks:
      - traefik
    labels:
      - traefik.enable=true
      - traefik.backend=traefik-api
      # Which network traefik should listen on
      - traefik.docker.network=traefik2_demo_traefik
      # Reverse proxy configuration - exposes the Traefik dashboard under traefik.${TRAEFIK_DOMAIN}
      - traefik.http.services.traefik.loadbalancer.server.port=8080
      # SSL configuration
      - traefik.http.routers.traefik-ssl.entryPoints=https
      - traefik.http.routers.traefik-ssl.rule=host(`traefik.${TRAEFIK_DOMAIN}`)
      - traefik.http.routers.traefik-ssl.tls=true
      - traefik.http.routers.traefik-ssl.tls.certResolver=le
      # Single Sign On middleware
      - traefik.http.routers.traefik-ssl.middlewares=sso@file

Note: docker-compose reads ${VARIABLE} substitutions from the .env file.

The important things to note from this definition are the volume mounts and labels. The volume mounts are pretty self explainatory, but the labels can be quite confusing – we’ll be going over them more later on.

Static vs dynamic configuration

We have two sources of config: static and dynamic. The official docs explain the difference between them pretty well so I’ll copy-paste that here:

Elements in the static configuration set up connections to providers and define the entrypoints Traefik will listen to (these elements don’t change often).

The dynamic configuration contains everything that defines how the requests are handled by your system. This configuration can change and is seamlessly hot-reloaded, without any request interruption or connection loss.

Static configuration - with comments

[log]
  level = "INFO"

[entryPoints]
  [entryPoints.http]
    address = ":80"
  [entryPoints.https]
    address = ":443"

[api]
  dashboard = true
  # Exposes the api without TLS, fine for our setup with TLS termination
  insecure = true

[providers]
  [providers.file]
    filename = "/etc/traefik/dynamic_conf.toml"
  [providers.docker]
    endpoint = "unix:///var/run/docker.sock"
    watch = true
    exposedbydefault = false
    # Adds a default routing rule for new containers 
    defaultrule = "Host(`.demo.carey.li`)"

# Used for certificate acquisition - explained later
[certificatesResolvers.le.acme]
  email = "hello@carey.li"
  storage = "/etc/traefik/acme.json"
  [certificatesResolvers.le.acme.dnsChallenge]
    provider = "digitalocean"

Dynamic configuration - with comments

[http.routers]
  # HTTP catchall router, matches all HTTP traffic and applies the httpsredirect middleware 
  [http.routers.https-only]
    entryPoints = ["http"]
    middlewares = ["httpsredirect"]
    rule = "HostRegexp(`{host:.+}`)"
    # Only here so the router is valid, is never actually used
    service = "noop"

[http.services]
  [http.services.noop.loadBalancer]
    [[http.services.noop.loadBalancer.servers]]
      url = "http://127.0.0.1"

[http.middlewares]
  # Used for SSO - explained later
  [http.middlewares.sso.forwardAuth]
    address = "http://traefik-fa:4181"
    authResponseHeaders = ["X-Forwarded-User"]
  # Does as it says on the tin
  [http.middlewares.httpsredirect.redirectScheme]
    scheme = "https"

Setting up HTTPS

Setting up your certificateResolver

If you want to acquire certificates via Lets Encrypt/ACME, you’ll need to setup a certificate resolver. Certificate resolvers support multiple ways of verifying whether a certificate should be issued for a given domain (tlsChallenge, httpChallenge, dnsChallenge). If wildcard certificates are required you’ll have to use dnsChallenge, but otherwise all three are equivalent. For the purposes of this post, we’ll be using dnsChallenge.

[certificatesResolvers.le.acme]
  email = "hello@carey.li"
  storage = "/etc/traefik/acme.json"
  [certificatesResolvers.le.acme.dnsChallenge]
    provider = "digitalocean"

Providing a DigitalOcean DNS token

The api key for the dnsChallenge is provided through the DO_AUTH_TOKEN environment within docker-compose.yml, which in turn is populated by the .env file:

DO_AUTH_TOKEN=do-api-token

Making a host HTTPS

This section is ugly, you’ll pretty much have to copy the following labels to all the services you want to enable SSL on:

labels:
  # Creates a router that listens on the https entrypoint
  - traefik.http.routers.service-ssl.entryPoints=https
  - traefik.http.routers.service-ssl.rule=host(`service.${TRAEFIK_DOMAIN}`)
  # Enables TLS on this router
  - traefik.http.routers.service-ssl.tls=true
  - traefik.http.routers.service-ssl.tls.certResolver=le

Enforcing HTTPS

Note that we didn’t attach to the http entrypoint, this was intentional! Without the attached http entrypoint, when a HTTP request comes in it’ll match against the HTTP catch-all router we defined earlier in our dynamic file config.

Traefik Services

This’ll apply the httpsredirect middleware we created in our dynamic config and enforce https everywhere!

Single Sign On

Using thomseddon/traefik-forward-auth, we can add authentication via Google in front of all our services. While this isn’t strictly necessary, I find it convenient and using this method shares authentication across all my services, reducing the need to sign on to every single service.

Traefik configuration

We define a forwardAuth middleware within our dynamic configuration like so:

[http.middlewares]
  # Used for SSO - explained later
  [http.middlewares.sso.forwardAuth]
    # Where to forward the requests credentials to, in order to verify the request.
    address = "http://traefik-fa:4181"
    # What header to inspect for the authenticated user, if any.
    authResponseHeaders = ["X-Forwarded-User"]

Traefik Forward Auth container

Deploying the forward auth container is pretty straight forward, we expose it over HTTPS, bind it to auth.${TRAEFIK_DOMAIN} and mount in our config.

version: "3.7"
services:
  traefik-fa:
    image: thomseddon/traefik-forward-auth
    container_name: traefik-fa
    volumes:
      - ./config/traefik/forward.ini:/forward.ini
    environment:
      - CONFIG=/forward.ini
    networks:
      - traefik
    labels:
      - traefik.enable=true
      - traefik.backend=traefik-fa
      - traefik.http.services.traefik-fa.loadBalancer.server.port=4181
      # SSL configuration
      - traefik.http.routers.traefik-fa-ssl.entryPoints=https
      - traefik.http.routers.traefik-fa-ssl.rule=host(`auth.${TRAEFIK_DOMAIN}`)
      - traefik.http.routers.traefik-fa-ssl.middlewares=sso@file
      - traefik.http.routers.traefik-fa-ssl.tls=true
      - traefik.http.routers.traefik-fa-ssl.tls.certResolver=le

Forward Auth config

# Cookie signing nonce, replace this with something random
secret = secret-nonce

# Google oAuth application values - you can follow https://rclone.org/drive/#making-your-own-client-id to make your own
providers.google.client-id = google-client-id
providers.google.client-secret = google-client-secret

log-level = debug

# Replace demo.carey.li with your own ${TRAEFIK_DOMAIN}
cookie-domain = demo.carey.li
auth-host = auth.demo.carey.li

# Add authorized users here
whitelist = hello@carey.li
whitelist = another@carey.li

Conclusion

After configuring everything here, your Traefik instance should be up and running!