I was recently introduced to a new software called Traefik.

A reverse proxy / load balancer that’s easy, dynamic, automatic, fast, full-featured, open source, production proven, provides metrics, and integrates with every major cluster technology… No wonder it’s so popular!

What else to say? Sounds exactly like a tool I would love. As my setup is already based on a great diversity of docker containers it sounded interesting to me to have a reverse proxy that is handling routing and load balancing automatically to them and can even manage the necessary Let’s encrypt certificates for me.

Setup

Traefik config

I am using my own dns server (https://portal.beechy.de) for Let’s encrypt dns verification which is supporting the httpreq provider. If you are interesed in using my dns server aswell, feel free to create an account for nothing, but a smile. It’s supporting all diffrent types of records, low ttls, dyndns with ipv6 support and an inbuild email server with spam protection.

Currently there is a bug in legos (acme libary used by Traefik) httpreq provider which cuts the endpoint url, but the PR is likely to be merged soon. https://github.com/xenolf/lego/pull/781

version: '3'

services:
  traefik:
    image: traefik
    container_name: traefik
    environment:
      - HTTPREQ_ENDPOINT=https://portal.beechy.de/api/dns/acme/
      - HTTPREQ_USERNAME=apitokeninportalsettings
      - HTTPREQ_PASSWORD=notneeded
    labels:
      - traefik.enable=true
      - traefik.backend=traefik
      - traefik.frontend.rule=Host:traefik.example.com
      - traefik.port=8080
    restart: always
    ports:
      - 80:80
      - 443:443
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./traefik.toml:/traefik.toml
      - ./acme.json:/acme.json
      - ./rules:/rules
# traefik.toml

#logLevel = "DEBUG"
defaultEntryPoints = ["http", "https"]

[web]
address = ":8080"
  [web.auth.basic]
  users = ["admin:httpasswd-hash"]

[entryPoints]
  [entryPoints.http]
  address = ":80"
    [entryPoints.http.redirect]
      entryPoint = "https"
  [entryPoints.https]
  address = ":443"
    [entryPoints.https.tls]

[acme]
email = "root@example.com"
storage = "acme.json"
entryPoint = "https"
onHostRule = true
exposedbydefault = false
acmeLogging = true
  [acme.dnsChallenge]
  provider = "httpreq"
  delayBeforeCheck = 10

[docker]
endpoint = "unix:///var/run/docker.sock"
watch = true
exposedByDefault = false
usebindportip = true
swarmMode = false
#swarmModeRefreshSeconds = 15

[file]
directory = "/rules"
watch = true

Website configs

I am currently providing 4 types of “websites”:

  • proxies to containers (Docker)
  • proxies via backend VPN
  • PHP websites
  • redirects (alias domains)

Proxies to containers (Docker)

Proxies to Docker containers like gitea or portainer and fusionauth are super simple to setup. I only had to recreate my containers and add a few labels to them:

docker run -td \
     --name portainer \
     --restart always \
     -v /var/run/docker.sock:/var/run/docker.sock \
     -v $PWD/data:/data \
     -l traefik.enable=true \
     -l traefik.backend=portainer \
     -l traefik.frontend.rule=Host:portainer.example.com \
     -l traefik.port=9000 \
     portainer/portainer

Proxies via backend VPN (File)

For my proxies accessed via my secured backend VPN (things like plex on my home server) I decided to use the Traefiks file backend with multiple .toml files. One for each domain I wanted to add.

# rules/plex.example.com.toml

[backends]
  [backends.plex]
    [backends.plex.servers]
      [backends.plex.servers.server0]
        url = "http://10.8.1.3:32400"
        weight = 1

[frontends]
  [frontends.plex-example-com]
    entryPoints = ["http", "https"]
    backend = "plex"
    passHostHeader = true
  [frontends.plex-example-com.routes.route1]
    rule = "Host:plex.example.com"

PHP websites

But the tricky part started by adding my php websites to Traefik. As Traefik is unable to talk to php directly I decided to leave nginx in front of php as the old fast webserver and let Traefik only manange ssl termination. Most of the professionals out there would have one seperate nginx + php container per domain, but my VPS doesn’t have that much disk space fo seperate images and I am lazy in setting php up with smtp and so on in multiple containers, so I took one nginx and one php for all sites and added multiple frontends in Traefik pointing to my “php-backend”:

# rules/php-backend.toml

[backends]
  [backends.php]
    [backends.php.servers]
      [backends.php.servers.server0]
        url = "http://nginx"
        weight = 1
# rules/beechy.de.toml

[frontends]
  [frontends.beechy-de]
    entryPoints = ["http", "https"]
    backend = "php"
    passHostHeader = true
  [frontends.beechy-de.routes.route1]
    rule = "Host:beechy.de,www.beechy.de"

Special nginx behind reverse proxy settings

After trying to run Traefik for the first time I faced two problems I already know from the good old days using ha-proxy. First: php isn’t not getting the correct client ip and second: browsers are sent into redirecting loops from some applications, because they are trying to force https which they wont get. This is due Traefik terminating ssl traffic and only requesting http from nginx.

By using real_ip_header X-Forwarded-For; we tell the ngx_http_realipmodule from which header it should get the correct client ip and by passing _fastcgi_param HTTPS $fastcgi_param_https_variable; php knows if it was accessed over https and wont redirect us.

location ~ \.php$ {
  try_files $uri =404;
  fastcgi_split_path_info ^(.+\.php)(/.+)$;
  fastcgi_pass php:9000;
  fastcgi_index index.php;
  fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
  set_real_ip_from ::/0;
  set_real_ip_from 0.0.0.0/0;
  real_ip_header X-Forwarded-For;
  fastcgi_param HTTPS $fastcgi_param_https_variable;
  include fastcgi_params;
}
# nginx.conf

http {
    ...
    map $http_x_forwarded_proto $fastcgi_param_https_variable {
         default '';
         https 'on';
    }
    ...
}

https://stackoverflow.com/questions/25929599/nginx-replace-remote-addr-with-x-forwarded-for

https://serverfault.com/questions/875245/nginx-how-to-enable-ssl-for-php-when-behind-a-reverse-proxy

Redirects (alias domains)

redirect from rediret.example.com to example.com

# rules/redirect.example.com.toml

[frontends]
  [frontends.redirect-example-com]
    entryPoints = ["http", "https"]
    [frontends.redirect-example-com.redirect]
        regex=^https?://redirect.example.com/(.*)
        replacement=https://example.com/$${1}
  [frontends.beechy-de.routes.route1]
    rule = "Host:redirect.example.com"