Continuous Deployment Revisited - Making Docker Containers Accessible with Traefik 2.x

February 11, 2021

written by:

In earlier blog posts, we've already discussed how we can make Docker containers accessible on the web using the Traefik cloud native edge router. Since then, Traefik has leapt a major version forward. Its configuration has changed, and it has learned some new tricks. So, it is time to talk about it again. In this post, we will show how to make Docker containers available on the web including Let's Encrypt registration.

Docker Traefik Continuous Deployment

The use case we will be dealing with is the same as in the original blog post (Continuous Deployment Pt. 1 - Making Docker Contaiers Accessible with Traefik). In the end, we want to make Docker containers available on the web, be it during a Gitlab build or a manual start of a popular docker image for, say a blog or a wiki. The image shall be deployed under a specified sub-domain and receive a Let's Encrypt certificate to protect its HTTP/s traffic.

Traefik 2.x

In the (not so) new version 2, some things have been changed compared to version 1. Most notably, the concepts of frontend and backend have been dropped. Instead, routers, services and middlewares are now used. Comparing the two versions, routers replace frontends and services replace backends, with each router referring to a service. For modifications to the traffic, Treaefik now uses middleware components; this functionality was configured in the backends previously. Due to these changes, the configuration will look very different.

The configuration of TLS has changed as well and is now applied per router. HTTP to HTTP/s forwarding is also applied on a per router basis. - We will have a quick look here, too.

What is Traefik

Traefik is an open source reverse proxy, load balancer and edge router which is fairly simply to use, integrate and maintain. There are a couple of features which make it particularly interesting:

  • It listens for Docker containers starting and stopping.
  • It then "reads" the containers Docker (-Compose) configuration.
  • It has built-in support for Let's Encrypt.

Additionally, there are a ton of useful middlewares which can be applied at ease, among them:

  • Load balancing, rate limiting, circuit breaker, retry, buffering
  • Access restriction, e.g. BasicAuth, DigestAuth, IP filtering
  • Adding and stripping path prefixes

All in all, there is a big toolbox of useful features which are just a configuration away!

There is, of course, a dashboard where you can check which containers have been discovered by Traefik, and monitoring / metric endpoints are built right in. So, if you want to monitor your system, you are good to go.

Setting up Traefik

For sake of simplicity, we will use a Docker-Compose file to set up Traefik, but labelling your Docker files should work just as fine. The following texts are updated from the blog article about Traefik 1.x to reflect the configuration changes. The use case in focus remains entirely the same.

To work properly, Traefik needs to be accessible on ports 80 for HTTP and 443 for HTTP/s (1). For this purpose, we create a network traefiknet, which is a bridge to the host (7). On a side note: Make sure ports 80 and 443 have been opened in your firewall.

In the setup we're laying out, port 80 is required so that Let's Encrypt can do its challenge / response requests when issuing a TLS certificate. Further requests on this port will be forwarded to port 443, that is: HTTP traffic will be forwarded to HTTP/s.

Containers, which are made accessible through Traefik, need to be reached by traffic. Otherwise, incoming requests cannot be forwarded to those containers. We deal with this by creating an external traefik_proxy network (8), which all "public" containers have to join. For obvious reasons, Traefik has to join it, too (3).

This way, many different isolated, self-contained Docker-Compose systems (website, wiki, blog, ...) may be created to use Traefik for routing web requests to them. Also, in this image Treafik itself poses such a system. Have in mind that only those containers of a Docker-Compose file need to join the traefik_proxy network, which shall be exposed to the web.

A brief example to underline the idea: Imagine you have a wiki which consists of a wiki software and its database. Only the wiki container should be added to the network, while the container for the database must not be accessible from the web. It receives its traffic solely from the wiki software. Consequently, only the container for the wiki software shall have an entry for the traefik_proxy network.

As we want Traefik to discover when Docker containers start and stop automatically, we need to give it access to /var/run/docker.sock (4).

Traefik needs a bit of configuration itself, so we mount the configuration file into the container (5). The acme.json file (6) is used by Traefik to write Let's Encrypt key information to it. We externalized both files, so we can recreate the container and backup the data independently.

version: '3'

services:
  traefik:
    image: traefik:v2.4
    container_name: traefik
    hostname: traefik  
    restart: always
    ports:
      - 80:80                                       # (1)
      - 443:443
    networks:
      - traefiknet                                  # (2)
      - traefik_proxy                               # (3)
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock   # (4)
      - ./conf/traefik.toml:/traefik.toml           # (5)
      - ./conf/acme.json:/acme.json                 # (6)

networks:
  traefiknet:
    driver: bridge                                  # (7)
  traefik_proxy:
    external:
      name: traefik_proxy                           # (8)

Creating the traefik_proxy network

As specified above, Traefik uses an external network to forward requests to containers it exposes. In the most simple case, creating the external traefik_proxy network is as easy as:

docker network create traefik_proxy

If you want to specify the details of your Traefik proxy network, please have a look at the (Docker Networks) documentation.

The only thing missing now is the configuration file for Traefik: traefik.toml.

Traefik Config File

Traefik may use a TOML file for configuration; however, YAML is also possible.

The following configuration follows a basic approach which will work for typical Let's Encrypt setups. It is using the ACME httpChallenge approach (8). However, as a requirement, the system needs to be reachable on port 80. There are other approaches (and many more providers) which do not have this requirement; see Traefik HTTP/s ACME documentation for further details.

The email address (6) is required for the Let's Encrypt certification process, and you will be notified, for example, if your certificate is about to expire. Traefik, however, will refresh certificates automatically. The acme.json file (7) is used to store Let's Encrypt key information.

In Traefik, entrypoints denote network ports where requests may be received. In this example, there are two entrypoints: port 80 for HTTP requests and port 443 for HTTP/s requests (1).

The acme.json file (7) is specified and also mounted by the Docker-Compose file above. Create an empty file for the beginning. Traefik will take care of the rest.

Traefik needs a way to discover when containers start and stop, it does so by listening to the docker.sock file (2). This is the reason why we mounted it in the Docker-Compose file above.

We want to exert some control over which containers are exposed by Traefik, thus we set exposedByDefault to false (3). This way, we have to tell Traefik explicitly which containers to make available.

Also, the external network traefik_proxy, which was discussed above, is configured here (4).

[entryPoints]                                    # (1)
  [entryPoints.web]
    address = ":80"
  [entryPoints.web-secure]
    address = ":443"

[providers.docker]
  endpoint = "unix:///var/run/docker.sock"       # (2)
  exposedByDefault = false                       # (3)
  network = "traefik_proxy"                      # (4)
  watch = true

[certificatesResolvers.letsencrypt.acme]
  email = "<EMAIL>"                              # (6)  
  storage = "acme.json"                          # (7)  
  [certificatesResolvers.letsencrypt.acme.httpChallenge] # (8)
    entryPoint = "web"

Once the docker-compose, traefik.toml and the empty acme.json files are ready, we are good to go.

Starting Traefik

Now, it is time to start Traefik. If we are in the same directory as your docker-compose file, we can just run:

docker-compose up -d

Traefik should now be waiting for other containers to start. We can watch its output by observing its log:

docker logs -f traefik

Once Traefik is up and running, it is time to deploy another container and see that everything is working as intended.

Deploying a Container

In the following, we will deploy the nginx hello world container, but the approach applies to more complex environments as well.

We need to make sure Traefik is able to reach the container. So we add the external traefik_proxy (8) network we created in the steps above and add the network to the container (1).

The labels section contains the most interesting parts. Since we have disabled Traefik to expose containers by default (see configuration), we need to direct it to expose this container, (2) does this. (4) and the next two lines define a router which is listening on the EntryPoint web (see configuration; essentially, this means port 80). Also, the domain name for the router is specified: <DOMAIN NAME> (e.g. hello.mydomain.com). The third line specifies a middleware to be used: hello-web-https. This middleware is specified in (5) and uses the Traefik middleware component .redirectscheme.scheme to redirect a HTTP request to HTTP/s.

Starting from (6), the router for https is defined. This time, the EntryPoint web-secure is set (see configuration, port 443 this time). The following row defines the domain name, just as above. Now we need to tell Traefik how to acquire a certificate for the domain. Again, Traefik provides a handy component .tls.certresolver, which is set to letsencrypt (numerous other providers are readily available, among them: Azure, Cloudflare, DigitalOcean, Dyn, Hetzner, ionos and MyDNS).

Finally, we need to specify the service, which will accept the incoming requests. Here, the service hello-web-service shall take care of them. The service (7) is configured to use the Traefik loadbalancer component and send requests to port 80 of our hello-world Docker container.

Please note how lines which configure a component use the same naming scheme (i.e. traefik.http.routers.hello-web...).

version: '3'

services:
  nginx:
    image: nginxdemos/hello
    container_name: hello-world
    hostname: hello-world
    networks:        
      - traefik_proxy                                      # (1)
    labels:
      traefik.enable: "true"                               # (2)
      traefik.docker.network: "traefik_proxy"
      traefik.http.routers.hello-web.entrypoints: "web"    # (4)
      traefik.http.routers.hello-web.rule: "Host(`<DOMAIN NAME>`)"
      traefik.http.routers.hello-web.middlewares: "hello-web-https"
      traefik.http.middlewares.hello-web-https.redirectscheme.scheme: "https" # (5)
      traefik.http.routers.hello-web-secure.entrypoints: "web-secure"         # (6)
      traefik.http.routers.hello-web-secure.rule: "Host(`<DOMAIN NAME>`)"
      traefik.http.routers.hello-web-secure.tls.certresolver: "letsencrypt"
      traefik.http.routers.hello-web-secure.service: "hello-web-service"    
      traefik.http.services.hello-web-service.loadbalancer.server.port: "80"  # (7)

networks:
  traefik_proxy:
    external:
      name: traefik_proxy                                  # (8)

That's it! We can start the compose file with docker-compose up -d. In the Docker logs docker logs -f traefik, we can see how Traefik starts collecting the TLS certificate from Let's Encrypt.

Now our container is accessible from the web, with a valid TLS certificate. We can extend this example to deploy much more complex Docker-Compose systems and expose only the front-facing containers. We could also use the Traefik labels in the deployment step of our CI-CD pipeline. This way, we are exposing the container on the web, once it is deployed after a successful build (as done here: Continuous Deployment Pt. 2 - Deploying Docker Containers with Ansible from GitLab).

DNS Settings

Have in mind that for using sub-domains for your containers, we have to manage the A-records of our DNS entries accordingly.

Adding Access Control

If a container shall not be openly accessible, we need to add access restrictions, such as the simplistic basic authentication (BasicAuth). Treafik provides a middleware component; all we need to do is to configure it in our compose file:

    traefik.http.routers.hello-web-secure.middlewares: "hello-web-basicauth"
    traefik.http.middlewares.hello-web-basicauth.basicauth.realm: "Protected-Hello-World"
    traefik.http.middlewares.hello-web-basicauth.basicauth.users: "<PASSWORD-HASH>"

To create the <PASSWORD-HASH>, we can use the htpasswd tool, like this: echo $(htpasswd -nb <USERNAME> <PASSWORD>) | sed -e s/\\$/\\$\\$/g. Because the $ character is used for accessing environment variables in Docker-Compose files, sed is used to escape them.

Load Balancing

If we want to load balance several of the same containers, this can be done very easily as well. In the most basic scenario, we simply reuse and adapt the container definition of the Docker-Compose file.

However, with very little tweaking we can tell Traefik to check whether our container instances are alive, and take them out of the rotation if they are not.

    traefik.http.services.hello-web-service.loadbalancer.healthcheck.path: "/"
    traefik.http.services.hello-web-service.loadbalancer.healthcheck.interval: "3s"

The first line tells Traefik where the health check endpoint of our container can be reached. As long as a request to this path returns an HTTP status code 200, the container is considererd alive. Otherwise, it will stop receiving web requests. The second line defines how often this check is repeated, in this case every three seconds. This time also defines how long it will take Traefik to realize that a container is unresponsive.

After updating our compose file, if we access the hello-world web page now, we will notice how the server name and IP address cycles with every request we make. Also, if we check the log files of both containers via docker logs -f hello-world-1 and -2, we will see the health inspection requests that Traefik sends.

For more details (on e.g. sticky sessions), please see the Traefik load balancing documentation.

Downloads

The Docker-Compose and Traefik configuration Toml files described in this post can be downloaded here:

Further Reading