Tobias Watzl

Programmer, photographer, engineer.

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.

Tobias Watzl

9-Minute Read

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 schematic visualization how a reverse proxy works.

A schematic visualization how a reverse proxy works.

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:

Recent posts

Categories

About

Blog