Domain routing with HTTPS via Traefik Reverse Proxy

Posted on September 16, 2023 • 10 min read • 2,100 words

How do you tell your server which service it should make available under which (sub)domain?

Domain routing with HTTPS via Traefik Reverse Proxy
Photo by www.slon.pics  on Freepik 

Many owners or leasers of a root server soon realise that the one hosted service is not utilising the server to anywhere near its full capacity. This realisation soon leads to the desire to host other services themselves. But this immediately raises some questions:

  • How can you offer several services under one IP address without having to address each service laboriously via a separate port? and
  • How can you fulfil your own requirement to provide all services via secure https connections?

As complex as the problem may sound, the solution is simple: by installing a reverse proxy such as the Traefik proxy used here or the somewhat less complex Nginx Proxy Manager project, which I also like to use.

Using the example of the http web server image created in the article Hugo Webseite & Webserver im Docker-Image , the following shows how Traefik is installed as a Docker container and how an http service can be operated via https.

Technologies used

All tools are completely free and open source.

From the Request in the Browser to the Web Server

For those who want to understand how Traefik knows which request to send where (domain routing), I have summarised a quick outline of the processes below. If you are familiar with this, you can of course skip this paragraph.

After entering the request to www.example.com/index.html, a lot happens in the background until the desired page is finally displayed.

  1. Everything on the Internet is IP-based, which is why a DNS request is first made that turns www.example.com -> 93.184.216.34 (surprise, the domain really exists)

  2. The browser sends an HTTP request for the desired page to this IP address, similarly:

GET /index.html HTTP/1.1
Host: www.example.com
User-Agent: Mozilla/5.0
Accept: image/gif, image/jpeg, */*
Connection: close
  1. This request is received by a proxy software on behalf of all web servers that can be reached at 93.184.216.34. This recognises that it is responsible for requests to www.example.com, but only via encrypted communication (HTTPS). The proxy therefore responds to the browser with a redirect (status code 301) to https://www.example.com/index.html for encrypted communication.

  2. The browser receives this response and creates a new request, this time to https://www.example.com/index.html.

  3. (The IP address remains the same, which is why no new DNS request is made)

  4. The proxy at 93.184.216.34 interprets the new request and realises that it is responsible for www.example.com and also knows how/where the responsible web server can be reached.

  5. The proxy then sends the certificate for www.example.com to the browser and immediately attaches a list of (in its view) trustworthy certificate companies.

  6. The browser now checks whether these companies are really trustworthy and then checks the validity of the certificate with one of them.

  7. The proxy has now forwarded the request to www.example.com/index.html to the internal web server. For this purpose, it has an assignment of domains to IP address:port, under which they can be reached. (The web server address can be that of the proxy or a virtual address on the proxy or a non-public address in a private subnet). The important thing is that this part of the communication takes place within secure walls and can therefore take place via HTTP.

  8. The web server interprets the request, generates the corresponding web page and sends it to the proxy, which then sends it back to the browser in encrypted form.

1. Traefik as Reverse Proxy with Let’s Encrypt

Traefik Architecture
Image by TraefikLabs from Documentation

As a reverse proxy, Traefik is therefore at the forefront and regulates the traffic arriving from the depths of the Internet:

  • Management of the externally visible ports 80 (HTTP) and 443 (HTTPS)
  • Checking whether a web server is configured for the incoming request
  • Possibly forwarding from HTTP to HTTPS
  • Management of SSL certificates and related communication with the browser
  • Automatic request/renewal of certificates with Let's Encrypt
  • Internal communication with the actual web server
  • Load balancing of requests if multiple web servers are configured
  • Enables HTTP endpoints to communicate securely with the outside world via HTTPS

The last point in particular is very attractive, as it allows our Hugo Nginx container, which only offers one HTTP endpoint, to be published securely on the Internet.

Installing Traefik

Due to the more extensive configuration in terms of writing, this is formulated for Docker Compose . Docker Compose is an alternative way of communicating with Docker, makes it much easier to work with containers and improves the readability of the configuration files.

Let’s first create a Docker Compose file to configure Traefik so that it can later publish the Hugo-Nginx container via HTTPS.

version: '3.3'

services:
  traefik:
    # Use latest v3.x Traefik image
    image: traefik:v3.0
    restart: unless-stopped
    ports:
      # Open Port 80 (HTTP), needed for forwarding to HTTPS
      - target: 80
        published: 80
        mode: host
      # Open Port 443 (HTTPS)
      - target: 443
        published: 443
        mode: host
    labels:
      # Enable starting Traefik Services
      - traefik.enable=true
      # Name of the Docker-Network the endpoints are located in 
      - traefik.docker.network=traefik-public
      # All services need to use this label
      - traefik.constraint-label=traefik-public
      # Admin-auth Middleware with HTTP Basic auth
      # Use environment variables USERNAME and HASHED_PASSWORD
      - traefik.http.middlewares.admin-auth.basicauth.users=${USERNAME?Variable not set}:${HASHED_PASSWORD?Variable not set}
      # Https-redirect Middleware to forward HTTP to HTTPS
      - traefik.http.middlewares.https-redirect.redirectscheme.scheme=https
      - traefik.http.middlewares.https-redirect.redirectscheme.permanent=true
      # Traefik-http configuration, just for forwarding to https
      # Use environment variable DOMAIN
      - traefik.http.routers.traefik-public-http.rule=Host(`${DOMAIN?Variable not set}`)
      - traefik.http.routers.traefik-public-http.entrypoints=http
      - traefik.http.routers.traefik-public-http.middlewares=https-redirect
      # Traefik-https configuration for HTTPS Router
      # Use environment variable DOMAIN
      - traefik.http.routers.traefik-public-https.rule=Host(`${DOMAIN?Variable not set}`)
      - traefik.http.routers.traefik-public-https.entrypoints=https
      - traefik.http.routers.traefik-public-https.tls=true
      # Use specific Traefik service api@internal for Web UI/Dashboard
      - traefik.http.routers.traefik-public-https.service=api@internal
      # Use "le" (Let's Encrypt), created below 
      - traefik.http.routers.traefik-public-https.tls.certresolver=le
      # Enable HTTP Basic auth using the above created Middleware
      - traefik.http.routers.traefik-public-https.middlewares=admin-auth
      # Define and use this Docker service internal port
      - traefik.http.services.traefik-public.loadbalancer.server.port=8080

    volumes:
      # Add Docker, so Traefik can read labels of other services
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # Add volume to store certificates
      - /production/traefik/:/certificates

    command:
      # Enable access to Docker, to read labels of other services
      - --providers.docker
      # Only use services with these labels
      # "traefik.constraint-label=traefik-public"
      - --providers.docker.constraints=Label(`traefik.constraint-label`, `traefik-public`)
      # Publish only explicitly mentioned Docker services to the outside
      - --providers.docker.exposedbydefault=false
      # Add "http" endpoint on Port 80
      - --entrypoints.http.address=:80
      # Add "https" endpoint on Port 443
      - --entrypoints.https.address=:443
      # Create certificat service "le" for Let's Encrypt, use environment variable EMAIL
      - --certificatesresolvers.le.acme.email=${EMAIL?Variable not set}
      # Store Let's Encrypt certifikcate to created volume
      - --certificatesresolvers.le.acme.storage=/certificates/acme.json
      # Use TLS Challenge for Let's Encrypt
      - --certificatesresolvers.le.acme.tlschallenge=true
      # Enable Access logs for HTTP requests
      - --accesslog
      # Enable Traefik logs for changing configuration or errors 
      - --log
      # Enable use of dashboards and API
      - --api
    networks:
      # Common network for Traefik  and all endpoints
      - traefik-public

networks:
  # Define common network "traefik-public"
  traefik-public:
    external: true

A separate Docker network is created for the server-internal data traffic between Traefik and the connected services (traefik-public). This isolates the data streams and thus increases security. Traefik is configured via --providers.docker.constraints so that it is only available to containers that are authorised to communicate with this virtual network.

The network is created via:

docker network create traefik-public

The referenced environment variables are specified in a .env.prod file, which increases the reusability of the docker-compose file.

# User name for Traefik
export USERNAME="<user>"
# Hashed Password for Traefik (escape "$" -> "\$")
export HASHED_PASSWORD="<hashed super password>"
# Domain to reach Traefik Dashboard
export DOMAIN="<domain>"
# EMail used for creating certificate with Let's Encrypt
export EMAIL="admin@<domain>"

The Traefik documentation points out that the HASHED_PASSWORD can be created with htpasswd, which is part of the Apache utils

sudo apt install apache2-utils
htpasswd -nb <user> "<super password>"
<user>:<hashed super password>

From the output, copy the hashed password after the colon (<hashed super password>) and copy it into .env.prod file as HASHED_PASSWORD.

Traefik is then started on the server by first setting the environment variables and then starting the service. (Presumably the image of Docker Hub is loaded and unpacked at the first start).

source .env.prod
docker-compose -f ./docker-compose.traefik.yml up

Traefik is then started in the console and shows the current log output. This is useful for finding typing errors at the first start.

If everything went well, you can reach the Traefik dashboard under the domain address specified in .env.prod with username and password, which looks something like this:


If everything runs satisfactorily, stop the execution of Traefik via CTRL+C and start it again, but this time using the parameter -d, which sends the service to the background.

docker-compose -f ./docker-compose.traefik.yml up -d

To stop the service

docker-compose ./docker-compose.traefik.yml down

2. Configuring the Hugo-Nginx-Container

Nothing has happened yet, except that we have installed a bouncer that looks at the incoming requests and (kindly) rejects them.

Let’s stick with the example of www.example.com and create a Docker Compose file to configure a Hugo Nginx container so that it is published by Traefik via HTTPS.

version: "3.9"

services:
  hugo_nginx:
    # Name of the container
    hostname: hugo-nginx
    # Use this image to be published via Traefik
    image: hugo-nginx:latest
    restart: unless-stopped
    labels:
      # Enable use of Traefik for this service
      - traefik.enable=true
      # Name of Docker network containing all endpoints
      - traefik.docker.network=traefik-public
      - traefik.constraint-label=traefik-public
      # Define routers with your own name for the website, e.g. "example_site"
      # HTTP for forwarding to HTTPS
      - traefik.http.routers.example_site-http.service=example_site
      - traefik.http.routers.example_site-http.rule=Host("example.com") || Host("www.example.com")  # << replace Domain
      - traefik.http.routers.example_site-http.entrypoints=http
      - traefik.http.routers.example_site-http.middlewares=https-redirect
      # HTTPS
      - traefik.http.routers.example_site-https.rule=Host("example.com") || Host("www.example.com")  # << replace Domain
      - traefik.http.routers.example_site-https.entrypoints=https
      - traefik.http.routers.example_site-https.tls=true
      # Use Let's Encrypt
      - traefik.http.routers.example_site-https.tls.certresolver=le
      # Webserver port to reach the Hugo-Nginx website
      # (this is the port of the virtuell IP running Hugo-NginX-Container, not the Host-Port)
      - traefik.http.services.example_site.loadbalancer.server.port=80

    networks:
      - traefik-public

networks:
  traefik-public:
    external: true

This configuration first creates a container of our hugo-nginx image and allows it to be a participant in the traefik-public network. At the same time, the configuration instructs Traefik via the specified labels (which can be read by Traefik):

  • Automatically redirect HTTP requests to HTTPS
  • Forward external requests to https://www.example.com internally to http port 80 of the container in which the Hugo-Nginx website is running
  • Automatically generate and renew HTTPS certificates via Let’s Encrypt

After a DNS entry has been changed, it takes a few (or a few more) minutes for it to be published worldwide and therefore becoming available. Impatient users can ping the domain to check whether it is already pointing to the server

ping www.example.com
ping: www.example.com: Name or service not known
[... some minutes later ...]
ping www.example.com
PING www.example.com (93.184.216.34) 56(84) bytes of data.
64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=1 ttl=54 time=112 ms
64 bytes from 93.184.216.34 (93.184.216.34): icmp_seq=2 ttl=54 time=111 ms
[...]

Now we have everything together and can start the container with the website, initially in the foreground again.

docker-compose -f ./docker-compose.hugo-nginx.yml up

If we now enter http://www.example.com in the browser, we get to see the Hugo Nginx website, which is redirected to https:// and delivered via HTTPS with a valid certificate. When everything is running, we stop/start the service and send it to the background.

docker-compose -f ./docker-compose.hugo-nginx.yml up -d

Stop serving the website any time using

docker-compose -f ./docker-compose.hugo-nginx.yml down