Traefik reverse proxy with docker swarm
A reverse proxy is used to distribute the traffic over a scalable application running in several containers. This is needed since you can’t publish the port for all the containers. Traefik is a docker aware reverse proxy that can route and distribute all the incoming traffic to the correct containers. We are going to solve a different problem with this. We are giving all our virtual appliances with web UI:s simple URLs and HTTPS security.
We have a docker swarm running a bunch of virtual appliances like Duplicati, OpenVPN-as and several more. Either we access the web UIs via ip-address:port or via hostname:port and they are all on different, hard to remember port numbers. We also have no HTTP, some of the container images support it but we want a central point of ingress that handles everything.
- Proper URLs for each appliance.
- Correct port for the web UIs, 443.
- LetsEncrypt certificates for everything.
- Ability to still publish other ports directly from the container.
In my setup I use a real domain name but the setup isn’t available from outside my network. We can still get a LetsEncrypt certificate with a DNS challange.
Traefik is docker aware which means it looks at the containers and changes and adopt! So any changes in your containers will be reflected in the reverse proxy right away. No manual configuration or need to apply for additional LetsEncrypt certificates. Traefik does this by consuming labels on the containers, which also means that you can apply these settings with docker-compose, directly on the containers or via Ansible. You can also change the labels without redeploying the container and by that change the proxy functionality. Read more about Traefik and all it’s capabilities at https://traefik.io/traefik/.
To make this post more readable I have published the complete docker-compose.yaml with comments on my Github and will go over the concept here. I recommend to look at the docker-compose.yaml at the same time for context.
To be able to get a certificate from LetsEncrypt we need a real domain. The domain I use isn’t used in the wild just an old domain I own. Since I will have no external exposure of this setup I can’t use the regular HTTP challenges from LetsEncrypt, I need the DNS challenge. For this I need a DNS service that is supported by Traefik, I use CloudFlare since the service I need is provided for free.
Internally I have a wildcard DNS record for my domain, *.example.com so I don’t have to add subdomains for each service. This points to my Docker Swarm but it also prevents Traefik to verify it’s DNS changes for the challenge before requesting a certificate from LetsEncrypt. So we need to let Traefik know that it should verify with CloudFlare DNS directly. We can add that as a command line argument with:
We also need to setup two CloudFlare API tokens. One with Zone / Zone / Read for the domain it self. We else need one with Zone / DNS / Edit to be able to write the challenge updates to the DNS. How to set this up is well documented and outside the scoop of this article. We then add them as secrets and feed them in to Traefik:
CF_DNS_API_TOKEN_FILE: /run/secrets/traefik_CF_DNS_API_TOKEN CF_ZONE_API_TOKEN_FILE: /run/secrets/traefik_CF_ZONE_API_TOKEN
Then we need persistent storage for the certificates otherwise we might hit the API limit on LetsEncrypt if we redeploy the Traefik container to many times. Trust me I know! So under volumes we map the following:
The official documentation states that you can map :/acme.json directly, I couldn’t get that to work at all. So I did a bind on the entire folder. Also touch the acme.json on the storage and make it chmod 600 before mapping it to Traefik.
Configuring the containers
Now we are ready to configure the the services with labels. I will show one example with a Transmission container that runs it’s web UI via Traefik but publish it’s TCP and UDP ports for the torrents directly. You can run these as TCP/UDP services through Traefik but we have no need for that since we doesn’t load balance this service and why add an additional hop for the traffic.
networks: # The network Traefik uses to talk to the containers. # We created this in the docker-compose.yaml for the Traefik service. - traefik_default deploy: labels: # Tell Traefik that it should publish this container traefik.enable: 'true' # Tell Traefik that we want HTTPS traefik.http.routers.transmission.tls: 'true' # Give it the tlsresolver we created traefik.http.routers.transmission.tls.certresolver: tlsresolver # Add the host name we want to use traefik.http.routers.transmission.rule: Host(`transmission.example.com`) # Tell Traefik what port the container accepts the traffic traefik.http.services.transmission.loadbalancer.server.port: '9091' # Tell Traefik what domains we want the certificate for. # By giving all of them the same wildcard reference it will # re-use the same certificate traefik.http.routers.transmission.tls.domains.main: example.com traefik.http.routers.transmission.tls.domains.sans: '*.example.com' # Tell Traefik on which ingress the traffic will come traefik.http.routers.transmission.entrypoints: websecure # This is important when you publish ports as well as use Traefik. # When publishing ports the container will default to the ingress network # resulting in Traefik trying to send traffic inside the swarm on the wrong # IP-address. Please note that this is the same network we attached above. traefik.docker.network: traefik_default
With this setup we can add as many services as we like on it’s own sub-domain. It will be secured with a proper SSL certificate and no more :9091 or other port numbers to remember. Once Traefik is setup we only need to add the proper labels to every new container and it will auto populate and work straight away. No tinkering with configuration files.