[SUPERCEDED] Using Traefik Proxy with Docker Compose and LetsEncrypt (version 1.3)
This blog entry is deprecated, since it refers to an older version of traefik (version 1.3). Please see the newer version of this article for up to date details (using version 1.7 of traefik).
Traefik Proxy is a fairly recent entry into the reverse proxy space, alongside 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.
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.
Traefik as Reverse Proxy for Docker Compose
Traefik can integrate with different Docker orchestration engines — I’ve used it with docker-compose (as in this article) and kubernetes (where it can operate as an ingress). The docker-compose example is simpler to understand and may well be sufficient for a dev environment. With kubernetes, Traefik shows its power more, for eample adapting to new containers added as a result of horizontal scaling. I’ll write a follow up article covering kubernetes and Traefik soon.
The example I’m going to be working with here is of three website containers which need hosting on a single server. Each will have multiple domain names and the Traefik reverse proxy will route requests to the appropriate container based on the domain name used in the web request.
If you’re not familiar with docker-compose, it uses a YAML-based syntax to define containers, ports, connections between containers, shared file volumes, etc. A simple example to manage one docker container would consist simply of a version string, a services list, and information about the container to run:
version: "2"
services:
website:
image: example/website
environment:
- GA_WEBSITE
This above example also defines an environmental variable (GA WEBSITE) which is inherited from the host environment. This is my token for Google Analytics which I want to externalise from my Docker compose configuration.
We will now add in a Traefik container, using the same syntax. Since this is going to be externally accessible, we need to define the ports we are exposing from the container (port 80 for http, port 443 for https, and port 8080 for monitoring UI). It also needs to be able to communicate with the Docker daemon, to perform its self-configuration. To achieve this, we use the volumes specifier to map the host’s Docker socket into the Traefik container:
version: "2"
services:
traefik:
image: traefik
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
website:
image: example/website
environment:
- GA_WEBSITE
This is all good, but we have not configured Traefik as a reverse proxy yet — it’s just sitting there minding its own business. Traefik, like most reverse proxies, has the notion of a frontend and a backend. The frontend is the part that web requests come into, whilst the backend is the part that talks to the servers being proxied.
To configure Traefik in our docker compose YAML file, we just need to label all our backends, and define the rules which the frontend uses to decide which server to route traffic to. Because we are routing based on domain names, our frontend rule will just be the list of domain names for each server. We use the standard label feature in docker compose to define backends and frontend rules:
version: "2"
services:
traefik:
image: traefik
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "traefik.backend=proxy"
website:
image: example/website
environment:
- GA_WEBSITE
labels:
- "traefik.backend=website"
- "traefik.frontend.rule=Host:example.uk, www.example.uk, example.co.uk, www.example.co.uk"
In the above example, you can see the list of domain names to be used for routing at the frontend, and the names of the backends. Because Traefik exposes a monitoring UI, it has backend functionality too. So we add a backend label to Traefik itself. We don’t need to add a frontend rule for Traefik because that is hard-wired to connections on port 8080.
To complete our example, let’s add in the other two web servers:
version: "2"
services:
traefik:
image: traefik
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "traefik.backend=proxy"
website:
image: example/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: example/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: example/holding-page
labels:
- "traefik.backend=holding"
- "traefik.frontend.rule=Host:example.org.uk, www.example.org.uk"
The important thing to note here is that we have simply added in two more containers, using labels to define their backend names and their frontend rules. We have not changed Traefik and we have not had to make any changes to any configuration files. Traefik detects any containers and uses the labels on them to self-configure its routing rules.
It’s worth also noting that we can add multiple frontend rules for a single server (like we have done for blog in the above example) and routing is performed when any of those domains are specified in a web request.
That covers the basics for usint Traefik with Docker compose. Other standard elements can be added (e.g. databases and links from web servers to those databases) and the Traefik configuration will continue to work fine.
Traefik and Let’s Encrypt for Ssl
Now we move on to the use of https://letsencrypt.org/ with Traefik to provide free, easily configured SSL certificates, making it extremely simple to have your websites running under https at no additional cost.
Let’s Encrypt was set up to remove the barriers to putting websites behind https and is backed by many industry giants such as Facebook, Chrome and the EFF. Traefik allows a few lines of configuration to automatically request certificates from letsencrypt and apply them at the reverse proxy.
Before going any further, I have to make a small confession. When I said earlier that Traefik didn’t need a configuration file, it does actually have a simple one which says what ports to listen on and also enables the Docker integration functionality. Here is my config.toml file for the above example:
logLevel = "INFO"
defaultEntryPoints = ["http", "https"]
[web]
address = ":8080"
[entryPoints]
[entryPoints.http]
address = ":80"
[entryPoints.https]
address = ":443"
[entryPoints.https.tls]
[docker]
domain = "example.uk"
watch = true
Hopefully it is self-evident — it sets up the ports for http, https and monitoring UI connections, and also tells Traefik to watch the Docker daemon.
We now need to add a section to this configuration file to tell Traefik to use letsencrypt. Letsencrypt uses a protocol called ACME (Automatic Certificate Management Environment) to allow web clients to communicate with it. For this reason the section of the configuration file is labelled acme:
[acme]
email = "letsencrypt@example.uk"
storage = "/etc/traefik/acme/acme.json"
entryPoint = "https"
acmeLogging = true
onDemand = false
OnHostRule = true
Letsencrypt uses an e-mail address as a unique address (and also sends e-mails to that address when certificates are about to expire), so that is the first configuration line. The next line, storage, is where the certificates received from letsencrypt will be stored locally — I have chosen to put this in a directory called /etc/traefik/acme. We then define the entry point this applies to (our https entrypoint defined earlier in the configuration file) and whether we enable logging for letsencrypt interactions.
The last 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 is not so useful for 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. We now need to make two minor changes to our docker-compose file to allow these changes to take effect. The first is because we have created our configuration file on the host file system and we need it to be available within our Traefik Docker container. We therefore need to map our config file into the Traefik container with a suitable volume mapping command, for example:
$PWD/traefik.toml:/etc/traefik/traefik.toml
If we leave our configuration like this, it will work, but every time we restart the container it will lose the certificates received from letsencrypt (because they are stored within the Docker container). Letsencrypt has a monthly limit on the number of times you can request certificates for any specific domain, so this can be problematic. I have therefore also mapped the /etc/traefik/acme directory (as mentioned earlier) out of the Traefik container back into the host filesystem:
$PWD/run/acme:/etc/traefik/acme
Putting this all together, we end up with this Docker compose file:
version: "2"
services:
traefik:
image: traefik
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- $PWD/traefik.toml:/etc/traefik/traefik.toml
- $PWD/run/acme:/etc/traefik/acme
labels:
- "traefik.backend=proxy"
website:
image: example/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: example/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: example/holding-page
labels:
- "traefik.backend=holding"
- "traefik.frontend.rule=Host:example.org.uk, www.example.org.uk"
We can now spin up our environment, with its different web servers, reverse proxy, host-based routing and https protection simply by typing:
docker-compose up
Now we have this setup, adding additional web servers is as simple as adding a few new lines for those servers in the docker-compose file. We 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 downloaded for the domains we define.
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 lines. What’s more, use of letencrypt is free, so there’s really no excuse not to have https on your website now.
As I said at the start, use of Docker compose is not really suitable for Production hosting, since it doesn’t have resilience, auto-healing or scaling. I have been using Kubernetes to support Production quality hosting and will demonstrate how Traefik needs a similarly small amount of configuration to work as Kubernetes ingress.