Using Traefik as reverse-proxy for my home server
This post is about how to configure and run Traefik as reverse-proxy for a docker-compose based server.
In this post I am going to describe the reverse proxy setup I am using on my private server. I am also using a very similar setup for my blog. However there is not much difference so I will simply go through my private setup here. As a matter of fact this setup is even more sophisticated as it allows to restrict some services only to my VPN.
Motivation
The first question that may arise is “Why would you need a reverse proxy in the first place?” And the short answer to this question would be “To host multiple services on the same server and redirect traffic to each one.”.
Now there are multiple options for hosting multiple services on the same server. One approach would be vhosts. However vhosts have the disadvantage that all hosted websites are on the same web server. Modern web services usually not only have a plain text website to serve, but also some backend code that needs to run. I never really looked into vhosts I have to admit, but I think they are generally a thing of the past.
A reverse proxy on the other hand can do much more than just provide access to multiple services. Traefik for example will do TLS encryption to the outside, route traffic according to which (sub)domain was entered, provide protection through basic auth and much more (load balancing, A/B testing).
Why Traefik
There are different options for reverse proxies. For example NGINX can be used as reverse proxy, haproxy and of course Traefik.
I used to have NGINX set up as reverse proxy, but that was quite cumbersome. For one the configuration is done in configuration files. When you have a containerized setup this is quite cumbersome. Whenever you wanted to have a new service you would have to manually create a new configuration and setup everything. NGINX doesn’t have service discovery, which means that the IP address of the container always has to be the same and so on. For TLS encryption I had to manually setup additional containers with certbot and all in all it did not work too reliable.
A couple of months ago I discovered Traefik and I was quite positively surprised by it. First I only intended to use it for reverse proxying the traffic from my VPN to internal services, but after some trials I decided to replace my whole NGINX setup with Traefik, because it was so awesome for use with docker compose.
So what is the main advantage of Traefik? Well for one Traefik still has configuration files. However the configurations in the files is static. That means the values that you configure in the config files only need to be configured once and won’t change much during the lifetime of your server.
An example how the config.yml file for Traefik could look like:
entryPoints:
http-internal:
address: ":8081"
http-external:
address: ":80"
https-external:
address: ":443"
providers:
file:
directory: "/config/rules"
providers:
docker:
exposedByDefault: false
defaultRule: "Host(`{{ .Name }}.internal.network`)"
On the other hand Traefik offers service discovery. That means it can detect running services automatically and route them accordingly. For example for docker containers it can host them as a subdomain under their name. E.g. if you have a container named nextcloud then it would be reachable as nextcloud.example.com.
Automatic service discovery is nice, but Traefik can do much cooler things together with docker containers.
You can use labels in your docker-compose.yml
file to configure how traefik should behave for this specific service. For example the configuration for my Traefik dashboard looks something like this:
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.internal.network`)"
- "traefik.http.routers.traefik.entrypoints=http-internal"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.docker.network=traefik-router"
Why this is a big deal you ask? Well suddenly you got all the configuration for your service in a single file. You don’t need to change any config files anymore. You can just deploy your docker-compose.yml, add the necessary labels and Traefik detects your service and you are done.
Plus if you have enabled TLS for Traefik through the config file it will automatically request a certificate and encrypt the traffic.
The Setup
So how does my Traefik setup work?
I have set up Traefik on my private server with 3 entrypoints. An entrypoint is a definition of what Traefik will expose to the outside. In my case this is:
- http-internal: an endpoint that is not encrypted, but only reachable from within my VPN.
- http-external: unencrypted http for external use. Will redirect to https page.
- https-external: encrypte https for external use. More on that later.
The basics are configured in the corresponding docker-compose.yml
, but first you have to create the data directory
$ mkdir -p /<path to your data directory>/traefik/
docker-compose.yml
:
version: '2'
volumes:
config-volume:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/<path to your data directory>/traefik/'
networks:
traefik-router:
external:
name: traefik-router
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik:v2.2
container_name: traefik
restart: unless-stopped
command:
# take config from commandline.
- "--configFile=/config/config.yml"
# Do not expose containers unless explicitly told so, this should be configured already in the config file
- "--providers.docker.exposedbydefault=false"
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.internal.network`)"
- "traefik.http.routers.traefik.entrypoints=http-internal"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.docker.network=traefik-router"
# HTTPS: redirect labels go here
- "traefik.http.middlewares.basicauth.basicauth.usersfile=/auth.users" # auth file for basicauth, comment out if you don't use it
ports:
- "192.168.0.10:80:80"
- "192.168.0.10:443:443"
# The HTTP port
- "192.168.255.10:80:8081"
# The Web UI (enabled by --api.insecure=true)
- "192.168.255.10:8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- config-volume:/config/:ro
# HTTPS: volume go here
- /<path to your data directory>/traefik/auth.users:/auth.users # auth file for basicauth, comment out if you don't use it
networks:
- "traefik-router"
You can see that docker-compose.yml
refers to /config/config.yml
as a config file. In the config file the entry points are defined as well as some rules for prohibiting auto discovery of docker containers. This is for security reasons as we want to control ourselves what is exposed and what not.
entryPoints:
http-internal:
address: ":8081"
http-external:
address: ":80"
https-external:
address: ":443"
providers:
file:
directory: "/config/rules"
providers:
docker:
exposedByDefault: false
defaultRule: "Host(`{{ .Name }}.internal.network`)"
api:
insecure: true
dashboard: true
debug: true
Network
You might have noticed the following section in the docker-compose.yml
:
networks:
traefik-router:
external:
name: traefik-router
This refers to an external network which has to be created beforehand using the following command:
$ docker network create traefik-router
The reason we need this external network is that we want to host services in separate docker-compose.yml
files and Traefik needs a way to communicate with these services.
Usage Example
The following shows how another service can be set up to be reachable via Traefik.
version: '2'
services:
syncthing:
image: syncthing/syncthing
ports:
- "8384:8384"
- "22000:22000"
- "21027:21027/udp"
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-router"
- "traefik.http.routers.syncthing.rule=Host(`syncthing.internal.network`)"
- "traefik.http.routers.syncthing.entrypoints=http-internal"
- "traefik.http.services.syncthing.loadbalancer.server.port=8384"
networks:
traefik-router:
external:
name: traefik-router
And once you run docker-compose up
your service is up and running. No need for restarting Traefik or anything.
It just works™
Note that this service does not have https. It is configured to be only reachable from the internal network.
Dashboard
Traefik offers a nice dashboard to see which services are running and which ones have issues. For security reasons the dashboard and api should of course only be reachable if authenticated. In my case I ensure this by using the entry point http-internal
which restricts access to clients within my personal VPN.
TLS
Of course we don’t want to have our services without encryption in the Internet, despite some politicians demanding the opposite. Fortunately with Traefik its awesomely easy to setup.
First you have to add the following code to your config.yml
file. It is recommended to set a real mail address.
certificatesResolvers:
letsencrypt:
acme:
email:
httpChallenge:
entryPoint: http-external
Note that the entrypoint for the challenge is the unencrypted http-external
. Let’s encrypt works by putting a secret on your server. Your server hosts that secret, let’s encrypt tries to find the secret and once it’s found it issues a certificate. That’s why it has to be reachable via http. (The chance of someone modifying the data to accidentally match the required secret is minimal.)
Then you need to add the following lines to the labels of Traefik in your docker-compose.yml
.
- "traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)"
- "traefik.http.routers.http_catchall.entrypoints=http-external"
- "traefik.http.routers.http_catchall.middlewares=https_redirect"
- "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.https_redirect.redirectscheme.permanent=true"
And add the following volume so Traefik can store the data for the TLS certificates.
volumes:
- /<path to your data directory>/traefik/acme.json:/acme.json
In the end the docker-compose.yml will look like this:
version: '2'
volumes:
config-volume:
driver: local
driver_opts:
type: 'none'
o: 'bind'
device: '/<path to your data directory>/data/traefik/'
networks:
traefik-router:
external:
name: traefik-router
services:
reverse-proxy:
# The official v2 Traefik docker image
image: traefik:v2.2
container_name: traefik
restart: unless-stopped
command:
# take config from commandline.
- "--configFile=/config/config.yml"
# Do not expose containers unless explicitly told so, this should be configured already in the config file
- "--providers.docker.exposedbydefault=false"
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`traefik.internal.network`)"
- "traefik.http.routers.traefik.entrypoints=http-internal"
- "traefik.http.services.traefik.loadbalancer.server.port=8080"
- "traefik.docker.network=traefik-router"
- "traefik.http.routers.http_catchall.rule=HostRegexp(`{any:.+}`)"
- "traefik.http.routers.http_catchall.entrypoints=http-external"
- "traefik.http.routers.http_catchall.middlewares=https_redirect"
- "traefik.http.middlewares.https_redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.https_redirect.redirectscheme.permanent=true"
- "traefik.http.middlewares.basicauth.basicauth.usersfile=/auth.users" # auth file for basicauth, comment out if you don't use it
ports:
- "192.168.0.10:80:80"
- "192.168.0.10:443:443"
# The HTTP port
- "192.168.255.10:80:8081"
# The Web UI (enabled by --api.insecure=true)
- "192.168.255.10:8080:8080"
volumes:
# So that Traefik can listen to the Docker events
- /var/run/docker.sock:/var/run/docker.sock
- config-volume:/config/:ro
- /<path to your data directory>/traefik/acme.json:/acme.json
- /<path to your data directory>/traefik/auth.users:/auth.users # auth file for basicauth, comment out if you don't use it
networks:
- "traefik-router"
Before using TLS you must also create the acme.json
:
$ sudo touch /home/twatzl/enc_data/traefik/acme.json
$ sudo chmod 600 /home/twatzl/enc_data/traefik/acme.json
Example
For using TLS encryption on one of your services you have to configure them as follows.
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik-router"
- "traefik.http.routers.nextcloud.entrypoints=https-external"
- "traefik.http.routers.nextcloud.rule=Host(`your.domain.here`)"
- "traefik.http.routers.nextcloud.tls=true"
- "traefik.http.routers.nextcloud.tls.certresolver=letsencrypt"
Note that we changed the entrypoint from http-external
to https-external
so that the service is reachable on port 443 and browsers know that it is encrypted.
Basic Auth
If you want to use basic auth as well create the necessary user file and add users with the following commands:
$ touch /<path to your data directory>/traefik/auth.users
$ htpasswd /<path to your data directory>/traefik/auth.users <username>
Restricting Services to VPN
Basically the setup shown above allows you also to restrict certain services so they are only reachable if a user is in the VPN (or local network). Of course for this to work you also have to set up your own DNS server and configure your VPN to use it as a primary DNS.
On your DNS server you can then configure a domain to redirect to your server where Traefik is running. For example you could use ‘.internal.network’ as a domain. Then you can configure a service to be reachable only from within the VPN by adding the following tags to the docker-compose file.
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.rule=Host(`myservice.internal.network`)"
- "traefik.http.routers.traefik.entrypoints=http-internal"
- "traefik.docker.network=traefik-router"
References
Of course I did not come up with all of this myself, so here are my sources for this setup:
- https://docs.traefik.io/providers/docker/#constraints
- https://www.simplecto.com/traefik-2-0-docker-and-letsencrypt/
- https://containo.us/blog/back-to-traefik-2-0-2f9aa17be305/
- https://containo.us/blog/traefik-2-0-docker-101-fc2893944b9d/
- https://chriswiegman.com/2020/01/running-nextcloud-with-docker-and-traefik-2/