Traefik with Let’s Encrypt and Cloudflare (pt 1)

Recently I decided to rebuild one of my homelab servers. Previously I was using Nginx as my reverse proxy but I decided to switch to Traefik since I have been using it professionally for some time now. One of the reasons I like Traefik is that it is stupid simple to set up certificates and when I am using it with Docker I don’t have to worry about a bunch of configuration files. If you aren’t familiar with how Traefik works with Docker, here is a brief example of a docker-compose.yaml

version: '3'

services:
  reverse-proxy:
    # The official v2 Traefik docker image
    image: traefik:v2.11
    # Enables the web UI and tells Traefik to listen to docker
    command:
      - --api.insecure=true
      - --providers.docker=true
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      # Set up LetsEncrypt
      - --certificatesresolvers.letsencrypt.acme.dnschallenge=true
      - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare
      - [email protected]
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      # Redirect all http requests to https
      - --entryPoints.web.http.redirections.entryPoint.to=websecure
      - --entryPoints.web.http.redirections.entryPoint.scheme=https
      - --entryPoints.web.http.redirections.entrypoint.permanent=true
      - --log=true
      - --log.level=INFO
    # Needed to request certs via lets encrypt
    environment:
      - CF_DNS_API_TOKEN=[redacted]
    ports:
      # The HTTP port
      - "80:80"
      - "443:443"
      # The Web UI (enabled by --api.insecure=true)
      - "8080:8080"
    volumes:
      # So that Traefik can listen to the Docker events
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # Used for storing letsencrypt certificates
      - ./letsencrypt:/letsencrypt
      - ./volumes/traefik/logs:/logs
    networks:
      - traefik
  ots:
    image: luzifer/ots
    container_name: ots
    restart: always
    environment:
      REDIS_URL: redis://redis:6379/0
      SECRET_EXPIRY: "604800"
      STORAGE_TYPE: redis
    depends_on:
      - redis
    labels:
      - traefik.enable=true
      - traefik.http.routers.ots.rule=Host(`ots.example.com`)
      - traefik.http.routers.ots.entrypoints=websecure
      - traefik.http.routers.ots.tls=true
      - traefik.http.routers.ots.tls.certresolver=letsencrypt
      - traefik.http.services.ots.loadbalancer.server.port=3000
    networks:
      - traefik
  redis:
    image: redis:alpine
    restart: always
    volumes:
      - ./redis-data:/data
    networks:
      - traefik
networks:
  traefik:
    external: true


In part one of this series I will be going over some of the basics of Traefik and how dynamic routing works. If you want to skip to the good stuff and get everything configured with Cloudflare, you can skip to part 2.

This example set’s up the primary Traefik container which acts as the ingress controller as well as a handy One Time Secret sharing service I use. Traefik handles routing in Docker via labels. For this to work properly the services that Traefik is trying to route to all need to be on the same Docker network. For this example we created a network called traefik by running the following:

docker network create traefik

Let’s take a look at the labels we applied to the ots container a little closer:

    labels:
      - traefik.enable=true
      - traefik.http.routers.ots.rule=Host(`ots.example.com`)
      - traefik.http.routers.ots.entrypoints=websecure
      - traefik.http.routers.ots.tls=true
      - traefik.http.routers.ots.tls.certresolver=letsencrypt
      - traefik.http.services.ots.loadbalancer.server.port=3000

traefik.enable=true – This should be pretty self explanatory but it tells Traefik that we want it to know about this service.

traefik.http.routers.ots.rule=Host('ots.example.com') - This is where some of the magic comes in. Here we are defining a router called ots. The name is arbitrary in that it doesn’t have to match the name of the service but for our example it does. There are many rules that you can specify but the easiest for this example is host. Basically we are saying that any request coming in for ots.example.com should be picked up by this router. You can find more options for routers in the Traefik docs.

– traefik.http.routers.ots.entrypoints=websecure
– traefik.http.routers.ots.tls=true
– traefik.http.routers.ots.tls.certresolver=letsencrypt

We are using these three labels to tell our router that we want it to use the websecure entrypoint, and that it should use the letsencrypt certresolver to grab it’s certificates. websecure is an arbitrary name that we assigned to our :443 interface. There are multiple ways to configure this, I choose to use the cli format in my traefik config:

“`

    command:
      - --api.insecure=true
      - --providers.docker=true
      # Our entrypoint names are arbitrary but these are convention.
      # The important part is the port binding that we associate.
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443

These last label is optional depending on your setup but it is important to understand as the documentation is a little fuzzy.

– traefik.http.services.ots.loadbalancer.server.port=3000

Here’s how it works. Suppose you have a container that exposes multiple ports. Maybe one of those is a web ui and another is something that you don’t want exposed. By default Traefik will try and guess which port to route requests to. My understanding is that it will try and use the first exposed port. However you can override this functionality by using the label above which will tell Traefik specifically which port you want to route to inside the container.

The service name is derived automatically from the definition in the docker compose file:


ots: # This will become the service name
image: luzifer/ots
container_name: ots