Domain routing with HTTPS via Traefik Reverse Proxy
Posted on September 16, 2023 • 10 min read • 2,100 wordsHow do you tell your server which service it should make available under which (sub)domain?
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:
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.
htpasswd
All tools are completely free and open source.
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.
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)
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
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.
The browser receives this response and creates a new request, this time to https://www.example.com/index.html
.
(The IP address remains the same, which is why no new DNS request is made)
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.
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.
The browser now checks whether these companies are really trustworthy and then checks the validity of the certificate with one of them.
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.
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.
As a reverse proxy, Traefik
is therefore at the forefront and regulates the traffic arriving from the depths of the Internet:
Let's Encrypt
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.
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:
-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
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):
https://www.example.com
internally to http port 80 of the container in which the Hugo-Nginx website is runningDomain-Entry
In this example, we are the owner of the domain example.com
and configure a DNS entry for the sub-domain www.example.com
so that it points to the IP address of our server.
Outside of the example, we need our own domain address. The DNS configuration is then usually carried out in a menu of the service provider with whom the domain was registered.
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