Skip to content

Traefik Reverse Proxy with HTTPS

Intro

So, you have a Docker Swarm mode cluster set up as described in Docker Swarm still rocks?

With Traefik you can add a versatile Load balancer and reverse proxy with HTTPS, that can do a lot of things:

  • Handle connections.
  • Expose specific services and applications based on their domain names.
  • Handle multiple domains (if you need to). Similar to "virtual hosts".
  • Handle HTTPS.
  • Acquire (generate) HTTPS certificates automatically (including renewals) with Let's Encrypt.
  • Add HTTP Basic Auth for any service that you need to protect and doesn't have its own security, etc.
  • Get all its configurations automatically from Docker labels set in your stacks (you don't need to update configuration files).

These ideas, techniques, and tools would also apply to other cluster orchestrators, like Kubernetes or Mesos, to add a main load balancer with HTTPS support, certificate generation, etc. But this article is focused on Docker Swarm mode. Traefik itself can be used as an ingress controller for Kubernetes, so you can apply the same ideas there similarly.

User Interface

The guide includes how to expose the internal Traefik web UI dashboard through the same Traefik load balancer, using a secure HTTPS certificate and HTTP Basic Auth.

How it works

The idea is to have a main load balancer/reverse proxy that covers all the Docker Swarm cluster and handles HTTPS certificates and requests for each domain.

But doing it in a way that allows you to have other Traefik services inside each stack without interfering with each other, to redirect based on path in the same stack (e.g. one container handles / for a web frontend and another handles /api for an API under the same domain), or to redirect from HTTP to HTTPS selectively.

Each stack dynamically configures Traefik, so that it can be deployed completely separated from the original (initial) Traefik stack.

Note

The setup we propose here, does not properly isolate stacks from each other, because each stack can arbitrarily configure any routes, i.e. one stack could "steal" the route from another. Security-wise we assume, that all stacks are "friendly". Because deploying a stack involves full remote control over the cluster anyway, this is a viable approach. For separating stacks you will want to use more advanced concepts (e.g. multiple Docker Swarm clusters, nested Kubernetes, Kubernetes with RBAC).

Concept

Without going into too much into detail(TM), this can be done by an overlay network in Docker Swarm that all nodes in the cluster are sharing and via the usage of the Traefik Docker provider, allowing for configuring Traefik via Docker Labels, that can be associated in a stack deployment file.

For any generic routing needs, the Traefik file provider is also used.

Preparation

  • You need either a wildcard DNS entry, that points to the public IP of any of the Docker Swarm nodes, or a DNS entry, resolving to the public IP of any of the Docker Swarm nodes
    nslookup traefik.on.dockerswarmstill.rocks
    nslookup whoami.on.dockerswarmstill.rocks
    
  • Connect to a manager node in your cluster (you might have only one node)
  • Create a network that will be shared with Traefik and the containers that should be accessible from the outside, with:
docker context use dockerswarmstillrocks
docker network create --driver=overlay traefik-public
  • Create a password for a very (HTTP) basic auth, that can be used to secure the Traefik Dashboard
  • Use openssl to generate the "hashed" version of password-is-a-bad-password and store it in an environment variable:
export ADMIN_HASHED_PASSWORD=$(openssl passwd -apr1 password-is-a-bad-password)
  • Export a email address that will be used to register the HTTPS certificates with Let's Encrypt:
export DOMAIN_EMAIL=admin@example.org

Tip: Please use a valid email address, as Let's Encrypt will send you important information about your certificates to that email address.

  • and now configure Traefik appropriately, its config consists of a services.yml, a traefik.yml and a docker-compose.yml
  • You can download templates:

    curl -L dockerswarmstill.rocks/stacks/traefik/services.yml -o services.yml
    curl -L dockerswarmstill.rocks/stacks/traefik/traefik.yml -o traefik.yml
    curl -L dockerswarmstill.rocks/stacks/traefik/docker-compose.yml -o docker-compose.yml
    curl -L dockerswarmstill.rocks/stacks/traefik/docker-compose.override.yml -o docker-compose.override.yml
    

  • Configure Traefik itself and either replace $DOMAIN_EMAIL with the email address you exported above:

    api:
      insecure: true
      dashboard: true
    #  debug: true
    
    log:
      level: ERROR
    
    accessLog: {}
    
    # uncomment to enable prometheus export
    # -> https://doc.traefik.io/traefik/observability/metrics/prometheus/
    #metrics:
    #  prometheus: {}
    
    entryPoints:
      http:
        address: :80
      https:
        address: :443
    
    
    certificatesResolvers:
      letsencrypt:
        acme:
    # this must be replaced via envsubst or similar
    # or use docker compose run --rm envsubst to stamp it
          email: $DOMAIN_EMAIL
          storage: /letsencrypt/acme.json
          tlsChallenge: {}
    #         uncomment for testing against staging...
    #      caServer: "https://acme-staging-v02.api.letsencrypt.org/directory"
    
    providers:
      file:
        filename: /etc/traefik/services.yml
        watch: true
      docker:
        exposedByDefault: false
        swarmMode: true
    
    or stamp it into it with:
    docker context use desktop-linux
    docker compose run --rm envsubst
    

    Tip: Your "local" Docker context might have a different name.

    Tip: If you get an error message, make sure, that you have also downloaded the docker-compose.override.yml file, as it contains the envsubst helper service.

  • Configure the file provider with its global middleware and the dashboard, either replace $HASHED_PASSWORD with the (hashed) password you generated above and replace $DOMAIN_TRAEFIK_DASHBOARD with the domain you want to use for the Traefik Dashboard (e.g. traefik.on.dockerswarmstill.rocks):

    http:
    # define global routers, that use global services
    # see -> https://doc.traefik.io/traefik/reference/install-configuration/providers/others/file/
      routers:
        traefik-dashboard:
          rule: "Host(`$DOMAIN_TRAEFIK_DASHBOARD`)"
          service: "dashboard@internal"
          entryPoints:
            - http
            - https
          middlewares:
            - admin-basic-auth
            - https-redirect
          tls:
            certResolver: letsencrypt
        traefik-api:
          rule: "Host(`$DOMAIN_TRAEFIK_DASHBOARD`) && PathPrefix(`/api`)"
          service: "api@internal"
          entryPoints:
            - http
            - https
          middlewares:
            - admin-basic-auth
            - https-redirect
          tls:
            certResolver: letsencrypt
    
      # global middlewares, that can be re-used
      middlewares:
        https-redirect:
          redirectScheme:
            scheme: https
            permanent: true
        admin-basic-auth:
          basicAuth:
            users:
              # this must be replaced via envsubst or similar
              # or use docker compose run --rm envsubst to stamp it
              - "admin:$ADMIN_HASHED_PASSWORD"
    
    # global routers use global services
    #  services:
    
    or export and stamp it (locally) into it with:
    export DOMAIN_TRAEFIK_DASHBOARD=traefik.on.dockerswarmstill.rocks
    docker compose run --rm envsubst
    

  • and finally configure the Traefik stack itself, either replace $DOMAIN_WHOAMI with the domain you want to use for the whoami service (e.g. whoami.on.dockerswarmstill.rocks):

    configs:
      services-yml:
        file: ./services.yml
      traefik-yml:
        file: ./traefik.yml
    
    
    services:
      reverse-proxy:
        image: traefik:v2.4
        configs:
          - source: services-yml
            target: /etc/traefik/services.yml
          - source: traefik-yml
            target: /etc/traefik/traefik.yml
        deploy:
            placement:
                constraints:
                - node.role == manager
        ports:
          # The HTTP port
          - "80:80"
          # HTTPS also
          - "443:443"
          # publish The Web UI (enabled by --api.insecure=true)
          # on a public port, uncomment to be able to reach it without DNS
          # but only for testing purposes!
    #      - "8080:8080"
        volumes:
          # Add Docker socket as a mounted volume, so that Traefik can read the labels of other services
          - /var/run/docker.sock:/var/run/docker.sock:ro
          # acme.json for le challenges
          - acme-data:/letsencrypt
        networks:
          - traefik-public
    
    # comment the following if you do not want to use the example whoami service
      whoami:
        # A container that exposes an API to show its IP address
        image: traefik/whoami:latest
        deploy:
          labels:
            - "traefik.enable=true"
            - "traefik.docker.network=traefik-public"
            # explicit http entrypoint
            - "traefik.http.routers.whoami-http.rule=Host(`${DOMAIN_WHOAMI}`)"
            - "traefik.http.routers.whoami-http.entrypoints=http"
            - "traefik.http.routers.whoami-http.service=whoamiService"
            - "traefik.http.services.whoamiService.loadbalancer.server.port=80"
            # https entrypoint
            - "traefik.http.routers.whoami-https.rule=Host(`${DOMAIN_WHOAMI}`)"
            - "traefik.http.routers.whoami-https.entrypoints=https"
            - "traefik.http.routers.whoami-https.tls=true"
            # explicitly use le as certresolver
            - "traefik.http.routers.whoami-https.tls.certresolver=letsencrypt"
            - "traefik.http.routers.whoami-https.service=whoamiHttpsService"
            - "traefik.http.services.whoamiHttpsService.loadbalancer.server.port=80"
        networks:
          - traefik-public
    
    volumes:
      acme-data:
    
    networks:
      # N.B. this must be created once on a manager node (e.g. docker network create --driver=overlay traefik-public)
      traefik-public:
        external: true
    
    or export it as an environment variable:
    export DOMAIN_WHOAMI=whoami.on.dockerswarmstill.rocks
    

Tip: The whoami service is merely for demonstrating proper usage of Traefik, if you do not want to deploy it anymore, simply remove or comment out the whoami service in the docker-compose.yml file.

Tip

Read the internal comments to learn what each configuration is for.

Deploy it

Deploy the stack with:

docker context use dockerswarmstillrocks
docker stack deploy -c docker-compose.yml traefik

Tip: Make sure, you are controlling your Docker Swarm manager.

Check it

  • Check if the stack was deployed with:
docker stack ps traefik

It will output something like:

ID             NAME                      IMAGE                   NODE                               DESIRED STATE   CURRENT STATE            ERROR     PORTS
jq7anezva94o   traefik_reverse-proxy.1   traefik:v2.4            dockerswarmstillrocks-4gb-nbg1-1   Running         Running 31 minutes ago             
zm4oaujqcvn7   traefik_whoami.1          traefik/whoami:latest   dockerswarmstillrocks-4gb-nbg1-1   Running         Running 31 minutes ago             
  • You can check the Traefik logs with:
docker service logs traefik_reverse-proxy

Check the user interface

After some seconds/minutes, Traefik will acquire the HTTPS certificates for the web user interface (UI).

You will be able to securely access the web UI at https://$DOMAIN_TRAEFIK_DASHBOARD (e.g. https://traefik.on.dockerswarmstill.rocks) using the username admin and the created password (a pity password-is-a-bad-password does not work, isn't it?).

Once you deploy a stack, you will be able to see it there and see how the different hosts and paths map to different Docker services / containers.

If you have also deployed the whoami service, you will be able to access it at https://$DOMAIN_WHOAMI (e.g. https://whoami.on.dockerswarmstill.rocks).

Advanced usage

Getting the client IP

If you need to read the client IP in your applications/stacks using the X-Forwarded-For or X-Real-IP headers provided by Traefik, you need to make Traefik listen directly, not through Docker Swarm mode, even while being deployed with Docker Swarm mode.

For that, you need to publish the ports using "host" mode.

So, the Docker Compose lines:

    ports:
      - 80:80
      - 443:443

need to be:

    ports:
      - target: 80
        published: 80
        mode: host
      - target: 443
        published: 443
        mode: host