Traefik Proxy with HTTPS - Technical Details¶
Note about Traefik v2¶
This article is for Traefik version 1.
There is now a guide for Traefik version 2, if you are starting a new project, you should check that one at DockerSwarm.rocks/traefik/.
Warning
The technique described here using Consul to store the Let's Encrypt certificates seemed to work well at first, but was fragile and error prone, and could lead to issues later. Because of that, the Traefik team disabled it in Traefik version 2.
Consul¶
Consul by default expects to be running independent of any cluster orchestrator. To have fixed IPs, etc.
This configuration includes everything necessary to make it work in Docker Swarm, in a distributed and resilient manner.
Consul expects to have a single leader at each time, and to have all the Consul instances communicating to each other, knowing each other's specific IP address/host.
So, by default, you would have to make sure to set up a specific Consul instance per node in your cluster.
And if your cluster has more nodes, or if one node goes down, you would have to manually create another Consul instance, etc.
Consul leader¶
In this Docker Compose stack we define a single Consul leader named consul-leader
, that is able to self-elect as a leader (-boostrap
).
There is also consul-replica
, a service with multiple replicas. Each one of them will contact consul-leader
, so, they will all end up exchanging their IP addresses and being able to synchronize (including the leader, once the replicas send him their data).
consul-leader
is configured to listen to the first internal private IP address by using the environment variable CONSUL_BIND_INTERFACE
listening on the first "ethernet" (virtual) interface eth0
.
It also has an evironment variable CONSUL_LOCAL_CONFIG
with local configuration {"leave_on_terminate": true}
. This means that if you re-deploy the service, the container it will leave the cluster before being turned off. And then the new container will be able to start. Otherwise, the new container will keep trying to contact the old container, without knowing that it is supposed to replace it.
It is attached to the default
network to be able to talk to the other consul-replica
service (of multiple replica containers) and to the external network traefik-public
, to be able to expose its web user interface with Traefik.
It has several deployment labels, these are what make Traefik expose the Consul web UI with specific settings:
traefik.frontend.rule=Host:consul.${DOMAIN?Variable not set}
: use as a host, the subdomainconsul
of the domain set in the environment variableDOMAIN
. This host name is what will be used to genereate/acquire the HTTPS certificates. If there's no environment variableDOMAIN
, show the error "Variable not set
".traefik.enable=true
: tell Traefik to expose this service in the web (otherwise, it wouldn't).traefik.port=8500
: expose the content from the port8500
(that's the port inside the container).traefik.tags=${TRAEFIK_PUBLIC_TAG:-traefik-public}
: as the main Traefik proxy will only expose services with thetraefik-public
tag (using a parameter below), make the Consul service have this tag too, so that the Traefik public can find it and expose it. Use as the tag the environment variableTRAEFIK_PUBLIC_TAG
, or by default, set it totraefik-public
.traefik.docker.network=traefik-public
: tell Traefik to get the contents provided by this service using that shared network.traefik.frontend.entryPoints=http,https
: make the web UI listen and serve on HTTP and HTTPS.traefik.frontend.redirect.entryPoint=https
: make Traefik redirect HTTP trafic to HTTPS for the web UI.traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
: enable basic auth, so that not everyone can access your Traefik web dashboard, it uses the username and password created above. If those environment variables are not set, show the error "Variable not set
" or "Variable HASHED_PASSWORD not set
".
version: '3.3'
services:
consul-leader:
image: consul
command: agent -server -client=0.0.0.0 -bootstrap -ui
volumes:
- consul-data-leader:/consul/data
environment:
- CONSUL_BIND_INTERFACE=eth0
- 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
networks:
- default
- traefik-public
deploy:
labels:
- traefik.frontend.rule=Host:consul.${DOMAIN?Variable not set}
- traefik.enable=true
- traefik.port=8500
- traefik.tags=${TRAEFIK_PUBLIC_TAG:-traefik-public}
- traefik.docker.network=traefik-public
- traefik.frontend.entryPoints=http,https
- traefik.frontend.redirect.entryPoint=https
- traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
consul-replica:
image: consul
command: agent -server -client=0.0.0.0 -retry-join="consul-leader"
volumes:
- consul-data-replica:/consul/data
environment:
- CONSUL_BIND_INTERFACE=eth0
- 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
networks:
- default
- traefik-public
deploy:
replicas: ${CONSUL_REPLICAS:-3}
placement:
preferences:
- spread: node.id
traefik:
image: traefik:v1.7
ports:
- 80:80
- 443:443
deploy:
replicas: ${TRAEFIK_REPLICAS:-3}
placement:
constraints:
- node.role == manager
preferences:
- spread: node.id
labels:
- traefik.frontend.rule=Host:traefik.${DOMAIN?Variable not set}
- traefik.enable=true
- traefik.port=8080
- traefik.tags=traefik-public
- traefik.docker.network=traefik-public
- traefik.frontend.entryPoints=http,https
- traefik.frontend.redirect.entryPoint=https
- traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
--docker
--docker.swarmmode
--docker.watch
--docker.exposedbydefault=false
--constraints=tag==traefik-public
--entrypoints='Name:http Address::80'
--entrypoints='Name:https Address::443 TLS'
--consul
--consul.endpoint="consul-leader:8500"
--acme
--acme.email=${EMAIL?Variable not set}
--acme.storage="traefik/acme/account"
--acme.entryPoint=https
--acme.httpChallenge.entryPoint=http
--acme.onhostrule=true
--acme.acmelogging=true
--logLevel=INFO
--accessLog
--api
networks:
- default
- traefik-public
depends_on:
- consul-leader
volumes:
consul-data-leader:
consul-data-replica:
networks:
traefik-public:
external: true
Consul replicas¶
consul-replica
is a service with multiple replicas (multiple containers).
Each of them will try to communicate and send their data to consul-leader
, using -retry-join="consul-leader"
.
By having them do that, and not forcing the leader to try to communicate with the replicas, you can set the replication of these consul-replica
s to 0
, and the leader will still work alone, in case you have a single node.
Then, if you have new nodes, you can set the variable again, and re-deploy. Docker Swarm mode will take care of making the deployed services consistent with the new state.
It is set to distribute those replicas spread across the cluster with spread: node.id
.
It uses the environment variable CONSUL_REPLICAS
, set by default to 3, to set the number of replicas.
So, if you have a huge cluster and you want to have more Consul services, you can just update that environment variable, to a bigger number, right before deploying the stack.
As each replica will send its data (IP, ID, etc) to the service consul-leader
, that service will have all the IPs and IDs of the replicas, and then it will send their data to the rest.
That way, all the Consul instances will be able to communicate and synchronize.
version: '3.3'
services:
consul-leader:
image: consul
command: agent -server -client=0.0.0.0 -bootstrap -ui
volumes:
- consul-data-leader:/consul/data
environment:
- CONSUL_BIND_INTERFACE=eth0
- 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
networks:
- default
- traefik-public
deploy:
labels:
- traefik.frontend.rule=Host:consul.${DOMAIN?Variable not set}
- traefik.enable=true
- traefik.port=8500
- traefik.tags=${TRAEFIK_PUBLIC_TAG:-traefik-public}
- traefik.docker.network=traefik-public
- traefik.frontend.entryPoints=http,https
- traefik.frontend.redirect.entryPoint=https
- traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
consul-replica:
image: consul
command: agent -server -client=0.0.0.0 -retry-join="consul-leader"
volumes:
- consul-data-replica:/consul/data
environment:
- CONSUL_BIND_INTERFACE=eth0
- 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
networks:
- default
- traefik-public
deploy:
replicas: ${CONSUL_REPLICAS:-3}
placement:
preferences:
- spread: node.id
traefik:
image: traefik:v1.7
ports:
- 80:80
- 443:443
deploy:
replicas: ${TRAEFIK_REPLICAS:-3}
placement:
constraints:
- node.role == manager
preferences:
- spread: node.id
labels:
- traefik.frontend.rule=Host:traefik.${DOMAIN?Variable not set}
- traefik.enable=true
- traefik.port=8080
- traefik.tags=traefik-public
- traefik.docker.network=traefik-public
- traefik.frontend.entryPoints=http,https
- traefik.frontend.redirect.entryPoint=https
- traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
--docker
--docker.swarmmode
--docker.watch
--docker.exposedbydefault=false
--constraints=tag==traefik-public
--entrypoints='Name:http Address::80'
--entrypoints='Name:https Address::443 TLS'
--consul
--consul.endpoint="consul-leader:8500"
--acme
--acme.email=${EMAIL?Variable not set}
--acme.storage="traefik/acme/account"
--acme.entryPoint=https
--acme.httpChallenge.entryPoint=http
--acme.onhostrule=true
--acme.acmelogging=true
--logLevel=INFO
--accessLog
--api
networks:
- default
- traefik-public
depends_on:
- consul-leader
volumes:
consul-data-leader:
consul-data-replica:
networks:
traefik-public:
external: true
Traefik¶
The traefik
service exposes the ports 80
(standard for HTTP) and 443
(standard for HTTPS).
It creates replicas set to the environment variable TRAEFIK_REPLICAS
, or by default, 3
.
It is marked to be started on Docker Swarm manager nodes, as it needs to be able to communicate to Docker directly, to be able to read the labels you create in the rest of the stacks.
It has labels that configure how its own UI interface should be exposed (by himself), all the labels are very similar to the ones described above for Consul.
Here are some specific details:
traefik.frontend.rule=Host:traefik.${DOMAIN?Variable not set}
: use as a host, the subdomaintraefik
of the domain set in the environment variableDOMAIN
. This host name is what will be used to genereate/acquire the HTTPS certificates. If the environment variableDOMAIN
is not set, show the error "Variable not set
".traefik.port=8080
: the content of the Traefik web UI is served in the container port8080
, this tells Traefik to get the content from this port when serving pages to the public (using the standard HTTPS port,443
).
The Traefik service is configured to communicate with Docker directly, using a mounted volume for /var/run/docker.sock
. This is needed for it to be able to read the labels in the stacks you create later.
The command has several flags:
--docker
: enable Docker.--docker.swarmmode
: enable Docker Swarm Mode.--docker.watch
: enable "watch", so it reloads its config based on new stacks and labels.--docker.exposedbydefault=false
: don't expose all the services, only services with traefik.enable=true.--constraints=tag==traefik-public
: only show services with traefik.tag=traefik-public, to isolate from possible intra-stack traefik instances.--entrypoints='Name:http Address::80'
: create an entrypoint http, on port 80.--entrypoints='Name:https Address::443 TLS'
: create an entrypoint https, on port 443 with TLS enabled.--consul
: enable Consul to store configurations.--consul.endpoint="consul-leader:8500"
: use theconsul-leader
host (the service in the same stack) with its domain to communicate with Consul.--acme
: enable Let's encrypt.--acme.email=${EMAIL?Variable not set}
: let's encrypt email, using the environment variable. If it is not set, show the error "Variable not set
".--acme.storage="traefik/acme/account"
: store the HTTPS certificates in this location in Consul.--acme.entryPoint=https
: the entrypoint for Let's encrypt - created above.--acme.httpChallenge.entryPoint=http
: use HTTP for the ACME (Let's Encrypt HTTPS certificates) challenge, as HTTPS was disabled after a security issue.--acme.onhostrule=true
: get new certificates automatically with host rules: "traefik.frontend.rule=Host:web.example.com".--acme.acmelogging=true
: log Let's encrypt activity - to debug when and if it gets certificates.--logLevel=INFO
: default logging, if the web UI is not enough to debug configurations and hosts detected, or you want to see more of the logs, set it toDEBUG
. Have in mind that after some time it might affect performance.--accessLog
: enable the access log, to see and debug HTTP traffic.--api
: enable the API, which includes the dashboard.
It is connected to the internal stack default
, to store the configurations in Consul, and to traefik-public
, to be able to communicate with other services in other stacks that are also attached to that network. That's how it can later serve the content from those services.
Technically, Traefik and Consul could just use the network traefik-public
to communicate, to store configurations, HTTPS certificates, etc. But it's more explicit what each network does having the intra-stack default
network declared too.
version: '3.3'
services:
consul-leader:
image: consul
command: agent -server -client=0.0.0.0 -bootstrap -ui
volumes:
- consul-data-leader:/consul/data
environment:
- CONSUL_BIND_INTERFACE=eth0
- 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
networks:
- default
- traefik-public
deploy:
labels:
- traefik.frontend.rule=Host:consul.${DOMAIN?Variable not set}
- traefik.enable=true
- traefik.port=8500
- traefik.tags=${TRAEFIK_PUBLIC_TAG:-traefik-public}
- traefik.docker.network=traefik-public
- traefik.frontend.entryPoints=http,https
- traefik.frontend.redirect.entryPoint=https
- traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
consul-replica:
image: consul
command: agent -server -client=0.0.0.0 -retry-join="consul-leader"
volumes:
- consul-data-replica:/consul/data
environment:
- CONSUL_BIND_INTERFACE=eth0
- 'CONSUL_LOCAL_CONFIG={"leave_on_terminate": true}'
networks:
- default
- traefik-public
deploy:
replicas: ${CONSUL_REPLICAS:-3}
placement:
preferences:
- spread: node.id
traefik:
image: traefik:v1.7
ports:
- 80:80
- 443:443
deploy:
replicas: ${TRAEFIK_REPLICAS:-3}
placement:
constraints:
- node.role == manager
preferences:
- spread: node.id
labels:
- traefik.frontend.rule=Host:traefik.${DOMAIN?Variable not set}
- traefik.enable=true
- traefik.port=8080
- traefik.tags=traefik-public
- traefik.docker.network=traefik-public
- traefik.frontend.entryPoints=http,https
- traefik.frontend.redirect.entryPoint=https
- traefik.frontend.auth.basic.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
command: >
--docker
--docker.swarmmode
--docker.watch
--docker.exposedbydefault=false
--constraints=tag==traefik-public
--entrypoints='Name:http Address::80'
--entrypoints='Name:https Address::443 TLS'
--consul
--consul.endpoint="consul-leader:8500"
--acme
--acme.email=${EMAIL?Variable not set}
--acme.storage="traefik/acme/account"
--acme.entryPoint=https
--acme.httpChallenge.entryPoint=http
--acme.onhostrule=true
--acme.acmelogging=true
--logLevel=INFO
--accessLog
--api
networks:
- default
- traefik-public
depends_on:
- consul-leader
volumes:
consul-data-leader:
consul-data-replica:
networks:
traefik-public:
external: true