Using HTTPS certificates with Traefik and Docker for a development environment
Traefik is a great “cloud” router that is perfect for use in a development environment to route traffic to different Docker hosts, but when I came to try and add some self-signed certificates to it so that my development environment more realistically mirrored the staging and production environments I ran into some problems and the Traefik documentation, whilst good, unfortunately, is a little vague around the subject of certificates in a general, so it took some Googling and putting various different things together to come up with a final solution that works well.
I already had several development sites set up using Traefik as the router to send traffic to the appropriate site based on the domain name, and after reading the docs, I thought it should be as easy as:
- Creating some self-signed certificates using
openssl
. - Adding and trusting the certificates to the certificate store on my Mac.
- Telling Traefik to use the certificates where appropriate.
With this, I was almost right, but I discovered a few “gotchas” along with way.
Generating Certificates
Whilst it is easy enough to generate a self-signed certificate using openssl
, it seems it is more difficult to get the browser to trust that certificate than it was previously, and in the case of Chrome, nearly impossible. There is, however, a very nice, free open source solution to this by the way of mkcert, which is also cross-platform, so can be used on Windows, Mac and Linux. This handy little tool will add itself as a certificate authority to your computers certificate store and then allow you to generated certificates, signed by its own “authority”, which means your browser will believe it is a “true” and “trusted” certificate.
Simply follow the installation instructions and note there is an extra step for Firefox, and once you have it installed you can generate certificates using the command:
mkcert [list of domains]
So you could do:
mkcert www.mydomain.local www.mydomain2.local test.mydomain.local
This command will generate one certificate file that contains a certificate with these three domains listed as valid for that certificate.
It also supports wildcard domains as well, so you could make a certificate like this:
mkcert "*.mydomain.local"
Then, you could use this certificate for any subdomain of mydomain.local
, so test.mydomain.local
, www.mydomain.local
, tasks.mydomain.local
and so on.
So, now we have certificates that the browser will recognise and trust, we need to add them to Traefik, and this is where the documentation let me down.
Adding Certificate to Traefik
The way I have Traefik configured is with a very simple YAML configuration file that is bound to the Traefik docker container via a bind on startup, so the traefik.yml
on my Mac is seen by the Traefik docker container as being /etc/traefik/traefik.yml
on the container.
From reading the documentation, I was under the impression all I needed to do was:
- Bind the directory containing my certificates to a directory on the Traefik docker container.
- Add a
tls:
section to mytraefik.yml
file to declare the certificate files to Traefik on the path they were bound to in step 1. - Add a couple of labels to the docker containers that would be using the certificate to turn on TLS and tell it which domains would be on TLS.
After doing all this, I could connect to the site on HTTPS but it kept giving me the “default certificate”, e.g. one Traefik generates automatically and not a certificate the browser sees as trusted. After much searching around, however, I found the solution. The tls:
configuration actually needs to be in a separate file, that Traefik refers to as a “dynamic configuration”. To do this, you need to tell Traefik about this “dynamic configuration” file in your main traefik.yml
file and then bind this new file into the Docker container at the point you have specified in your traefik.yml
file. So I ended up with this:
traefik.yml
## traefik.yml ## Static configuration entryPoints: web: address: ":80" websecure: address: ":443" # Docker configuration backend providers: docker: network: "my-bridge-network" exposedByDefault: false watch: true file: filename: /etc/traefik/dynamic_conf.yml watch: true # API and dashboard configuration api: insecure: true
dynamic_conf.yml
# Dynamic configuration tls: certificates: - certFile: "/etc/traefik/certs/www.domain1.andrew.pem" keyFile: "/etc/traefik/certs/www.domain1.andrew-key.pem" - certFile: "/etc/traefik/certs/_wildcard.domain2.andrew.pem" keyFile: "/etc/traefik/certs/_wildcard.domain2.andrew-key.pem" - certFile: "/etc/traefik/certs/www.domain3.andrew+2.pem" keyFile: "/etc/traefik/certs/www.onlytease.andrew+2-key.pem"
With the following binds:
Host | Container |
/Users/andrew/certs/ | /etc/traefik/certs |
/Users/andrew/dynamic_conf.yml | /etc/traefik/dynamic_conf.yml |
/Users/andrewdixon/traefik.yml | /etc/traefik/traefik.yml |
Then, on each of the web site containers I added the following labels:
traefik.http.routers.[site_router_name].entrypoints
traefik.http.routers.[site_router_name].tls
traefik.http.routers.[site_router_name].tls.domains[0].main
traefik.http.routers.[site_router_name].tls.domains[0].sans
So, for example, it might look like this if the site_router_name
was mydomain
and the site was served on the domains www.mydomain.local
, tasks.mydomain.local
and test.mydomain.local
:
traefik.http.routers.mydomain.entrypoints
= websecure
traefik.http.routers.mydomain.tls
= true
traefik.http.routers.mydomain.tls.domains[0].main
= www.mydomain.local
traefik.http.routers.mydomain.tls.domains[0].sans
= tasks.mydomain.local test.mydomain.local
After this is done and everything has been restarted, you can then access your development sites on HTTPS and with no browser warning. If you need to add more in future, it is as simple as generating another certificate, adding it to the dynamic_conf.yml
file and adding the labels to the container.
Thank you for this great article… It saved my day 🙂
No problem, glad it helped 😀
Just wanted to say thank you for posting this, helped me and my team figure it out immediately!
Glad it helped.