Posted on :: Tags: , ,

Basic Configuration

This is a copy-paste ready collection of recipes, aimed to run dockerized Traefik proxy and proxying other services. This recipes tailored to run in Docker's standalone mode. You can mix and match them at your digression, but be aware that running same recipes in Docker Swarm may require some adaptation.
Traefik can accept configuration in various ways. I will use Docker labels for this purpose.

Traefik: The Proxy Itself

Here is a basic Traefik service, configured using command line parameters and Docker service labels:

services:
  traefik:
    image: traefik:3.4
    restart: unless-stopped
    command:
      # if you need more details in logs
      # change `INFO` to `DEBUG`
      - --log.level=INFO
      - --accesslog=true
      # provider options
      # for swarm swap coment in next two lines
      - --providers.docker=true
      # - --providers.docker.swarmMode=true
      - --providers.docker.exposedbydefault=false
      # entrypoints
      - --entrypoints.web.address=:80
    ports:
      - "80:80"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - letsencrypt:/letsencrypt
    labels:
      traefik.enable: false
    networks:
      - internal

networks:
    internal:

volumes:
    letsencrypt:

Whoami: Our Test Subject

This service will become our guinea pig for all following experiments. I will use traefik/whoami image, but really, you can use anything you want. I choose it, because it can display some information about requests, which will be handy.

A basic service definition is not complicated:

whoami:
    image: "traefik/whoami"
    labels:
      # mark a container for Traefik to notice it
      traefik.enable: true
      # let Traefil know that we want this domain name for a container
      traefik.http.routers.whoami.rule: Host(`whoami.localhost`)
      # let Traefik know to expect a container answer that port
      traefik.http.services.whoami.loadbalancer.server.port: 80
    networks:
      - internal

Let's check how everything works. Deploy the stack and open http://whoami.localhost in your browser.

Basic configuration working as intended

Basic configuration working as intended

The Headers

Here's our request headers, shown by whoami service. Those are pretty common headers, with some exceptions.

Hostname: 82add48227da
IP: 127.0.0.1
IP: ::1
IP: 172.20.0.2
RemoteAddr: 172.20.0.3:36092
GET / HTTP/1.1
Host: whoami.localhost
User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:129.0) Gecko/20100101 Firefox/129.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/png,image/svg+xml,*/*;q=0.8
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en;q=0.5
Priority: u=0, i
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: cross-site
Upgrade-Insecure-Requests: 1
X-Forwarded-For: 172.20.0.1
X-Forwarded-Host: whoami.localhost
X-Forwarded-Port: 80
X-Forwarded-Proto: http
X-Forwarded-Server: 9f5eb482116f
X-Real-Ip: 172.20.0.1

Take a look at highlighted lines. Those headers are so-called Forwarding headers. They are not part of original request and added by Traefik itself and are essential to proper proxy-to-app interaction. Laravel application, for example, can detect SSL is available and generate correct links based on X-Forwarded-Proto header.

More info on X-Forwarded-Proto and other headers can be found here.

HTTPS

This addition requred with any chalenge

  traefik:
    #...
    command:
      #...
      # ssl
      - --entrypoints.websecure.http3
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entryPoint.to=websecure
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - --entrypoints.web.http.redirections.entrypoint.permanent=true
      # ...
    ports:
      #...
      - "443:443/tcp"
      - "443:443/udp"
      #...
    #...

For Traefik, we're adding a new entrypoint called websecure, assigning port 443 to it and establishing permanent redirects to it. Scheme for redirects will be changed to https. Also, for Docker, we need to mention protocols now. By default, Docker will open port for TCP traffic only. HTTP3 uses UDP as a transport protocol, hence same port with another protocol.

Self-Signed certificate

Configuration above should be sufficient to provide us with self-signed certificate, generated by Traefik. We need to add a couple of lines to service container though:

  whoami:
    labels:
      #...
      # SSL
      traefik.http.routers.whoami.tls: true
      #...

Deploy the stack and try to access http://whoami.localhost or https://whoami.localhost

Firefox complaining about self-signed certificate

Firefox complaining about self-signed certificate

Advanced... button will reveal additional details

Advanced... button will reveal additional details

Self-Signed certificate datails

Self-Signed certificate datails

Whoami page, served via HTTPS

Whoami page, served via HTTPS

Its totally possible to use your own certificates for this, but is somewhat outside our current scope.

After modifications done, re-deploy the stack and check, if it still works.
In our case - the local deployment, Traefik will issue a self-signed certificate and browser (Firefox in my case) will not be happy about it. Click Accept risk and continue to let it know that it's ok. The browser will open whoami page as usual. The only reminder will be a crossed out https badge in address bar.
Note, that X-Forwarded-Port is now 443 and X-Forwarded-Proto - https.

Now, for Real

Lets setup TLS for our services. As a bonus we will get HTTP2 and HTTP3 running with almost no additional configuration.

  traefik:
    #...
    command:
      #...
      # ssl
      - --entrypoints.websecure.http3
      - --entrypoints.websecure.address=:443
      - --entrypoints.web.http.redirections.entryPoint.to=websecure
      - --entrypoints.web.http.redirections.entryPoint.scheme=https
      - --entrypoints.web.http.redirections.entrypoint.permanent=true
      # ssl cert provider
      - --certificatesresolvers.myresolver.acme.tlschallenge=true
      # - --certificatesresolvers.myresolver.acme.httpchallenge=true
      # - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.myresolver.acme.email=${LE_EMAIL:?LE_EMAIL not set}
      - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json
      # - --certificatesresolvers.myresolver.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/director
      #...
    ports:
      #...
      - "443:443/tcp"
      - "443:443/udp"
      #...
    #...

Lines from 12 to 17 are options related to ACME protocol which used to automatically issue and renew TLS certificates. In order to get certificate issued, you need to prove your domain ownership. There are a couple strategies available for this:

Option called email defines an email which, will be used to create an ACME account. The storage option defines where to store issued certificate. Ideally storage should point to persistent storage of some sort. A Docker volume, for example.

Using dnsChallenge and a little DNS trickery you can obtain certificate for services running in your home lab without exposing them to Internet or using tunnels.
More on that - later.

Additionally, you can use caserver option to define your ACME server of choice. In case of Let's Encrypt you may want to set it to https://acme-staging-v02.api.letsencrypt.org/director, while experimenting. It will instruct Traefik to use LE's staging servers, and save you from getting temporary banned for frequent requests. I believe you can plug your own CA authority here (something like this, this or this) and get it working too. Main point - ACME protocol support. Not tested, though.

Now we need to instruct Traefik to use TLS for particular service:

  whoami:
    labels:
      #...
      # SSL
      traefik.http.routers.whoami.tls: true
      traefik.http.routers.whoami.entrypoints: websecure
      #...

We're instructing Traefik to use TLS for our service and use entry point called websecre for traffic. Latter may be unnecessary, but better to have that line there anyway.

Before deploying the stack export variable called LE_EMAIL containing a valid email address.

The End?

I tried my best to fit all my recipes/snippets in one post. They just keep piling up, as I scanned through my notes. To that extent, I managed to fit just a basic setup here. And I want to post something already! So, I will call it the end of part one. Stay tuned for part two!