If you’re like me and run multiple services through Docker containers on your home lab server, you’ve likely found yourself struggling to remember the port numbers you assigned to a given service.
You may have also felt the ‘Not Secure’ warning beckoning you from the URL bar every time you open an application, begging for SSL.
No more!
Traefik is here to solve all your routing and SSL issues. Not just for your docker containers but all of your services, even the ones running on a separate server entirely β like your router’s web UI.
Before we begin#
This guide assumes Docker Compose is being used to manage containers. It also assumes you have a domain and are managing your DNS through Cloudflare. Other DNS providers may work but YMMV.
File Structure#
For starters, create the following file structure on your docker host. The rest of the guide assumes your files are in this structure:
~/
βββ docker/
β βββ traefik/
β β βββ docker-compose.yml
β βββ code-server/
β βββ docker-compose.yml
βββ configs/
βββ traefik/
βββ acme.json
βββ traefik.yml
βββ dynamic/
βββ config.yml
Don’t worry about the content of these files for now.
Configuring Traefik#
Traefik configuration is loaded from two files;Β traefik.ymlΒ and the dynamicΒ config.yml.
Your traefik.yml will look something like this:
api:
dashboard: true
debug: true
metrics:
prometheus: {}
entryPoints:
http:
address: ":80"
https:
address: ":443"
serversTransport:
insecureSkipVerify: true
providers:
docker:
endpoint: "unix:///var/run/docker.sock"
exposedByDefault: false
file:
directory: /dynamic
watch: true
certificatesResolvers:
cloudflare:
acme:
email: [email protected] #replace with your cloudflare email address
storage: acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
The dynamic file can be used to configure middleware and external services. We’ll come back to that shortly.
The Traefik Container#
First off, we want to create a new directory and docker-compose file for Traefik. It should look something like this:
# docker-compose.yml
version: "3"
services:
traefik:
image: traefik:latest
container_name: traefik
networks:
- traefik
environment:
# Enter your Cloudflare API credentials here
- CF_API_EMAIL=
- CF_API_KEY=
ports:
- "80:80"
- "443:443"
volumes:
- /etc/locatime:/etc/localtime:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /[docker parent path]/configs/traefik/traefik.yml:/traefik.yml:ro
- /[docker parent path]/configs/traefik/acme.json:/acme.json # where your certificates will be stored
- /[docker parent path]/configs/traefik/dynamic:/dynamic:ro # The dynamic directory allows you to make 'hot' changes to traefik configuration without restarting the container
labels:
- "traefik.enable=true"
- "traefik.http.routers.traefik.entrypoints=http"
- "traefik.http.routers.traefik.rule=Host(`traefik.subdomain.example.com`)" # Replace with your subdomain
- "traefik.http.middlewares.traefik-https-redirect.redirectscheme.scheme=https"
- "traefik.http.middlewares.sslheader.headers.customrequestheaders.X-Forwarded-Proto=https"
- "traefik.http.routers.traefik.middlewares=traefik-https-redirect"
- "traefik.http.routers.traefik-secure.entrypoints=https"
- "traefik.http.routers.traefik-secure.rule=Host(`traefik.subdomain.example.com`)"
- "traefik.http.routers.traefik-secure.tls=true"
- "traefik.http.routers.traefik-secure.tls.certresolver=cloudflare"
- "traefik.http.routers.traefik-secure.tls.domains[0].main=subdomain.example.com" # The subdomain configured for traefik E.g. homelab.scottmckendry.tech
- "traefik.http.routers.traefik-secure.tls.domains[0].sans=*.subdomain.example.com" # The SANs wildcard domain the will be used for all services
- "traefik.http.routers.traefik-secure.service=api@internal"
restart: unless-stopped
networks:
traefik:
external: true
IMPORTANT:Β Make sure you add a DNS entry for your Traefik container and point it to your public IP address. You’ll also need to configure port forwarding rules for ports 80 and 443 on your router to your server.
Help:
Check your file carefully and make sure to replace any dummy variables, paths and domains!
Create a Docker Network and Start Traefik#
If we start the Traefik container with the config above, we’ll get the following output:
ERROR: Network traefik declared as external, but could not be found. Please create the network manually using `docker network create traefik` and try again.
In order for Traefik to communicate with other containers, we need to create a network. We can create one using the same syntax in the error message above:
docker network create traefik
Now we can start our Traefik container.
To start and restart containers, run the following from the directory containing your docker-compose.yaml file.
docker-compose up -d --force-recreate
The --force-recreate
flag restarts the container regardless of any changes to the compose file.
Now that we’ve started the container we should be able to access the Traefik dashboard by going to “traefik.subdomain.example.com”. If everything is configured correctly, you should see the dashboard below:
You’ll also see a valid certificate in the address bar. Congratulations, you’re now serving your first HTTPS web application.
Adding Containers#
Now that Traefik is up and running, we can start adding services behind it. As an example, we’ll use theΒ Linuxserver.io’s openvscode-serverΒ docker image.
Old Config#
The default docker-compose file is below:
---
version: "2.1"
services:
openvscode-server:
image: lscr.io/linuxserver/openvscode-server:latest
container_name: openvscode-server
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
ports:
- 3000:3000
restart: unless-stopped
This will serve a VS Code server locally on http://[YourServerIp]:3000. With a few tweaks, we can serve it on our own subdomain with SSL.
New Config#
With a few updates, we can ‘Traefik-ify’ the container:
---
version: "2.1"
services:
openvscode-server:
image: lscr.io/linuxserver/openvscode-server:latest
container_name: openvscode-server
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/London
# ports:
# - 3000:3000
restart: unless-stopped
# Traefik configuration:
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.code.entrypoints=http"
- "traefik.http.routers.code.rule=Host(`code.subdomain.example.com`)"
- "traefik.http.middlewares.code-https-redirect.redirectscheme.scheme=https"
- "traefik.http.routers.code.middlewares=code-https-redirect"
- "traefik.http.routers.code-secure.entrypoints=https"
- "traefik.http.routers.code-secure.rule=Host(`code.subdomain.example.com`)"
- "traefik.http.routers.code-secure.tls=true"
- "traefik.http.routers.code-secure.service=code"
- "traefik.http.services.code.loadbalancer.server.port=3000"
- "traefik.docker.network=home"
networks:
traefik:
external: true
π‘ Don’t forget to add your DNS entry for ‘code.subdomain’!
Start the container and navigate to the new URL. Great! Now we have a template we can apply to all of our containers.
But what if we want to expose a service to the public internet? We can’t leave this container in its current state, anyone can use it! That’s where middleware comes into play.
Middlewares#
Middlewares allow Traefik to manipulate requests before they reach your services. They can even outright deny access altogether. There are several middlewares available in the Traefik Plugin Catalog.
The three we’re going to implement to tighten up security on our services are:
- BasicAuthΒ - Add a username and password to any Traefik router.
- RateLimitΒ - Restrict the number of requests sent to a given service.
- IPWhiteListΒ - Restrict access to a given service based on the IP address.
We define these in our dynamic config.yml file as shown below:
http:
middlewares:
auth:
basicAuth:
users:
- "user:5f4dcc3b5aa765d61d8327deb882cf99" #MD5 Hash of "password"
ratelimit:
ratelimit:
average: 100
burst: 50
ipWhiteList:
ipWhiteList:
sourceRange:
- "192.168.1.0/24" #restricting to devices on the local network
- "172.16.1.0/24" #and other containers on the same network e.g. the "traefik" network we created earlier.
htpasswd
in a bash shell.To add our newly created middleware to our container, we add the following label:
- "traefik.http.routers.code-secure.middlewares=auth@file, ipWhiteList@file, ratelimit@file"
Restart the container for our changes to take effect. When you reopen code server, you get a prompt to enter your username and password.
If your IP is not whitelisted, you’ll get a 403 forbidden error.
Adding External Services#
We now have a template for serving docker containers behind Traefik. But what if we want to add an external service behind the Traefik proxy? For example, our router/firewall configuration page?
This can also be done in the dynamic config.yaml file. First, we’ll want to define a new middleware called “defaultHeaders” we can tack this on underneath the middleware we created earlier:
deafultHeaders:
headers:
frameDeny: true
sslRedirect: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
customFrameOptionsValue: SAMEORIGIN
customRequestHeaders:
X-Forwarded-Proto: https
Without going into great detail, the above headers should allow most services to work behind Traefik. At least, I haven’t run into any issues yet.
Now we can add an external service. I’m going to add my router’s admin page. This can go beneath our default headers middleware:
routers:
router:
entrypoints:
- "https"
rule: "Host(`router.subdomain.example.com`)"
middlewares:
- ratelimit
- ipWhiteList
- auth
- deafultHeaders
tls: {}
service: router
services:
router:
loadBalancer:
servers:
- url: "https://192.168.1.1"
As you can see, we’ve also added our other middleware as well as default headers.
At this point, our config.yaml file should look something like this:
http:
middlewares:
auth:
basicAuth:
users:
- "user:5f4dcc3b5aa765d61d8327deb882cf99" #MD5 Hash of "password"
ratelimit:
ratelimit:
average: 100
burst: 50
ipWhiteList:
ipWhiteList:
sourceRange:
- "192.168.1.0/24" #restricting to devices on the local network
- "172.16.1.0/24" #and other containers on the same network e.g. the "traefik" network we created earlier.
deafultHeaders:
headers:
frameDeny: true
sslRedirect: true
browserXssFilter: true
contentTypeNosniff: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 15552000
customFrameOptionsValue: SAMEORIGIN
customRequestHeaders:
X-Forwarded-Proto: https
routers:
router:
entrypoints:
- "https"
rule: "Host(`router.subdomain.example.com`)"
middlewares:
- ratelimit
- ipWhiteList
- auth
- deafultHeaders
tls: {}
service: router
services:
router:
loadBalancer:
servers:
- url: "https://192.168.1.1"
And that’s it!
We’ve now successfully configured Traefik, a container and an external service all with valid SSL certificates.
My full config is available on my GitHub:
My Docker Compose and config for Traefik