How to setup Traefik with Docker Compose

Setting up services and securing them can be a daunting task, but it doesn't have to be. Every time I looked up a tutorial on how to set up a service, it was long and difficult to follow. Many of them required a dedicated server (or virtual machine), setting up a reverse proxy, then manually updating the proxy configuration every time a new service was installed. But most services now are containerized; which means we can use Docker. And we can use a single proxy to serve multiple services. The proxy for this tutorial will be Traefik, and I'll explain how you can set it up with a single Docker Compose file.

Why Traefik?

Traefik has built-in support for Docker by utilizing the docker.sock file of your machine. With this support, Traefik is able to detect when new containers are added. This makes adding and configuring your services to a proxy network very easy.

Basic Setup

Lets take a look at a simple Traefik configuration in a Docker Compose YAML file:

version: "3.9"

networks:
  public-network:
    name: public
    driver: bridge
    
volumes:
  network-logs:
    name: traefik-logs

services:
  traefik:
    image: traefik:v2.4
    container_name: traefik
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - network-logs:/var/log/
    networks:
      - public-network
    ports:
      - 80:80
    command:
      - --entrypoints.web.address=:80
      - --log=true
      - --log.level=DEBUG
      - --log.filepath=/var/log/traefik.log
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=public

So far this doesn't look so scary. Lets go over  some of the configurations in the stack.

We need to configure a network for the Traefik so it can forward requests to the services we want public. We also want to set up a bridge network for direct communication.

networks:
  public-network:
    name: public
    driver: bridge

You'll see here we are mounting a file called "docker.sock". This file provides the APIs to our Docker instance so Traefik can monitor the status of our containers. We also put ":ro" at the end to make sure that Traefik has read-only permissions.

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro
  - network-logs:/var/log/

This one probably looks the weirdest. The command keyword allows us to define CLI inputs after the Docker entrypoint runs. In this case, these will run as Traefik's command arguments.

command:
  - --entrypoints.web.address=:80
  - --log=true
  - --log.level=DEBUG
  - --log.filepath=/var/log/traefik.log
  - --providers.docker=true
  - --providers.docker.exposedbydefault=false
  - --providers.docker.network=public

Lets go over some of the commands above. The entrypoints.web.address marks the local port 80 as the entry-point for http requests. Commands with log are pretty self explaining, they configure the logging. providers.docker declares configurations for docker. providers.docker=true enables docker as a provider. providers.docker.exposedbydefault=false configures Traefik to not expose a docker container publically, by default. This is recommend to prevent accidental exposure. providers.docker.network=public is telling Traefik that the public-network is the proxy network where public services can communicate.

HTTPS Setup

You will also notice we are now opening port 443, but keeping port 80 opening still. This is because we will be redirecting all HTTP (port 80) request to HTTPS (443). Sometimes browsers will still make an HTTP request, so its best to keep this port open for redirects.

version: "3.9"

networks:
  public-network:
    name: public
    driver: bridge
    
volumes:
  network-logs:
    name: traefik-logs

services:
  traefik:
    image: traefik:v2.4
    container_name: traefik
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - network-logs:/var/log/
    networks:
      - public-network
    ports:
      - 80:80
      - 443:443
    command:
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      - --entrypoints.websecure.address=:443
      - --log=true
      - --log.level=DEBUG
      - --log.filepath=/var/log/traefik.log
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=public

You can see in the entrypoints.web commands we are telling Traefik to redirect HTTP to websecure and the HTTPS scheme.

Challenges

No this section isn't about difficulties you will run into. This section is about challenges a certification resolver makes when obtaining a certification. These challenges are part of the ACME standard to help prove ownership of your domain.

DNS Challenge

A DNS challenge is done by contacting your DNS provider to help validate your ownership of your domain. This type of challenge is useful if you do not have ports 80 or 443 open. This challenge is also used to make wildcard certifications.

DNS challenges vary per DNS provider. Because of how different the configurations can be per provider, I will not be covering DNS challenges in this tutorial. Traefik does have a list of supported DNS providers on their documentation. In the Traefik documentation, you can find links to each provider's website for help configuring.

HTTP Challenge

An HTTP challenge involves a ACME client to create a token and pass to the certification resolver for validation. After the token is validated, a cert can be created. This challenge is easier to set up and is most commonly used today.

To perform any ACME challenge, you will need a certification resolver. In this tutorial we will be using Let's Encrypt. Let's Encrypt is a non-profit organization that provides free SSL certifications for your websites.

Let's Encrypt commands

version: "3.9"

networks:
  public-network:
    name: public
    driver: bridge
    
volumes:
  network-logs:
    name: traefik-logs
  certs-volume:
    name: traefik-certs

services:
  traefik:
    image: traefik:v2.4
    container_name: traefik
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - certs-volume:/letsencrypt
      - network-logs:/var/log/
    networks:
      - public-network
    ports:
      - 80:80
      - 443:443
    command:
      - --certificatesresolvers.letsencrypt.acme.httpchallenge=true
      - --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web
      - --certificatesresolvers.letsencrypt.acme.email=support@example.com # CHANGE ME
      - --certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json
      - --entrypoints.web.address=:80
      - --entrypoints.web.http.redirections.entrypoint.to=websecure
      - --entrypoints.web.http.redirections.entrypoint.scheme=https
      - --entrypoints.websecure.address=:443
      - --entrypoints.websecure.http.tls=true
      - --entrypoints.websecure.http.tls.certResolver=letsencrypt
      - --log=true
      - --log.level=DEBUG
      - --log.filepath=/var/log/traefik.log
      - --providers.docker=true
      - --providers.docker.exposedbydefault=false
      - --providers.docker.network=public

You will notice new commands for the certificatesresolvers.letsencrypt.acme namespace. These command will configure an ACME client for Let's Encrypt. The first command declares we are using an HTTP challenge. The second command will declare we are using our address for port 80. The third sets your domain's email (change this to YOUR domain's email). The fourth command sets the Let's Encrypt ACME file where your certs will be stored. Lastly, entrypoints.websecure.http.tls.certResolver will declare we are using Let's Encrypt.

Adding Services to Traefik

Now that you have Traefik set up from the Docker Compose file, you can compose services to proxy their exposure through Traefik. Follow the template below to add your services to Traefik.

version: "3.9"

networks:
  public-network:
    external: true
    name: public
    driver: bridge
    
services:
  SERVICE: # Change
    image: service-image # Change
    restart: unless-stopped
    networks:
      - public-network
    labels:
      - traefik.docker.network=public
      - traefik.enable=true
      # Change SERVICE to the name of your service
      - traefik.http.routers.SERVICE.entrypoints=websecure
      - traefik.http.routers.SERVICE.rule=Host(`subdomain.CHANGEME.com`) # Change to your domain
      - traefik.http.routers.SERVICE.tls=true
      - traefik.http.routers.SERVICE.tls.certresolver=letsencrypt
      - traefik.http.services.SERVICE.loadbalancer.server.port=99999 # Change to service port

It's important that you make your service on the same network as your Traefik container. To do this, you will need to make sure the service is on the public-network we defined eariler. Make sure specify that the network is external if you are creating a separate compose file.

networks:
  public-network:
    external: true # Add this if your service's compose file is different than traefik
    name: public
    driver: bridge

Throughout the template, you'll notice the word SERVICE all thorughout. Replace this with the name of your service you want to host. This is important because these labels need to be unique to your service; otherwise, Traefik may get confused and send requests to the wrong container.

Lets go over the labels to have a better understand what this container is trying to tell Traefik

    labels:
      - traefik.docker.network=public
      - traefik.enable=true
      # Change SERVICE to the name of your service
      - traefik.http.routers.SERVICE.entrypoints=websecure
      - traefik.http.routers.SERVICE.rule=Host(`subdomain.CHANGEME.com`) # Change to your domain
      - traefik.http.routers.SERVICE.tls=true
      - traefik.http.routers.SERVICE.tls.certresolver=letsencrypt
      - traefik.http.services.SERVICE.loadbalancer.server.port=99999 # Change to service port

If we look at the first label, you'll notice this label points to the public-network name. This label will tell Traefik to use the public-network to forward request to your service.

The second line tells Traefik that this service should be publically available.

The next two lines tell Traefik this service will be using HTTPS on the provided URL. Make sure you update this URL to your actual domain and subdomain of choice.

The TLS labels let Traefik know we are using Let's Encrypt certified TLS connections.

The last label is important because it lets Traefik know what port your service runs on. For example, you may have a service that runs on port 8080, but you want to make it available on the HTTPS port 443. The value for this label will be 8080.

And that's it! Now you can add any containers to your Docker and have Traefik expose your services securely, automatically.

It seems like a lot after reading. But utlimately the sections above our final Docker Compose file are meant to help you understand how we are configuring Traefik. I hope this helps get you started with Traefik and thank you for reading!