Published on Saturday, 2020-03-21 at 12:00 UTC

Let’s Encrypt For Your Docker App in Less Than 5 Minutes

Secure any app with free HTTPS certificates in absolutely no time with Traefik.

A while ago, I wrote an article titled “Nginx and Let’s Encrypt in Less Than 5 Minutes”. It quickly became my most popular post ever, with thousands of claps on Medium and hundreds of forks and stars on GitHub.

My original approach included a script that needed to be run once for each deployment. It’s effective, simple — but a little clumsy. So I kept wondering: What if there is an even easier way to set up Let’s Encrypt with docker-compose?

I like keeping things simple. This is why I tend to avoid unnecessary dependencies in my projects. You certainly won’t find me using packages like is-odd or the infamous left-pad. So when my colleague Christian suggested adding a dependency “just” for managing HTTPS certificates, I wasn’t a big fan. Why not just use my script, I wondered. But I gave it a try and never looked back.

So here it is, my new best solution for securing your Docker app with an HTTPS certificate from Let’s Encrypt. This article is going to use docker-compose but you could use the same technique with Kubernetes or “pure” Docker.

Traefik

The Unix Principle

Do One Thing and Do It Well. This is one of the main tenants of Unix philosophy. Following this principle, let’s ask the question: what do we need when we want to “Add Let’s Encrypt to a Docker App”? We want to request and renew HTTPS certificates, we want to terminate TLS (HTTPS) connections and, finally, pass the connection on to our containerized application. You might argue that this is actually four different things, but for our purposes, it’s the combination of these features that matters. Fortunately, there is a tool that implements this very set of features: Traefik.

Traefik (pronounced traffic) is a modern HTTP reverse proxy and load balancer that […] configures itself automatically and dynamically.
The Traefik Docs

Traefik is written in Go and its logo is inspired by the Go Gopher.

You probably think of Nginx or Apache when they hear the term reverse proxy. Many modern web frameworks come with their own HTTP server (e.g. Express in Node.js, Puma in Ruby, Plug in Elixir, or Tomcat in Java). But more often than not, that server isn’t exposed directly to your users. Instead, a reverse proxy terminates TLS connections and then passes on simple HTTP requests to your app.

This means, Traefik can take the role of Nginx if you’re running an application with a web server. But Traefik can also be a reverse proxy for Nginx, taking care of routing and HTTPS.

Automatic configuration

Now to the interesting part: The docs say Traefik configures itself automatically. How is that even possible? The answer is Docker Object Labels.

Every Docker container can be assigned one or several labels. Once you’ve got Traefik running, making a service available on a specific host name is as simple as assigning it the following label: traefik.http.routers.my-app.rule=Host(`example.com`).

Traefik processes all Docker labels in real time. This makes sure your routes are always configured according to the containers you have running. And since Traefik supports Let’s Encrypt by default, your container will receive an HTTPS certificate almost instantly.

Setting up Let’s Encrypt Certificates For Your Docker App With Traefik

Setting Up Traefik

Before we get started, we need to create the Traefik config files. Traefik distinguishes between two types of configuration: static configuration is loaded at startup and describes the Traefik instance. dynamic configuration is loaded at runtime and describes services and middlewares.

Start by creating a folder named traefik that will contain our configuration.

Here’s what we want to configure in the static configuration, traefik/traefik.toml:

  • An entry point http on port 80
  • An entry point https at port 443
  • A certificate resolver lets-encrypt to automatically retrieve certificates from Let’s Encrypt via the ACME protocol
# traefik/traefik.toml

[log]
  level = "WARN"

[providers]
  [providers.docker]
    exposedByDefault = false
  [providers.file]
    directory = "/etc/traefik/dynamic"

[entryPoints]
  [entryPoints.http]
    address = ":80"
  [entryPoints.https]
    address = ":443"

[certificatesResolvers.lets-encrypt.acme]
  storage = "/etc/traefik/acme.json"
  email = "foo@example.com"

Note: Setting providers.docker.exposedByDefault to false makes sure we don’t accidentally expose containers and need to explicitly enable Traefik for each container we want to use it with.

Make sure to insert a working email address in the configuration to get notified by Let’s Encrypt whenever there is a problem with your certificate.

In order to enforce HTTPS for our domains, we now need to add a router and a middleware to our dynamic configuration, traefik/dynamic/force-https.toml:

# traefik/dynamic/force-https.toml

[http.routers]
  [http.routers.force-https]
    entryPoints = ["http"]
    middlewares = ["force-https"]
    rule = "HostRegexp(`{any:.+}`)"
    service = "noop"

[http.middlewares]
  [http.middlewares.force-https.redirectScheme]
    scheme = "https"

[http.services]
  [http.services.noop.loadBalancer]

Next, we need an empty file in which Traefik can store retrieved HTTPS certificates. Make sure to set restrictive rw------- permissions or else Traefik refuses to use the file.

touch traefik/acme.json
chmod 600 traefik/acme.json

Finally, let’s create a docker-compose.yml file that starts Traefik:

# docker-compose.yml

version: '3.4'
services:
  traefik:
    image: traefik:2.1
    restart: always
    ports:
      - '80:80'
      - '443:443'
    volumes:
    - ./traefik:/etc/traefik
    - /var/run/docker.sock:/var/run/docker.sock:ro

Configuring Your Container(s)

Now we’re ready to launch our own application! Add your app to docker-compose. Keep in mind that you’ll have to replace “my-app” with the actual name of your application and “example.com” with your actual domain.

# docker-compose.yml (cont’d)

version: '3.4'
services:
  # ...
  my-app:
    image: my-app:latest
    labels:
      - 'traefik.enable=true'
      - 'traefik.http.routers.my-app.rule=Host(`example.com`)'
      - 'traefik.http.routers.my-app.tls=true'
      - 'traefik.http.routers.my-app.tls.certresolver=lets-encrypt'

Pro tip: If you need multiple domains for one container, you can use comma-separated values (e.g. Host(foo.bar, fizz.buzz)). And in case you need a lot of domains, you can use \ to break the expression into multiple lines.

Done!

Are we done already? Yes, we are!

As soon as you run docker-compose up, Traefik and your application will start. Traefik will then immediately get a Let’s Encrypt certificate for your domain and you’ll be online, secured with HTTPS, in seconds. If only life could always be so easy.