Set Up Traefik on Docker - A Modern Reverse Proxy Solution

For many years, I used NGINX as a reverse proxy. It's pretty bulletproof as long as you have your configuration set up properly from the get-go. There's even a number of ways to automatically create new SSL certs for new containers but this mostly has to be done in a separate container itself. This makes the process of upgrading, testing, rolling back, patching and whatnot a bit of a mither. Enter Traefik.

If you saw the comparison post on why I chose Traefik vs Caddy then you'll know I liked both of them, but I'm glad that I made the choice that I did. Since that post, Traefik has gone fully 2.0, updated a lot of their documentaion and just keeps getting better and better. I will still keep a cursory eye on Caddy but I doubt I'll be switching over any time soon.

Traefik support the defining of configuration via command line arguments or via a TOML file. To keep things simple and not require the backing up of an extra file, I run my container with command line arguments. This means all of the config is stored within the docker-compose file and makes it rather easy to share. The full config will be at the end of the post, but for now, let's go through it in smaller chunks.

Traefik Container Walkthrough

version: "3.3"

services:
  traefik:
    container_name: traefik
    image: traefik:v2.2.0
    command:

Obviously this is just the headings and whatnot, what you name the service/container is up to you. The key bit is that you should peg your container to an actual version, not just latest, to save upgrading your container in the future and encountering breaking changes which destroys your reverse proxy. You will get log messages from time to time about upgrades if they are available. The command section is where we pass the command line arguments to the Traefik binary that will run in the container.

#- --log.level=DEBUG

Useful if you need to debug/troubleshoot your config (obviously) but it does produce a LOT of noise so disable once you're up and running.

# Entrypoints
      - --entrypoints.http.address=:80
      - --entrypoints.https.address=:443

Entrypoints are essentially where you're expecting traffic to come in to Traefik. Here, we set the address of an entrypoint called http and https to their relevant ports. You could very well define - --entrypoints.irc.address=:6697 if you liked.

# Provider Info
      - --providers.docker

Again, self-explannatory really. This tells Traefik that we're dealing with docker containers. There are plenty of other supported providers out there too.

# Certificate Resolver Info
      - --certificatesresolvers.le.acme.email=your@email.domain
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true

This is probably my favourite part, the auto SSL cert retrieval. The le portion is configurable and this is what you'll reference in your container setups, along with a Host(`sub.domain.tld`) rule. See the bottom of this post for an example using my Jenkins container.

That's it for the basics, really. But, because I'm usually not content with the basics, here's some more advanced config.

labels:
      # Middleware Redirect
      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
      # Global HTTP -> HTTPS Redirect
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=http"
      - "traefik.http.routers.redirs.middlewares=https-redirect"

This section creates a middleware element and a router to redirect all HTTP traffic to HTTPS. Setting this as a label on the Traefik container itself means that you don't have to create individual middleware for each new container and then add the redir config into those containers routers in order for this to function. Much simpler and I haven't found a problem with it thus far (I'm running 8-10 containers at any one time).

ports:
      - "80:80"
      - "443:443"
    volumes:
        - "/var/run/docker.sock:/var/run/docker.sock"
        - "certs:/letsencrypt"
    restart: unless-stopped
    networks:
      - proxy

volumes:
  certs:

networks:
  proxy:
    driver: bridge

The end part is all fairly self-explannatory too, if you've worked with containers before. We set up a volume to hold our certs JSON file, and we create the proxy network, which on my host is the only network which communicates with the outside world. We need to bind port 80 and 443 to this container because they will always be handling the public traffic routing, that' kind of what a reverse proxy is for...

By virtue of the --provider=docker flag we went through earlier, Traefik knows that it should be looking for containers on this network so that's what it will check when it receives new events from the /var/run/docker.sock which we've allowed it to see. That means that you don't have to expose port 80 of 443 in the individual containers' docker-compose.yml files, you just have to add a few labels to point Traefik in the right direction (and yes, it supports external -> internal port redirection too so those containers with fixed internal ports can be mapped just fine).

Client Container Walkthrough

We'll leave out the boring headers this time, the same advice applies from the Traefik header portion.

labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jenkins.entrypoints=https"
      - "traefik.http.routers.jenkins.rule=Host(`sub.domain.tld`)"
      - "traefik.http.routers.jenkins.tls.certresolver=le"
      - "traefik.http.services.jenkins.loadbalancer.server.port=8080"

The labels section is the key bit of client config we need for Traefik. This tells Traefik to enable this container for reverse proxy goodness, to use our entrypoint (https) and out certresolver (le) which we defined earlier, as well as uses the Host(`sub.domain.tld`) rule.

The new thing here is the loadbalancer rule, which is how you can enable the external -> internal port mapping that I mentioned earlier. This will cause cause Traefik to route incoming traffic on the entrypoint (https/:443 here) to the specified internal port (8080 here) of the container.

networks:
      - traefik_proxy

networks:
  traefik_proxy:
    external: true

Here we make sure that the container has the traefik_proxy network attached as an external network. This was defined in the Traefik container simply as proxy but Docker references these things like so: service_thing so if you run docker network list then you'll see the correct network to add. You can also run docker network inspect service_thing and look at the "Containers" key to see that the traefik container is in there.

Full Traefik Container YAML

version: "3.3"

services:
  traefik:
    container_name: traefik
    image: traefik:v2.2.0
    command:
      #- --log.level=DEBUG
      # Entrypoints
      - --entrypoints.http.address=:80
      - --entrypoints.https.address=:443
      # Provider Info
      - --providers.docker
      # Certificate Resolver Info
      - --certificatesresolvers.le.acme.email=your@email.domain
      - --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
    labels:
      # Middleware Redirect
      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
      # Global HTTP -> HTTPS Redirect
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=http"
      - "traefik.http.routers.redirs.middlewares=https-redirect"
    ports:
      - "80:80"
      - "443:443"
    volumes:
        - "/var/run/docker.sock:/var/run/docker.sock"
        - "certs:/letsencrypt"
    restart: unless-stopped
    networks:
      - proxy

volumes:
  certs:

networks:
  proxy:
    driver: bridge

Full Client (Jenkins) Container Walkthrough

version: '3'

services:
  jenkins:
    container_name: jenkins
    image: jenkins/jenkins:lts
    restart: unless-stopped
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.jenkins.entrypoints=https"
      - "traefik.http.routers.jenkins.rule=Host(`sub.domain.tld`)"
      - "traefik.http.routers.jenkins.tls.certresolver=le"
      - "traefik.http.services.jenkins.loadbalancer.server.port=8080"
    volumes:
      - jenkins_home:/var/jenkins_home
    networks:
      - traefik_proxy

volumes:
  jenkins_home:

networks:
  traefik_proxy:
    external: true
Song of the Post: In Heart's Wake just released a new single (which is awesome), have a listen to this banger from their back-catalogue.