Information


Blog Posts


Collections



Contact


Things Ian Says

Using Traefik Proxy with Docker Compose and LetsEncrypt

Traefik Proxy is one of the newer reverse proxies available (compared to more established applications such as nginx and Apache httpd). The thing which differentiates traefik is that it was created in a post-Docker world and integrates with Docker to reduce the manual configuration needed. It also supports let’s encrypt to provide SSL encryption, with minimal extra effort.

This article looks at how we can use traefik as a reverse proxy across a docker-compose managed suite of containers and then use let’s encrypt to add SSL certificates for https access.

Background

Back in the earlier days of web hosting, we typically used a single application (like Apache httpd, then more recently nginx) to host all our sites (on a specific server). These applications took care of both serving up the websites, and routing traffic to the correct website. Although this worked perfectly well, for me, it never felt aligned with the Unix tooling principle of do one thing well.

When Docker containers first became a production-ready technology, the first thing we did was to package up the technology we were already using into containers (for example creating Docker containers with multiple web sites running in nginx). This made testing and deployment much simpler, but was clearly just a first step. Once we got a bit more sophisticated, and started getting tooling to orchestrate multiple containers (from Docker Compose to Docker Swarm to Kubernetes), we could start looking at optimisations, like creating much smaller containers (for example, I previously described how switching from nginx to busybox httpd could reduce our container size from 100M to 1M).

This meant that I could break down the way I hosted content, into separate, self-contained, independently deployable units. However, I still needed a way to route traffic to the correct one. Initially, I used nginx for this, but I was looking around for suitable alternatives, and came across Traefik. This looked very promising, since it has some great features — I can define the routing, just by adding labels to my container (I don’t need special configuration), and it would also handle generating certificates from Let’s Encrypt.

This article describes how I use Traefik to manage my hosting, enabling me to serve websites from three different containers, with a proxy handling routing and https handoff:

Hosting Architecture

Traefik Configuration File

A small amount of configuration needs to be written for Traefik. Different configuration file formats are supported, and I chose to use toml, which is kind of a cross between YAML and basic property files. My file is called traefik.toml and I put it in my base directory.

The start of this file just sets up log level, and the fact that I will allow http and https:

logLevel = "INFO"
defaultEntryPoints = ["http", "https"]

The remainder of the configuration file is split into sections. The toml format specifies sections by enclosing the section name in square brackets.

The first section I created was concerned with_Traefik_’s ability to surface information about its configuration and use via an API, which can then be presented using a dashboard (which I show later). This is configured in the api section of the config file. I will label the _entrypoint_ for this as api, which allows me to specify a different configuration than the one I use for other requests (http/https). I will also configure want the dashboard to be enabled, but without any debug-level information coming through. That looks like this:

[api]
entrypoint = "api"
dashboard = true
debug = false

My next section concerns the entryPoints — how Traefik responds to calls. First thing is that I don’t want anybody being able to connect over a plain http connection. So I set up my http port to redirect to https. I also set up the port for https connections too:

[entryPoints]

  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"

  [entryPoints.https]
  address = ":443"

  [entryPoints.https.tls]

I also set up my entry point for the API (dashboard) connection I defined earlier. I set this up to listen on port 8080 (internally), but I want to password protect it (so that it’s not available to the entire World). I do that by adding a subsection for Basic HTTP Auth. There are multiple ways I can add my username and password, but for the purposes of this example, I will just add them in the configuration file:

  [entrypoints.api]
  address = ":8080"
    [entrypoints.api.auth.basic]
    users = ["secret:$apr1$XdZOrEEp$xu/mDVqCVZaILlngqUm/e1"]

There are different ways to create the user’s username and password, but I find the htpasswd utility does the trick. You supply the username, then it prompts you for a password and generates the appropriate result. If you supply the -n flag to the command, it displays the result as an output. Here’s an example where I am using the word secret as both my username and password:

$ htpasswd -n secret
New password:
Re-type new password:
secret:$apr1$XdZOrEEp$xu/mDVqCVZaILlngqUm/e1

My next section is to support Docker. It specifies the domain to use for my containers, and I also set the watch flag, which means Traefik will reload my configuration file whenever it detects a change to my setup:

[docker]
domain = "example.uk"
watch = true

Finally, I set up my automated free certificates with Let’s Encrypt. This section is called acme, because that is the name of the protocol used for communication with Let’s Encrypyt (the Automatic Certificate Management Environment). As part of this configuration, I need to specify the challenge that Let’s Encrypt will use to verify that I own the host I am requesting for the certificate. I use the http challenge, which means that Let’s Encrypt will specify a web page it expects to find on my server, and I need to set up a positive response to a request for that page. Fortunately, Traefik handles all that for me, so I don’t need to worry about the details of that process.

Here’s that final section of configuration:

[acme]
email = "letsencrypt@example.uk"
storage = "/etc/traefik/acme/acme.json"
entryPoint = "https"
onDemand = false
OnHostRule = true
acmeLogging = true

[acme.httpChallenge]
entryPoint = "http"

The two lines, onDemand and onHostRule, specify under what circumstances Traefik requests certificates from letsencrypt. The onDemand option will cause Traefik to request certificates whenever a web request is received for a domain which does not already have a certificate. This can potentially be used for denial of service attacks (by requesting many different domain names from the server) so I have set this to false. The onHostRule only requests new certificates for domain names listed in Traefik’s frontend rules in the docker-compose file. This reduces the chances of a denial of service attack, since we have a small list of valid domains to choose from. I have therefore set this to true.

That completes the Traefik configuration changes we need. For completeness, here is the whole configuration:

logLevel = "INFO"
defaultEntryPoints = ["http", "https"]

[api]
entrypoint = "api"
dashboard = true
debug = false

[entryPoints]

  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
    entryPoint = "https"

  [entryPoints.https]
  address = ":443"

  [entryPoints.https.tls]

  [entrypoints.api]
  address = ":8080"
    [entrypoints.api.auth.basic]
    users = ["secret:$apr1$XdZOrEEp$xu/mDVqCVZaILlngqUm/e1"]

[docker]
domain = "example.uk"
watch = true

[acme]
email = "letsencrypt@example.uk"
storage = "/etc/traefik/acme/acme.json"
entryPoint = "https"
onDemand = false
OnHostRule = true
acmeLogging = true

[acme.httpChallenge]
entryPoint = "http"

Docker Compose

I am using Docker Compose to orchestrate my Docker containers. Let’s start by looking at the basic docker-compose.yaml file, which spins up my containers:

version: "2"
services:

  traefik:
    image: traefik:1.7-alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/run/acme:/etc/traefik/acme
      - $PWD/traefik.toml:/etc/traefik/traefik.toml

  website:
    image: my-website
    environment:
      - GA_WEBSITE

  blog:
    image: my-blog
    environment:
      - GA_BLOG

  holdingPage:
    image: my-holding-page

Just running through those containers from the top, I use the alpine version of the Traefik Docker image, since alpine usually gives smaller containers. I expose ports 80 and 443 (for http and https) to allow standard web access. I also need to map some files into the container. Traefik needs to read information from the Docker daemon, so I need to map in docker.sock to allow that communication. I need to be able to persist the certificate details from Let's Encrypt — that is /etc/traefik/acme inside the container, and I am using /run/acme on my host. Finally, I need to map in the traefik.toml file I created earlier.

The other three containers (website, blog and holdingPage) are just run as basic invocations, with the addition of environmental variables which define my Google Analytics tracking token.

If I were using a conventional proxy server (like nginx), I would now need to set up a set of rules in nginx configuration, specifying how to proxy all the services correctly. However, the magic of Traefik is that I can now just add labels to this configuration file, and Traefik will use them to set up the proxying automatically.

Let’s start with my website. Traefik needs two parts to proxy traffic — the backend (the container to connect to) and the frontend (the rule for when to send traffic to this container). For my website, I want to be able to reach it when someone types in the web address https://example.uk or https://example.co.uk (plus the variations with www on the front). So that is the rule I put in the frontend. The backend just needs the name website:

website:
  image: my-website
  environment:
    - GA_WEBSITE
  labels:
    - "traefik.backend=website"
    - "traefik.frontend.rule=Host:example.uk, www.example.uk, example.co.uk, www.example.co.uk"

I do a similar thing for my blog:

blog:
  image: my-blog
  environment:
    - GA_BLOG
  labels:
    - "traefik.backend=blog"
    - "traefik.frontend.rule=Host: blog.example.org.uk, www.blog.example.org.uk"
    - "traefik.frontend.rule=Host: example.com, www.example.com"

Notice that I have split the frontend into two separate rules. This doesn’t affect the behaviour of Traefik as a proxy, but it does influence the certificates we get from Let’s Encrypt. With the above configuration, I will get two certificates — one for blog.example.org.uk and www.blog.example.org.uk, and another one for example.com and www.example.com. If I put all the URLs in the same frontend rule, I would get a single certificate covering all these domains.

For completeness, here are the labels I add to my holding page container:

holdingPage:
  image: my-holding-page
  labels:
    - "traefik.backend=holding"
    - "traefik.frontend.rule=Host:example.org.uk, www.example.org.uk"

Finally, I need to add labels to the Traefik container itself. This is to allow me to access the dashboard pages. The backend rule is just like the other containers, and I also need to add a traefik.enable label (it’s switchable, since not every setup will want to expose the Traefik dashboard). Because the standard ports on this container are being used as the public access to the hosting, I need to use a traefik.port label to specify a different port for this backend (port 8080). Finally, the frontend rule is slightly more involved, because I want to use the same domain as one of my other containers (example.org.uk), but with /traefik/ added to the end of the web address. So, I specify the hosts the same as before, but also add a PathPrefixStrip term too — this will only match when the URL ends with /traefik/, but it will strip it off the URL before it passes it to the backend. Putting it all together, it looks like this:

traefik:
  image: traefik:1.7-alpine
  labels:
    - "traefik.backend=traefik"
    - "traefik.enable=true"
    - "traefik.port=8080"
    - "traefik.frontend.rule=Host:example.org.uk, www.example.org.uk; PathPrefixStrip:/traefik/"
  ports:
    - "80:80"
    - "443:443"
  volumes:
    - /var/run/docker.sock:/var/run/docker.sock
    - $PWD/run/acme:/etc/traefik/acme
    - $PWD/traefik.toml:/etc/traefik/traefik.toml

And here is the entire Docker compose file:

version: "2"
services:

  traefik:
    image: traefik:1.7-alpine
    labels:
      - "traefik.backend=traefik"
      - "traefik.enable=true"
      - "traefik.port=8080"
      - "traefik.frontend.rule=Host:example.org.uk, www.example.org.uk; PathPrefixStrip:/traefik/"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - $PWD/run/acme:/etc/traefik/acme
      - $PWD/traefik.toml:/etc/traefik/traefik.toml

  website:
    image: my-website
    environment:
      - GA_WEBSITE
    labels:
      - "traefik.backend=website"
      - "traefik.frontend.rule=Host:example.uk, www.example.uk, example.co.uk, www.example.co.uk"

  blog:
    image: my-blog
    environment:
      - GA_BLOG
    labels:
      - "traefik.backend=blog"
      - "traefik.frontend.rule=Host: blog.example.org.uk, www.blog.example.org.uk"
      - "traefik.frontend.rule=Host: example.com, www.example.com"

  holdingPage:
    image: my-holding-page
    labels:
      - "traefik.backend=holding"
      - "traefik.frontend.rule=Host:example.org.uk, www.example.org.uk"

I can now start up my servers (using docker-compose up) and I will be able to access all my backend containers via Traefik as a proxy. I also have a useful dashboard (which is password protected) which will let me see information about my hosting. Here is the screen which shows my frontends and backends:

Traefik Dashboard Providers Screen

And here is the screen which shows me the health of my hosting (e.g. uptime, response time, etc):

Traefik Dashboard Health Screen

Now I have this setup, adding additional web servers is as simple as adding a few new lines for those servers in the docker-compose file. I don’t need to change the Traefik configuration in any way to accommodate the server and it will automatically be added to the routes and get new SSL certificates generated and deployed (and renewed) for the domains we define.

Using Let’s Encrypt to get Certificates

The best part about using Traefik is that I don’t actually need to write anything in this section. The configuration and Docker compose file I gave above takes care of fetching the Let’s Encrypt certificates if they don’t exist, and also renews them when they are due to expire. It also sorts out responding to the http challenge when setting up those certificates. So I don’t need to do anything extra to get my free https certificates.

Summary

As you can see, there is only a small amount of configuration needed for both the domain-based routing and the https connections to be supported. And once we have created the inital configuration, adding in a new container just requires a couple of extra labels on top of the normal docker-compose.yaml content. What’s more, use of Let’s Encrypt is free, so there’s really no excuse not to have https on your website now.