Ultimate Traefik Docker Compose Guide [2022] with LetsEncrypt

This step-by-step Traefik Docker Compose tutorial will help take your Docker server to the next level with simplified SSL privacy and security.

It has been over two years since I published my first Docker Traefik guide, which has helped hundreds of thousands of people. To go along with this guide, I also published my setup on GitHub.

Note: An updated version of this guide is available: Ultimate Traefik Docker Compose Guide [2024]: LE, SSL, Reverse Proxy

However, over the last two years, my setup has evolved and the differences between the original guide and GitHub repo kept growing, even though I did my best to keep the Traefik Docker Compose guide up-to-date. [Read: 60+ Best Docker Containers for Home Server Beginners 2023]

With the release of Ubuntu 22.04 Jammy Jellyfish, an LTS release, I updated all my stacks to the latest versions of everything. Recently, I updated my Docker media server guide, which now includes a simple reverse proxy with LetsEncrypt SSL using Nginx Proxy Manager. [Read: Podman vs Docker: 6 Reasons why I am HAPPY I switched]

But Traefik is a much more powerful reverse proxy, which is why I still use it. So without further ado, let us begin our Traefik Docker-Compose stack.

Table of Contents

About This Docker Traefik Stack

This Docker Traefik compose how-to is a follow up to my recently published Docker media server guide. If you aren't familiar with my setup/ecosystem, you will be lost.

Confused Ryan Reynolds | Smarthomebeginner

If you are just starting, then I strongly recommend that you start with the above guide, leave out Nginx Proxy Manager, and continue with this guide to add the Docker Compose Traefik bit.

We are going to cover most of everything there is to set up a Docker Home Server with Traefik 2, LetsEncrypt SSL certificates, and Authentication (Basic Auth) for security. However, frequently, I will refer you back to my previous guides for some reading to not make this guide too lengthy.

This Traefik Reverse Proxy Docker stack is a key component of my smart home setup.

Differences From Previous Traefik Docker Guide

As mentioned before, since the original publication of the Docker Traefik guide for Ubuntu 20.04, my setup changed significantly. You can review the commit logs on my GitHub for the sequence of changes. I try to leave detailed comments.

But here are some key changes:

  • Implemented Docker Secrets and extensions.
  • Added a Docker Socket Proxy.
  • Several apps added/removed.
  • Disconinued adding user to Docker group - use sudo with docker commands.
  • Moved from TOML to YAML for Traefik configuration files - you will still find TOML examples in my repo.
  • Moved all app data to one folder called appdata.
  • Published my bash_aliases Github, which shows how I simplified some of the admin tasks.

There are many more minor changes.

In this guide, however, I will be removing some of the topics (to shorten and simplify the guide) that were discussed in my previous guide. I will link to relevant sections as needed. So, you won't be lost. You can easily add those using other existing guides on this site.

If you are interested in adding these apps, complete this guide first and head over to the guides linked above.

Objectives of this Traefik 2 Docker Home Server Setup

My objectives for this setup remain pretty much the same as explained in my Docker media server guide. With a reverse proxy included (Nginx Proxy Manager), the previous guide should work for many homelab users.

It already offers:

  • Secure access to apps over the internet without having to port-forward on the router
  • An easy way to access the apps using user-friendly domain names instead of ports
  • A higher level of security since the apps will be exposed to the internet

Traefik vs Nginx Proxy Manager

But Traefik offers several advanced features over Nginx Proxy Manager:

  • Scalability.
  • Metrics.
  • Traefik Pilot Integration.
  • Easier way to configure proxies via Docker Compose, instead of GUI.
  • Better integration with Authentication services like Google Oauth and Authelia, compared to just basic HTTP authentication in NPM.
  • And Enterprise support.

For me, the integration with authentication services is a big deal. In addition, I can also do some advanced routing that allows bypassing authentication for certain apps (eg. Radarr or Sonarr remote mobile phone app).

Requirements for this Docker Traefik 2 Setup

Several requirements must be met before proceeding with this Docker tutorial for setting up microservices behind Traefik reverse proxy. These are exactly the same as those listed in my Docker media server guide:

  1. Host Operating System - Debian/Ubuntu Server. Any RPM based distro should be fine too with some minor tweaks.
  2. Docker - Installed
    Install Docker on Ubuntu (with Compose) - Don't Do It WRONG

  3. Docker Compose - Installed separate or as Docker Plugin
  4. Domain Name
  5. Proper DNS Records - A minimum of 2 records. A record pointing to WAN IP and CNAME record (or a wildcard (*)) pointing to the root domain.
    Note: If you orange-cloud the DNS records on Cloudflare (i.e. Cloudflare proxy enabled) then you will not see the LetsEncrypt certificate when it is pulled. Since your service is behind Cloudflare proxy, you will see Cloudflare's SSL certificate. Therefore, during inital testing and setup I recommend leaving the proxy off (gray-cloud). After ensuring that proper LetsEncrypt certificates are pulled, you can enable Cloudflare proxy, following which you will only see Cloudflare's SSL certificates.
  6. Cloudflare SSL Settings - Full SSL
  7. Port 80 and 443 Forwarding for Traefik

Check the above sections to ensure you have everything before proceeding with this Traefik Docker Compose guide.

In addition, you will also need to:

  1. Setup the Docker environment exactly as described in the Docker guide.
  2. Familiarize yourself with my GitHub Repo - You can use any of the YML files with t2 (for Traefik v2) for reference. My main one is docker-compose-t2.yml.

Be the 1 in 200,000. Help us sustain what we do.
25 / 150 by Dec 31, 2024
Join Us (starting from just $1.67/month)

Traefik 2 Setup

Note: An updated version of this guide is available: Ultimate Traefik Docker Compose Guide [2024]: LE, SSL, Reverse Proxy

Unlike my previous Traefik guides, I am not going to show you the docker-compose examples for all the 50 or so apps I run. Instead, my approach with this Traefik Docker Compose guide is to show you how to set up Traefik 2 and a few apps as examples.

The example apps I have chosen will showcase various possibilities with Traefik 2 and cover some common scenarios. You can use these as examples and adapt the docker-compose examples for other apps.

Try Auto-Traefik
Automatically setup Docker, Socket Proxy, and Traefik with Proper Let's Encrypt Certificates.

Traefik Reverse Proxy Overview

Traefik reverse proxy provides convenience and security for your internet-facing services (eg. Radarr, Sonarr, SABnzbd, WordPress, Nginx, etc.). A reverse proxy server typically sits behind a firewall (router or internet gateway) and directs clients to the appropriate apps using a common name (radarr.example.com) without the client having to know the server's IP address or port. The client interacts with the reverse proxy and the reverse proxy directs the communication to the back-end app to provide/retrieve information.

Docker Traefik Guide - Reverse Proxy Schematic
Reverse Proxy Schematic

Basic information on reverse proxy has already been covered in detail in my previous Traefik tutorial. Here are the links to relevant sections for your review:

In a nutshell, we are going to use Traefik mainly to:

  • Put our apps behind a convenient and easy-to-remember URL
  • Add basic HTTP authentication for the apps
  • Add security headers for the web interfaces of the apps
  • Reduce security risks by avoiding port forwarding to individual apps (not exposing them directly to the internet)
  • Put all our services behind LetsEncrypt SSL certificates that are automatically pulled and renewed by Traefik 2

In my previous guide, I discussed two ways for accessing your apps from the internet: subdirectory and subdomain.

In this guide, for the sake of convenience, we are only going to show you the subdomain method with a private domain name. If you want to go with the subdirectory method, you can combine the information from my previous guide to figure things out (in my opinion, it is worth spending a few bucks on a domain name - only $8 on Cloudflare per year).

Traefik 2 Routers, Middlewares, and Services

Traefik v2 uses three components major components in the configuration: Routers, Services, and Middlewares.

Traefik Provider
Traefik Providers Overview

Routers

Routers are like frontends, they manage the incoming requests. In the routers section, youโ€™ll define the entrypoint, certificate resolver, and define the rules for the request.

Services

Services are like backends, they identify where to send requests. This is where you can define the port that Traefik will proxy to and any additional load balancers.

Middlewares

Middlewares are one of the exciting new features of Traefik v2. Middlewares modify the request, and indeed they are the "middleware" between the routers and services. You can easily add things like headers, authentication, path-prefix, or combine them and create reusable groups.

With these three concepts, we c identify the incoming request, define where the request is going, and choose how we want to modify the request as it is routed.

Getting Started with Traefik v2

Here are a few tips that I recommend while setting up Traefik 2.0 to hopefully make things a bit easier:

  • Keep It Simple: Sometimes it can be difficult to troubleshoot a problem when a lot of things are changed at once. Start with a basic example and access the Traefik dashboard first, then continue to add services from there.
  • Traefik Dashboard/API Auth Required: The Traefik dashboard must be protected by authentication, otherwise you will need to use the insecure flag. By following this guide we will have a secure configuration and will not need to use this flag.
  • Go Formatting: Traefik is written in Go, and therefore we need to use Go formatting depending on the input type (string, boolean, array). This means, for example, that your hostname must be defined with backticks, such as `traefik.example.com` (apostrophes will not work!).

That completes most of the basic information that you might need. Let's move on to setting up our Traefik Docker Compose stack.

Traefik 2 Configuration

Note: An updated version of this guide is available: Ultimate Traefik Docker Compose Guide [2024]: LE, SSL, Reverse Proxy

There are multiple ways to configure Traefik 2 and this can be quite overwhelming to newbies. Even with some experience, I find it difficult to wrap my head around Traefik 2 configuration methods at times.

So let's spend time understanding some important Traefik 2 configuration details: static vs dynamic configuration.

Static And Dynamic Configurations For Traefik V2
Traefik 2 Configuration Overview

Dynamic Configuration

Traefik is a dynamic reverse proxy, meaning it can add and remove routes automatically when containers start or stop. The labels attached to each container in the docker-compose-t2.yml, for example, are part of Traefikโ€™s dynamic configuration.

You could also route to additional hosts as part of your dynamic configuration if you defined them with the File provider (rules folder - more on this later).

Notice that we could define our dynamic configuration 1) with our Docker provider by using labels, and 2) with our File provider.

Static Configuration

While the dynamic methods are one of the main advantages of Traefik, itโ€™s important to understand that Traefik also uses a static configuration which is defined differently, and must be kept separate.

There are three different ways to define the static configuration for Traefik 2. Only one can be used at the same time.

  1. Configuration file - can be in a TOML or YAML file (eg. traefik.toml or traefik.yml).
  2. Command-line (CLI) arguments - these arguments are passed during docker run (this is what we will be using).
  3. As environment variables - here is a list of all environmental variables.

These ways are evaluated in the order listed above.

In this Docker Compose Traefik guide, we are going to take advantage of the CLI arguments method listed above. This gives us one less file to worry about and combines our Docker provider config into one docker-compose file.

Prep Work for Traefik 2

Next, letโ€™s do some prep work to get the Traefik v2 Docker container up and running. We are going to use the same folder structure that was used for our Docker media server guide.

Create Basic HTTP Authentication Credentials

Basic HTTP authentication provides a layer of security. You will have to provide a username and password before you can access the app that is behind Traefik. For this, we need to create a .htpasswd file with the login credentials.

We are going to put this file in $DOCKERDIR/shared folder. Use this HTPASSWD Generator, to create a username and password and add them to the $DOCKERDIR/shared/.htpasswd file as shown below:

username:mystrongpassword

Replace/Configure:

  1. username: with your HTTP username.
  2. mystrongpassword: with your hashed HTTP password generated using the link above.
Note that if you put the username and password in Docker compose file directly instead of in the environment file, then any $ signs in the hashed password must be escaped by adding another $ signed in front. For example:

user:$apr1$bvj3f2o0$/01DGlduxK4AqRsTwHnvc1

Should be:

user:$$apr1$$bvj3f2o0$$/01DGlduxK4AqRsTwHnvc1

In the environment file, $ signs do not need to be escaped.

Alternatively, you can use the following command on Linux to generate a username and password that is already escaped:

echo $(htpasswd -nb username mystrongpassword) | sed -e s/\$/\$\$/g

Save the file and exit.

Basic HTTP Authentication is a good start, but for a more secure authentication layer, consider switching to Google OAuth or Authelia later, which support multi-factor authentication.

Create Traefik 2 Environmental Variables

We already added several environmental variables for docker in the previous guide. We will add a few more on top of those.

Note: In my GitHub repo (which should be your main source of reference for docker-compose examples as it has the most up-to-date information), I use several domain names: DOMAINNAME_HOME_SYNOLOGY (for my Docker Home Server on Synology), DOMAINNAME_CLOUD_SERVER (for my Dedicated Server in a Datacenter, with Proxmox), DOMAINNAME_SHB (domain name for this website), and DOMAINNAME_KHUB (domain name of another non-WordPress website I host). You may find any of these domain variables in my examples. Make sure to substitute this variable with your own.

Open the same .env file and add the following new variables:

DOMAINNAME_CLOUD_SERVER=example.com
CLOUDFLARE_EMAIL=email@example.com
CLOUDFLARE_API_KEY=XXXXXXXXXXXX

Replace/Configure:

  1. example.com: Your private or dynamic DNS domain name.
  2. email@example.com: Email from your cloudflare account. This is only required only if you are doing DNS Challenge for Wildcard Traefik Letsencrypt certificates.
  3. XXXXXXXXXXXX: API key from your cloudflare account (Global API key from Profile page. Not the Zone ID or Account ID). Again, this only required for DNS Challenge for running apps under subdomains.
    Cloudflare Global Api Key For Dns Challenge
    Cloudflare Global Api Key For Let's Encrypt Dns Challenge

As explained before, in this guide we will be using the DNS Challenge method to make Traefik get wildcard certificates from LetsEncrypt. To do that with Cloudflare we need the above two variables set (CLOUDFLARE_EMAIL and CLOUDFLARE_API_KEY).

Let'S Encrypt Traefik Wildcard Dns Providers List
Let's Encrypt Traefik Wildcard Dns Providers List

If you use one of the other DNS providers instead of Cloudflare, make sure to include the required configuration parameters in the environmental variables file.

Define Trusted IPs

In order to keep our compose file visually simple, let us define a couple more variables. These are lists of IPs that Traefik can trust:

LOCAL_IPS=127.0.0.1/32,10.0.0.0/8,192.168.0.0/16,172.16.0.0/12
CLOUDFLARE_IPS=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22

These are pretty standard and do not require any customizations. But, I would just check and make sure that your local IP address (host network, docker network, t2_proxy, etc.) are covered in the ranges defined for LOCAL_IPS. The rnages defined will also cover the IPs set by ZeroTier or similar services for internal networks, if you decide to implement those later on.

Prepare Traefik 2 Folders and Files

Next, we need to create new folders for Traefik and ACME in the docker appdata folder ($DOCKERDIR/appdata) described previously:

mkdir traefik2
mkdir traefik2/acme
mkdir traefik2/rules
mkdir traefik2/rules/cloudserver

traefik2 is the folder we will use to store all Traefik 2.0 related configurations.

Note:

  1. Docker cannot create missing files (only directories).
  2. Since all my Docker hosts are synched through Syncthing, I keep host specific files in separate folders. In the above case, I put all rules related to my cloudserver in traefik2/rules/cloudserver. If you are not following the same approach or have only one host, then feel free to put all your Traefik rules in traefik2/rules instead of traefik2/rules/cloudserver. Or, feel free to rename folders as you please. However, you will have to ensure that you mount the right folder in Traefik volumes section below.

So we will need to create an empty file for Traefik to store our LetsEnrypt certificate. Create acme.json empty file inside appdata/traefik2/acme folder using the following command

touch acme.json

Next, set proper permission for acme.json file using the following command (from inside appdata/traefik2/acme):

chmod 600 acme.json

Without proper permissions, Traefik won't start. The acme.json file will store all the SSL certificates that are generated. In the later steps of this guide, you will be asked to open and check this file.

Similarly, we are going to create log files for Traefik to write logs to. I store all my logs in a centralized location ($DOCKERDIR/logs). From within the logs/cloudserver/traefik folder (create the folders if they do not exist), let us create empty log files:

touch traefik.log
touch access.log

Unlike my previous Traefik guide, here we are defining a separate Traefik log and Access log because I am setting the stage for implementing Crowdsec later on. CrowsdSec is an free and awesome intrusion prevention system that is a great alternative to Fail2ban. So watch out for this guide in the near future.

You may customize the location of the log files. But be sure to specify the right paths for Traefik volumes that will defined later.

Traefik 2 Docker Compose

All basic information has been presented and the prep work has been done. Let's start with what you have all been waiting for. Building the Traefik Docker Compose file.

Caution: Pay attention to blank spaces in the docker-compose examples below. Spacing is very important for YAML read your files correctly.

Create a file called docker-compose-t2.yml in the docker root folder we discussed earlier in this guide (same location as .env file, $DOCKERDIR). We will start out by adding the following to it:

version: "3.9"

It basically says we are going to use Docker Compose file format 3.9.

Define Networks for Traefik Docker Compose

Add the networks block.

Note: Any line with # in front is a comment and is ignored by Docker Compose.
########################### NETWORKS
# You may customize the network subnet (192.168.90.0/24) below as you please.
# Docker Compose version 3.5 or higher required to define networks this way.

networks:
  default:
    driver: bridge
  t2_proxy:
    name: t2_proxy
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.90.0/24

We are defining two networks (default and t2_proxy). All services behind Traefik 2 Proxy network will use IP addresses between 192.168.90.1 and 192.168.90.254.

In this basic guide, we will not be using Socket Proxy to protect the Docker socket. It is probably not needed for Docker homelab environment. But if you decide to expand and use Docker Socket Proxy later on, you will have to add a socket_proxy network as well.

Define Extension Fields

One of the main issues with docker-compose is that we will be reusing code bits multiple times. This makes the compose file long. My compose files are over 1300 lines long. To eliminate repetitions, I recently implemented Docker extension fields.

The downside of extension fields is that it reduces the readability of compose files (which can be confusing for beginners). So you will have to pay extra attention to the syntax.

Copy-paste the code block below after the network block.

########################### EXTENSION FIELDS
# Helps eliminate repetition of sections
# More Info on how to use this: https://github.com/htpcBeginner/docker-traefik/pull/228

# Common environment values
x-environment: &default-tz-puid-pgid
  TZ: $TZ
  PUID: $PUID
  PGID: $PGID

# Keys common to some of the core services that we always to automatically restart on failure
x-common-keys-core: &common-keys-core
  networks:
    - t2_proxy
  security_opt:
    - no-new-privileges:true
  restart: always

# Keys common to some of the dependent services/apps
x-common-keys-apps: &common-keys-apps
  networks:
    - t2_proxy
  security_opt:
    - no-new-privileges:true
  restart: unless-stopped

# Keys common to some of the services in media-services.txt
x-common-keys-media: &common-keys-media
  networks:
    - t2_proxy
  security_opt:
    - no-new-privileges:true
  restart: "no"

First, we are defining the variables $TZ, $PUID, and $PGID. So any service that requires these three variables as environmental variables (most LinuxServer.io images do) all we need to do is include <<: *default-tz-puid-pgid under environment section of the service's compose (you will see this in action later under Heimdall).

Next, we are defining the network (with a default network of t2_proxy) and security options to be used with docker containers.

Then we are adding restart policy. Some services (e.g. Traefik, Portainer) are core services and we want those to be restarted always, in case of failure or reboot. Some of them we do not want to restart when manually stopped by the user.

And, then some others no automatic restart at all upon failure or reboot. For example, my media containers that rely on rclone Google drive mounts are not started upon boot. Instead, I use bash scripts to check the required media mounts before starting them.

So for any service that requires always restart with standard network and security policies, all we need to do is to just include <<: *common-keys-core.

Note: It is possible to make exceptions or overrides by redefining the above within each docker service (you will see this in action later).

So as I said before, loss of readability but fewer lines of code.

Start Adding Docker Media Server Containers

Right below the extensions block, let us start adding our services/containers. First, begin by adding the following lines:

########################### SERVICES
services:
Note that ########################### SERVICES is ignored by Docker compose. With # in the front, this line is basically a comment and a visual demarkation within our long docker-compose.yml file.

Within services: we are going to start adding services to our traefik docker compose stack.

Note: Note that everything within services should indented by two blank spaces.

Frontend Services

I call these frontend services because they are in front of many of the apps in our traefik docker stack. Let us begin by adding the line below (don't ignore the 2 blank spaces in the front) under the services block.

  ############################# FRONTENDS

Add Docker Compose for Traefik v2

Hold on tight, this is going to be a long one. But don't worry we will do it in bits and pieces.

First, let's add the Traefik 2 service basics:

# Traefik 2 - Reverse Proxy
  traefik:
    <<: *common-keys-core # See EXTENSION FIELDS at the top
    container_name: traefik
    image: traefik:2.7

Nothing fancy here. We are using the common-keys-core docker extension, which should automatically set the network to t2_proxy and the restart policy to always. We are also specifying which version of Traefik to use (2.7).

In the past, certain Traefik versions included breaking changes, causing unexpected service outage. Therefore, I recommend you to use specific version tags instead of the latest image tag. Every few months, I test new versions and upgrade.

Next, we will add our CLI arguments to Docker Compose Traefik.

    command: # CLI arguments
      - --global.checkNewVersion=true
      - --global.sendAnonymousUsage=true
      - --entryPoints.http.address=:80
      - --entryPoints.https.address=:443
      # Allow these IPs to set the X-Forwarded-* headers - Cloudflare IPs: https://www.cloudflare.com/ips/
      - --entrypoints.https.forwardedHeaders.trustedIPs=$CLOUDFLARE_IPS,$LOCAL_IPS
      - --entryPoints.traefik.address=:8080
      - --api=true
      # - --api.insecure=true
      - --api.dashboard=true
      # - --serversTransport.insecureSkipVerify=true
      - --log=true
      - --log.filePath=/logs/traefik.log
      - --log.level=INFO # (Default: error) DEBUG, INFO, WARN, ERROR, FATAL, PANIC
      - --accessLog=true
      - --accessLog.filePath=/logs/access.log
      - --accessLog.bufferingSize=100 # Configuring a buffer of 100 lines
      - --accessLog.filters.statusCodes=204-299,400-499,500-599
      - --providers.docker=true
      - --providers.docker.endpoint=unix:///var/run/docker.sock # Use Docker Socket Proxy instead for improved security
      # - --providers.docker.endpoint=tcp://socket-proxy:2375 # Use this instead of the previous line if you have socket proxy.
      - --providers.docker.exposedByDefault=false
      - --entrypoints.https.http.tls.options=tls-opts@file
      # Add dns-cloudflare as default certresolver for all services. Also enables TLS and no need to specify on individual services
      - --entrypoints.https.http.tls.certresolver=dns-cloudflare
      - --entrypoints.https.http.tls.domains[0].main=$DOMAINNAME_CLOUD_SERVER
      - --entrypoints.https.http.tls.domains[0].sans=*.$DOMAINNAME_CLOUD_SERVER
      # - --entrypoints.https.http.tls.domains[1].main=$DOMAINNAME2 # Pulls main cert for second domain
      # - --entrypoints.https.http.tls.domains[1].sans=*.$DOMAINNAME2 # Pulls wildcard cert for second domain
      - --providers.docker.network=t2_proxy
      - --providers.docker.swarmMode=false
      - --providers.file.directory=/rules # Load dynamic configuration from one or more .toml or .yml files in a directory
      # - --providers.file.filename=/path/to/file # Load dynamic configuration from a file
      - --providers.file.watch=true # Only works on top level files in the rules folder
      - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory # LetsEncrypt Staging Server - uncomment when testing
      - --certificatesResolvers.dns-cloudflare.acme.email=$CLOUDFLARE_EMAIL
      - --certificatesResolvers.dns-cloudflare.acme.storage=/acme.json
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.delayBeforeCheck=90 # To delay DNS check and reduce LE hitrate

The above Traefik v2.0 CLI arguments are what goes into a traefik.toml (or yml) file if you decided to use one.

Here are some important lines that will require your attention:

--entrypoints.https.forwardedHeaders.trustedIPs

When using Cloudflare and all your traffic is behind their proxy (orange cloud in the DNS records), the request's origin IP is replaced with Cloudflare's IP (CLOUDFLARE_IPS). The result is that all of your services will know that a Cloudflare IP has connected, but you can't see the actual origin IP. This line tells Traefik to trust forwarded headers information (X-Forwarded-*) for the publicly available Cloudflare IPs, which means that Traefik will accept the X-Forwarded-For header set by Cloudflare.

This option is not needed if you are not using Cloudflare's proxy features (DNS entries are grey-clouded). The same applies for Local IP addresses (LOCAL_IPS--api.insecure=true

This line makes the API available through the insecure entrypoint (port 8080). Since we are going to use api@internal (shown later). We do not need to turn on insecure API.

--serversTransport.insecureSkipVerify=true

I recommend leaving this line disabled (commented out), which will default to "false". Certain apps that require HTTPS for their web UI can be finicky behind Traefik (eg. NextCloud, Unifi Controller). If you set insecureSkipVerify to true this disables SSL certificate verification. Doing so makes those finicky apps work but we want to avoid this if possible. We can use TCP routers that will allow us to access services like Nextcloud and Unifi Controller that require HTTPS and have their own certificate.

--log.level=DEBUG

Start out by setting this to DEBUG. Once you ensure everything is working fine and LetsEncrypt Certificates are being pulled correctly, you can change this to WARN.

--entrypoints.https.http.tls.options=tls-opts@file

For TLS connections, Traefik will use a few default options. But we can strengthen this a bit by defining some custom TLS options. Create a file called tls-opts.yml in appdata/traefik2/rules/cloudserver folder and add the following contents to it before you start Traefik.

If you customize the path and folder name in appdata/traefik2/rules/cloudserver, be sure to set the volumes for Traefik properly in the sections below.
tls:
  options:
    tls-opts:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
        - TLS_FALLBACK_SCSV # Client is doing version fallback. See RFC 7507
      curvePreferences:
        - CurveP521
        - CurveP384
      sniStrict: true

You can find an example of tls-opts.yml file in my Traefik Docker-Compose Github repo. Nothing to customize in this file.

--providers.file.directory=/rules

In this folder, we will store some configurations that Traefik 2 can pick up dynamically in real-time (if --providers.file.watch is set to true), without needing a restart. Examples include middlewares, configurations for proxying external or non-docker apps, etc. Some example rules and scenarios are discussed later in this Docker Traefik 2 tutorial.

--providers.file.filename=/path/to/file

This line is commented out because we are not going to use rules.toml (or yml). Instead, we are going to use a rules folder, which is enabled in the previous line of the configuration.

--certificatesResolvers.dns-cloudflare.acme Lines

Now we are going to talk about the last 5 CLI arguments:

      - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory # LetsEncrypt Staging Server - uncomment when testing
      - --certificatesResolvers.dns-cloudflare.acme.email=$CLOUDFLARE_EMAIL
      - --certificatesResolvers.dns-cloudflare.acme.storage=/acme.json
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53

This is where we define our certificate resolver(s). In this guide, we're using the ACME DNS challenge with Cloudflare as our provider, so I've chosen dns-cloudflare as the name for this cert resolver.

If you want Traefik to get wildcard Lets Encrypt SSL certificates (*.domain.com), then ACME DNS challenge is the only way to validate you domain.

If you are using another ACME challenge or a DNS verification provider other than Cloudflare you may want to name your cert resolver differently.

caServer: This is a very important line. When enabled, we will be using the LetsEncrypt staging server to check if everything is working fine. As mentioned previously, LetsEncrypt has a rate limit to prevent system abuse. In case you have errors in your Traefik 2 Docker Compose, you may be locked out of LetsEncrypt validation. To prevent this, we will use the staging server for the initial setup. Once we ensure everything is working well (shown later) we will comment out this line and have Traefik 2 get the real LetsEncrypt SSL certificates from the real server.

Nothing to change with email or storage. For Cloudflare, make sure your environmental variable ($CLOUDFLARE_EMAIL) is set. Other providers may have different requirements and may not require an email.

provider: Change if you use a provider other than Cloudflare for DNS challenge.

Networks for Traefik 2 Dashboard

Next comes the network block. You can either let Docker assign a dynamic IP for the traefik service or manually assign a static IP.

    networks:
      t2_proxy:
        ipv4_address: 192.168.90.254 # You can specify a static IP
    # networks:
    #  - t2_proxy

The example above is for a static IP on the Docker network of 192.168.90.254. This IP is only accessible by the host and on the Docker network. If you want Docker to assign the container IP dynamically, comment out the first three lines and uncomment the last two.

Setting a static IP is helpful for some services like databases (MariaDB, InfluxDB, etc.) or whenever one of your containers need to refer to another statically. For example, Radarr connecting to Transmission or Nzbget using their static Docker IP instead of exposing any ports to the host.

Traefik 2 Ports

Next, we are going to specify the port information in the Traefik Docker-Compose file. Traefik needs three ports: 80, 443, and 8080.

The last one is optional as we will put the Traefik dashboard behind its subdomain later (instead of accessing it through IP-ADDRESS:8080), using api@internal.

    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      # - target: 8080 # insecure api wont work
      #   published: 8080
      #   protocol: tcp
      #   mode: host

Traefik 2 Volumes

Here are the volumes that I have specified in my Traefik 2 docker-compose file:

    volumes:
      - $DOCKERDIR/appdata/traefik2/rules/cloudserver:/rules # file provider directory
      - /var/run/docker.sock:/var/run/docker.sock:ro # If you use Docker Socket Proxy, comment this line out
      - $DOCKERDIR/appdata/traefik2/acme/acme.json:/acme.json # cert location - you must create this empty file and change permissions to 600
      - $DOCKERDIR/logs/cloudserver/traefik:/logs # for fail2ban or crowdsec
      - $DOCKERDIR/shared:/shared

The rules volume contains the configuration for our File provider (eg. middlewares, rules for external/non-docker apps). More on these later in this Traefik docker-compose reverse proxy guide.

As explained previously, I use the shared folder to share some common information between various services. Remember that we also placed the Basic HTTP authentication credentials in this folder.

The rest of the volumes/files can be used as-is.

Traefik Environmental Variables

We are going to pass three additional environmental variables for Traefik 2 service to use:

    environment:
      - TZ=$TZ
      - CF_API_EMAIL=$CLOUDFLARE_EMAIL
      - CF_API_KEY=$CLOUDFLARE_API_KEY
      - DOMAINNAME_CLOUD_SERVER # Passing the domain name to the traefik container to be able to use the variable in rules. 
Note: In my GitHub Repo, these variables (and the .htpasswd) are set using Docker Secrets and not using .env file. So don't be shocked if this looks different from my repository. Environment variables shown here should still work. But if you are interested in proving security, check out Docker Secrets.

Both $CLOUDFLARE_EMAIL and $CLOUDFLARE_API_KEY, which were set previously set using .env file will be passed on to CF_API_EMAIL and CF_API_KEY, respectively, for DNS challenge verification.

DOMAINNAME_CLOUD_SERVER is not needed now but we are adding it to simplify adding non-docker apps behind Traefik reverse proxy (shown later). This will pass the domain name to the traefik container to be able to use the variable in the rules.

Traefik 2 Docker Labels

The last one is a big one: Traefik docker-compose labels.

The first is the line to enable or disable traefik for services. Quite simple.

    labels:
      - "traefik.enable=true"

When the container starts a route will automatically be created. This is necessary because weโ€™ve specified exposedByDefault=false as part of our static configuration.

Next, we add the routers to redirect all HTTP traefik to the secure HTTPS port:

      # HTTP-to-HTTPS Redirect
      - "traefik.http.routers.http-catchall.entrypoints=http"
      - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"

Then, we add additional routers for entrypoints, certificate resolving, and domain information:

      # HTTP Routers
      - "traefik.http.routers.traefik-rtr.entrypoints=https"
      - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME_CLOUD_SERVER`)"
      - "traefik.http.routers.traefik-rtr.tls=true" # Some people had 404s without this
      - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-cloudflare" # Comment out this line after first run of traefik to force the use of wildcard certs
      - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME_CLOUD_SERVER"
      - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME_CLOUD_SERVER"
      # - "traefik.http.routers.traefik-rtr.tls.domains[1].main=$DOMAINNAME2" # Pulls main cert for second domain
      # - "traefik.http.routers.traefik-rtr.tls.domains[1].sans=*.$DOMAINNAME2" # Pulls wildcard cert for second domain

By default, Traefik will listen for incoming requests on all available entrypoints. You can limit or specify an entrypoint if youโ€™d like to do so. In the above case, Traefik will listen on only HTTPS (secure entrypoint).

A rule is how we define which requests this router will apply to. The majority of my containers use the Host(`FQDN`) rule, but you could use regex, pathprefix or other options as well.

The rule has to follow Go formatting, which means that we need to use backticks around string values, not apostrophes!

With tls, we are explicitly stating that this router should connect via TLS, and should not accept HTTP connections.

Take a note of the certresolver label. After initial testing and first Traefik run to pull LetsEncrypt wildcard certificates, we will have to comment out this line to force usage of wildcard certificates and stop creating separate certificates for individual services.

Iโ€™ve chosen arbitrary names to help describe their function, for example, Iโ€™ve chosen dns-cloudflare because Iโ€™m using the DNS challenge and Cloudflare is my provider. The name can be changed to anything, but it must be the same as the certresolver we defined in our CLI arguments.

You'll also notice I'm using different names for each router, for example, the HTTP-to-HTTPS Redirect router is arbitrarily named http-catchall and the HTTP Router is traefik-rtr. Routers are grouped using these names, but the name can be changed as you like to describe the router.

In the example above, we are using only one domain ($DOMAINNAME_CLOUD_SERVER and its wildcard *.$DOMAINNAME_CLOUD_SERVER). To define additional domains, uncomment the last two lines. Make sure $DOMAINNAME2 is set in your .env file. You can specify additional domains using domains[2], domains[3], etc. Traefik will pull SSL certificates for all of these domains.

Next, we will define where to find the service to be proxied. With the insecure API disabled, we tell Traefik to find the API internally in the container using the following line.

      ## Services - API
      - "traefik.http.routers.traefik-rtr.service=api@internal"

The Traefik dashboard/API is a special case and should be defined exactly as api@internal. We will do this differently for the rest of the services in this guide.

We are not done yet, we still need to add authentication for the Traefik dashboard through middlewares.

Traefik 2 Basic HTTP Authentication - Middleware

The last part of the labels is a big one: middlewares. This will specify the security headers, set authentication, etc. First, let's start simple, add a basic authentication, and then add additional labels.

Note: In the previous version of this Traefik Docker Compose tutorial, I used TOML. However, I have since switched to YAML (or YML), which is much more cleaner and simpler. In my GitHub repo, you will find both TOML and YAML examples. But note that only the YAML files are continually tested and updated.

Basic HTTP Authentication is the simplest form of authentication you can put your services behind. Let's create our first middleware using our File provider. Create a file named middlewares.yml inside the Traefik 2 rules folder ($DOCKERDIR/appdata/traefik2/rules/cloudserver) and add the following content to it:

http:
  middlewares:
    middlewares-basic-auth:
      basicAuth:
        # users:
        #   - "user:$apr1$bvj3f2o0$/01DGlduxK4AqRsTwHnvc1"
        usersFile: "/shared/.htpasswd" # be sure to mount the volume through docker-compose.yml
        realm: "Traefik 2 Basic Auth"

The above code block adds a basic authentication middleware. We can specify users for authentication right here if you wish (the commented-out lines).

But as discussed previously, we are going to use .htpasswd file to store our credentials. So we will specify the path to usersFile.

Now let us add this middleware to our Traefik Docker Compose service. This requires defining the middlewares we want to use on our router. At the end of the Docker Compose for Traefik (right after the api@internal line we added previously), add the following:

      ## Middlewares
      - "traefik.http.routers.traefik-rtr.middlewares=middlewares-basic-auth@file" 

We are basically asking Traefik to look for the middlewares-basic-auth middleware that we defined with our File provider (@file) in the rules folder.

Using the @ symbol to specify the provider is a new feature in Traefik v2, but it is required to identify the source of the middleware. If no provider is specified, then Traefik assumes the source of the middleware is the current provider (i.e. @docker).

Note that the underlined part here middlewares-basic-auth@file matches the name specified in the middlewares.yml (line 3) file shown above.

Testing Docker Traefik 2 Setup

At this point, let's save the file and start Traefik 2. From the docker root folder, run:

sudo docker-compose -f docker-compose-t2.yml up -d

Alternatively, you can use either dcup2 or dcrec2 traefik bash_aliases shortcuts to start Traefik 2.

Immediately, let's start following the logs for the Traefik service to look for any obvious errors. Use the following command or the shortcut dclogs2 traefik. Again, from the docker root folder run:

sudo docker logs -tf --tail="50" traefik

Here are some messages that show that everything went well:

  • Waiting for DNS record propagation
  • The server validated our request
  • Validations succeeded, requesting certificates
  • Server responded with a certificate

At this point, if you see any "bad certificate" or "unknown certificate" messages, ignore them. Remember that we are using the LetsEncrypt staging server, which does not provide valid certificates yet.

There are multiple ways to check if Traefik 2 is configured correctly and succeeded in obtaining the LetsEncrypt certificate. We'll show you two of them here.

Let's open Traefik 2 dashboard in a browser and see the certificate information, as shown below. Notice the Fake LE Intermediate X1. This shows that an SSL (fake) certificate was obtained by Traefik 2 from the LetsEncrypt staging server.

Traefik 2.0 Successful Staging Shows Fake Le Intermediate Certificate
Traefik 2.0 Successful Staging Shows Fake Le Intermediate Certificate

Alternatively, you can open acme.json file located inside the traefik2 folder ($DOCKERDIR/appdata/traefik2/acme/acme.json) and look for signs of successful validation, as shown below.

Acme.json File For Staged Traefik 2.0 Letsencrypt Domain Validation
Acme.json File For Staged Traefik 2.0 Letsencrypt Domain Validation

If you have been successful so far, then congratulations. You are ready to get real now.

Note: If you orange-cloud the DNS records on Cloudflare (i.e. Cloudflare proxy enabled) then you will not see the LetsEncrypt certificate when it is pulled. Since your service is behind Cloudflare proxy, you will see Cloudflare's SSL certificate. Therefore, during inital testing and setup I recommend leaving the proxy off (gray-cloud). After ensuring that proper LetsEncrypt certificates are pulled, you can enable Cloudflare proxy, following which you will only see Cloudflare's SSL certificates.

Fetching Real LetsEncrypt Wildcard Certificates using Traefik

Since our staging was successful, let us now open the docker-compose-t2.yml file and comment out the following line (as shown below) so that we can reach the real LetsEncrypt server for the DNS challenge.

     # - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory 

Save the Traefik docker-compose file.

Open the acme.json file, delete all contents, and save it. Alternatively, you may delete acme.json file and recreate an empty file (remember to set the right permissions as described previously).

Next, recreate Traefik (dcrec2 traefik or the full command listed previously), and follow the logs (dclogs2 traefik or the full command listed previously) once again to make sure everything goes smoothly.

Dns Challenge Traefik 2 Logs Showing Successful Certificate Retrieval From Letsencrypt
Dns Challenge Traefik 2 Logs Showing Successful Certificate Retrieval From Letsencrypt

The logs look good without any errors. Now let us check the certificates in the browser to verify the SSL certificate.

Final Letsencrypt Ssl Certificate Using Traefik Reverse Proxy
Final Letsencrypt Ssl Certificate Using Traefik Reverse Proxy

Looks good. Notice that the certificate includes both example.com and the wildcard *.example.com. Let us also check the acme.json file.

Acme.json File For Final Traefik 2.0 Letsencrypt Domain Validation
Acme.json File For Final Traefik 2.0 Letsencrypt Domain Validation

Notice that it does not say "staging" anymore. So all is good so far and our Docker Compose Traefik stack is coming along quite well.

Forcing the Use of Wildcard Certs

Now that the wildcard certificates have been pulled, let us force Traefik to use that (*.example.com certificate) instead of creating a separate certificate for each service (service.example.com).

To do this, comment out the certresolver in the docker-compose file as shown below.

     # - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-cloudflare"
Note: There is nothing wrong in keeping the above line enabled. Technically, it is not needed. However, some people have seen 404 errors without it.

Recreate your traefik 2.0 service and check to make sure the dashboard is accessible.

Be the 1 in 200,000. Help us sustain what we do.
25 / 150 by Dec 31, 2024
Join Us (starting from just $1.67/month)

Securing Traefik 2 Dashboard

Now that everything is working great, let us start improving the security of our services. We already added some security with the basic authentication middleware. [Read: Crowdsec Docker Compose Guide Part 1: Powerful IPS with Firewall Bouncer]

Here are a few more you can add.

Rate Limit

The rate limit middleware ensures that services will receive a fair number of requests. This is helpful if intentionally (eg. security breach) or unintentionally your services are being bombarded with requests causing a denial of service (DDoS) situation.

We are going to open the middlewares.yml file we created above and add the rate limit middleware just as we did for basic authentication. Add the following lines below what we already added for basic authentication (pay attention to the spacing/formatting).

    middlewares-rate-limit:
      rateLimit:
        average: 100
        burst: 50

Now we can add this middleware to Traefik 2 dashboard the same way we added the basic authentication middleware - by modifying the middlewares label in docker compose as follows:

      - "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-basic-auth@file" 

Note that I added rate-limiting as the first line of defense.

Redirect to HTTPS

We can also make Traefik auto-redirect all HTTP service requests to HTTPS, instead of throwing an error. To do this, let us re-open middlewares.yml and add the following middleware right below what we added previously:

    middlewares-https-redirectscheme:
      redirectScheme:
        scheme: https
        permanent: true

To add this to our Traefik or any service, we will have to modify the middleware line as follows:

      - "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-https-redirectscheme@file,middlewares-basic-auth@file" 

Security Headers

In our Traefik 1 docker-compose, we had several browser security headers. Let us start adding those to our Traefik 2 services. You can add them as labels or as middleware using the File provider.

Using the File provider for middlewares significantly reduces the size of docker-compose files by avoiding repetitions and reusing code.

Security Headers as Middlewares File

Explaining what these security headers do is outside the scope of this post, which is already turning out to be one of the longest posts I have ever written. You can read more about traefik security headers here. What is provided here should work for most of you and most services.

Open middlewares.yml again and add the following lines below what we've already added:

    middlewares-secure-headers:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        customFrameOptionsValue: "allow-from https:{{env "DOMAINNAME_CLOUD_SERVER"}}" #CSP takes care of this but may be needed for organizr.
        contentTypeNosniff: true
        browserXssFilter: true
        # sslForceHost: true # add sslHost to all of the services
        # sslHost: "{{env "DOMAINNAME_CLOUD_SERVER"}}"
        referrerPolicy: "same-origin"
        permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()"
        customResponseHeaders:
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex,"
          server: ""
          # https://community.traefik.io/t/how-to-make-websockets-work-with-traefik-2-0-setting-up-rancher/1732
          # X-Forwarded-Proto: "https"

While adding security headers via a middleware file simplifies the Traefik docker-compose file, it does come with limitations compared to using labels. Notice that the lines sslForceHost and sslHost have been commented out.

These two options add a little bit more security but unfortunately will break apps. This is because the sslHost option requires a specific hostname (eg. service.example.com) and therefore one cannot provide a universal host that will apply to all services that will use Traefik 2.

Note: You cannot add only sslForceHost and sslForceHost as labels in docker-compose and leave the rest in the middleware file.

What is provided as labels will completely overwrite what is provided in the middlewares file. At this point, it looks like Traefik 2 does not append the two. So the only options are to either exclude those two lines (very slight decrease in security for convenience) or specify all security headers in the docker-compose files as labels (long docker-compose files). I chose to exclude (comment-out) those two lines in the middlewares.yml file.

As with other middlewares provided in the file, we now have to include this middleware in the compose file by modifying the line as follows:

      - "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-https-redirectscheme@file,middlewares-secure-headers@file,middlewares-basic-auth@file" 

As always, recreate the services after any changes to the docker-compose file.

Compression

Lastly, we are going to add one more middleware to compress the output to reduce bandwidth and increase speed. Once, again open middlewares.yml file add the following middleware at the end:

    middlewares-compress:
      compress: {}

Next, let us enable this middleware by adding it to our middleware definition in compose:

      - "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-https-redirectscheme@file,middlewares-secure-headers@file,middlewares-basic-auth@file,middlewares-compress@file" 

That's it. All of our middlewares are now added.

Now would be a good point to recreate Docker Compose Traefik service and make sure that the logs have no errors and the dashboard is accessible.

Middleware Chains

There is a simpler way to provide middlewares than specifying each of them individually. We can create what is known as "middleware chains". A Chain is simply a group of middlewares.

Create a file called middlewares-chains.yml in the rules folder and add the following lines:

http:
  middlewares:
    chain-no-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-https-redirectscheme
          - middlewares-secure-headers
          - middlewares-compress

    chain-basic-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-https-redirectscheme
          - middlewares-secure-headers
          - middlewares-basic-auth
          - middlewares-compress
My Github includes one additional middleware for CrowdSec. That is discussed in my CrowdSec guide.

What this code block does is that it creates two chains:

  1. chain-no-auth: This specifies the chain to use for services that we do not want to have an authentication layer in front (eg. Plex as it can interfere with Plex access on client devices). For these, we are only specifying middleware for rate limit, HTTPS redirection, security headers, and compression.
  2. chain-basic-auth: For services that will use basic authentication in front of the service, we are specifying middlewares-basic-auth with rate limit, HTTPS redirection, security headers, basic HTTP authentication, and compression.
Note that all the middlewares would only apply when the service is accessed through the FQDN (fully qualified domain name; e.g. https://traefik.example.com).

With these two middleware chains defined, now we can modify the middlewares label in Traefik docker compose as follows:

      - "traefik.http.routers.traefik-rtr.middlewares=chain-basic-auth@file" 

Instead of the long:

      - "traefik.http.routers.traefik-rtr.middlewares=middlewares-rate-limit@file,middlewares-https-redirectscheme@file,middlewares-secure-headers@file,middlewares-basic-auth@file,middlewares-compress@file" 

As evident, middleware chains further simplify docker-compose files. You can call these chains on any container and they will use the same middleware configuration.

Full Traefik Docker-Compose Example

Note: An updated version of this guide is available: Ultimate Traefik Docker Compose Guide [2024]: LE, SSL, Reverse Proxy

Now that we have done all of the customizations to the Docker-compose Traefik service, you should have the full compose file already and it may look like what is shown below.

version: "3.9"

########################### NETWORKS
# You may customize the network subnet (192.168.90.0/24) below as you please.
# Docker Compose version 3.5 or higher required to define networks this way.

networks:
  default:
    driver: bridge
  t2_proxy:
    name: t2_proxy
    driver: bridge
    ipam:
      config:
        - subnet: 192.168.90.0/24

########################### EXTENSION FIELDS
# Helps eliminate repetition of sections
# More Info on how to use this: https://github.com/htpcBeginner/docker-traefik/pull/228

# Common environment values
x-environment: &default-tz-puid-pgid
  TZ: $TZ
  PUID: $PUID
  PGID: $PGID

# Keys common to some of the core services that we always to automatically restart on failure
x-common-keys-core: &common-keys-core
  networks:
    - t2_proxy
  security_opt:
    - no-new-privileges:true
  restart: always

# Keys common to some of the dependent services/apps
x-common-keys-apps: &common-keys-apps
  networks:
    - t2_proxy
  security_opt:
    - no-new-privileges:true
  restart: unless-stopped

# Keys common to some of the services in media-services.txt
x-common-keys-media: &common-keys-media
  networks:
    - t2_proxy
  security_opt:
    - no-new-privileges:true
  restart: "no"

########################### SERVICES
services:
  ############################# FRONTENDS
  # Traefik 2 - Reverse Proxy
  traefik:
    <<: *common-keys-core # See EXTENSION FIELDS at the top
    container_name: traefik
    image: traefik:2.7
    command: # CLI arguments
      - --global.checkNewVersion=true
      - --global.sendAnonymousUsage=true
      - --entryPoints.http.address=:80
      - --entryPoints.https.address=:443
      # Allow these IPs to set the X-Forwarded-* headers - Cloudflare IPs: https://www.cloudflare.com/ips/
      - --entrypoints.https.forwardedHeaders.trustedIPs=173.245.48.0/20,103.21.244.0/22,103.22.200.0/22,103.31.4.0/22,141.101.64.0/18,108.162.192.0/18,190.93.240.0/20,188.114.96.0/20,197.234.240.0/22,198.41.128.0/17,162.158.0.0/15,104.16.0.0/13,104.24.0.0/14,172.64.0.0/13,131.0.72.0/22
      - --entryPoints.traefik.address=:8080
      - --api=true
      # - --api.insecure=true
      - --api.dashboard=true
      # - --serversTransport.insecureSkipVerify=true
      - --log=true
      - --log.filePath=/logs/traefik.log
      - --log.level=INFO # (Default: error) DEBUG, INFO, WARN, ERROR, FATAL, PANIC
      - --accessLog=true
      - --accessLog.filePath=/logs/access.log
      - --accessLog.bufferingSize=100 # Configuring a buffer of 100 lines
      - --accessLog.filters.statusCodes=204-299,400-499,500-599
      - --providers.docker=true
      - --providers.docker.endpoint=unix:///var/run/docker.sock # Use Docker Socket Proxy instead for improved security
      # - --providers.docker.endpoint=tcp://socket-proxy:2375 # Use this instead of the previous line if you have socket proxy.
      - --providers.docker.exposedByDefault=false
      - --entrypoints.https.http.tls.options=tls-opts@file
      # Add dns-cloudflare as default certresolver for all services. Also enables TLS and no need to specify on individual services
      - --entrypoints.https.http.tls.certresolver=dns-cloudflare
      - --entrypoints.https.http.tls.domains[0].main=$DOMAINNAME_CLOUD_SERVER
      - --entrypoints.https.http.tls.domains[0].sans=*.$DOMAINNAME_CLOUD_SERVER
      # - --entrypoints.https.http.tls.domains[1].main=$DOMAINNAME2 # Pulls main cert for second domain
      # - --entrypoints.https.http.tls.domains[1].sans=*.$DOMAINNAME2 # Pulls wildcard cert for second domain
      - --providers.docker.network=t2_proxy
      - --providers.docker.swarmMode=false
      - --providers.file.directory=/rules # Load dynamic configuration from one or more .toml or .yml files in a directory
      # - --providers.file.filename=/path/to/file # Load dynamic configuration from a file
      - --providers.file.watch=true # Only works on top level files in the rules folder
      - --certificatesResolvers.dns-cloudflare.acme.caServer=https://acme-staging-v02.api.letsencrypt.org/directory # LetsEncrypt Staging Server - uncomment when testing
      - --certificatesResolvers.dns-cloudflare.acme.email=$CLOUDFLARE_EMAIL
      - --certificatesResolvers.dns-cloudflare.acme.storage=/acme.json
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.provider=cloudflare
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.resolvers=1.1.1.1:53,1.0.0.1:53
      - --certificatesResolvers.dns-cloudflare.acme.dnsChallenge.delayBeforeCheck=90 # To delay DNS check and reduce LE hitrate
    networks:
      t2_proxy:
        ipv4_address: 192.168.90.254 # You can specify a static IP
    # networks:
    #  - t2_proxy
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      # - target: 8080 # insecure api wont work
      #   published: 8080
      #   protocol: tcp
      #   mode: host
    environment:
      - CF_API_EMAIL=$CLOUDFLARE_EMAIL
      - CF_API_KEY=$CLOUDFLARE_API_KEY
      - DOMAINNAME_CLOUD_SERVER # Passing the domain name to the traefik container to be able to use the variable in rules. 
    volumes:
      - $DOCKERDIR/appdata/traefik2/rules/cloudserver:/rules # file provider directory
      - /var/run/docker.sock:/var/run/docker.sock:ro # If you use Docker Socket Proxy, comment this line out
      - $DOCKERDIR/appdata/traefik2/acme/acme.json:/acme.json # cert location - you must create this empty file and change permissions to 600
      - $DOCKERDIR/logs/cloudserver/traefik:/logs # for fail2ban or crowdsec
      - $DOCKERDIR/shared:/shared
    labels:
      - "traefik.enable=true"
      # HTTP-to-HTTPS Redirect
      - "traefik.http.routers.http-catchall.entrypoints=http"
      - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      # HTTP Routers
      - "traefik.http.routers.traefik-rtr.entrypoints=https"
      - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME_CLOUD_SERVER`)"
      - "traefik.http.routers.traefik-rtr.tls=true" # Some people had 404s without this
      - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-cloudflare" # Comment out this line after first run of traefik to force the use of wildcard certs
      - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME_CLOUD_SERVER"
      - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME_CLOUD_SERVER"
      # - "traefik.http.routers.traefik-rtr.tls.domains[1].main=$DOMAINNAME2" # Pulls main cert for second domain
      # - "traefik.http.routers.traefik-rtr.tls.domains[1].sans=*.$DOMAINNAME2" # Pulls wildcard cert for second domain
      ## Services - API
      - "traefik.http.routers.traefik-rtr.service=api@internal"
      ## Middlewares
      - "traefik.http.routers.traefik-rtr.middlewares=chain-basic-auth@file" 
Always refer to my Github Repo for the latest examples. Note that you will find some differences from what you see here and on my GitHub. This is because we still have not described some advanced features (e.g. Docker secrets, socket proxy, etc.) in this guide to keep it simple. Check the troubleshooting section below if you run into trouble.

Adding Apps to Traefik Docker Compose Stack

Now that our Traefik 2 and Basic Authentication are up and running, let us start adding some apps. Earlier, I listed examples of some of the apps that I wanted my Traefik 2 Docker server to run.

We are not going to show you how to install all of these apps using docker and put them behind Traefik 2 reverse proxy. Instead, we will show you a few apps that highlight a specific kind of configuration.

Once you read and understand this guide, it should be quite simple to use the docker-compose snippets from my GitHub Repo to set up the other apps that you are interested in.

Portainer with Traefik 2 and OAuth

We have covered Portainer installation previously, as well as in our docker media server guide. Portainer provides a WebUI to manage all your docker containers. I strongly recommend this for newbies.

Here is the Portainer docker-compose example to copy-paste into your compose file (pay attention to blank spaces at the beginning of each line):

  # Portainer - WebUI for Containers
  portainer:
    <<: *common-keys-core # See EXTENSION FIELDS at the top
    container_name: portainer
    image: portainer/portainer-ce:latest
    command: -H unix:///var/run/docker.sock # Use Docker Socket Proxy and comment this line out, for improved security.
    # command: -H tcp://socket-proxy:2375 # Use this instead, if you have Socket Proxy enabled.
    networks:
      - npm_proxy
    # ports: # Commented out because we are going to use Traefik to access portainer WebUI.
    #  - "$PORTAINER_PORT:9000"
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro # Use Docker Socket Proxy and comment this line out, for improved security.
      - $DOCKERDIR/appdata/portainer/data:/data # Change to local directory if you want to save/transfer config locally.
    environment:
      - TZ=$TZ
    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.portainer-rtr.entrypoints=https"
      - "traefik.http.routers.portainer-rtr.rule=Host(`portainer.$DOMAINNAME_CLOUD_SERVER`)"
      - "traefik.http.routers.portainer-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.portainer-rtr.middlewares=chain-basic-auth@file"
      # - "traefik.http.routers.portainer-rtr.middlewares=chain-no-auth@file"
      ## HTTP Services
      - "traefik.http.routers.portainer-rtr.service=portainer-svc"
      - "traefik.http.services.portainer-svc.loadbalancer.server.port=9000"

Configure:

  1. PORTAINER_PORT: Port number on which you want the portainer WebUI to be available at. It could be the same port as the container: 9000 (must be free). Set PORTAINER_PORT in your .env file. Specifying ports is optional since we are using reverse proxy and can reach portainer at portainer.example.com.
  2. Authentication: Basic authentication middleware chain is enabled. If you want no authentication, uncomment the corresponding line and comment out the other middlewares.

Once done, use the docker-compose up command listed above or the shortcut dcup2 if you have bash_aliases setup.

Portainer WebUI should be available at https://portainer.example.com.

Portainer With Traefik 2 Letsencrypt Wildcard Ssl Certificate
Portainer With Traefik 2 Letsencrypt Wildcard Ssl Certificate

Traefik 2 seems to be using the correct SSL certificates.

Heimdall - Application Dashboard

Once your stack grows, you may probably want a dashboard to easily access all your apps. In my original Docker guide, I had Organizr. Overtime Organizr became too clunky and so I replaced it with Heimdall.

Here is the Heimdall docker-compose example to copy-paste into your compose file (pay attention to blank spaces at the beginning of each line):

  # Heimdall - Application Dashboard
  heimdall:
    <<: *common-keys-core # See EXTENSION FIELDS at the top
    image: lscr.io/linuxserver/heimdall
    container_name: heimdall
    # ports:
      # - "$HEIMDALL_HTTP_PORT:80" # 80 used by Traefik
      # - "$HEIMDALL_HTTPS_PORT:443" # 443 used by Traefik. Disabled because we will put Heimdall behind proxy.
    volumes:
      - $DOCKERDIR/appdata/heimdall:/config
    environment:
      <<: *default-tz-puid-pgid
    labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.heimdall-rtr.entrypoints=https"
      - "traefik.http.routers.heimdall-rtr.rule=Host(`$DOMAINNAME_CLOUD_SERVER`,`www.$DOMAINNAME_CLOUD_SERVER`)"
      - "traefik.http.routers.heimdall-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.heimdall-rtr.middlewares=chain-basic-auth@file"
      ## HTTP Services
      - "traefik.http.routers.heimdall-rtr.service=heimdall-svc"
      - "traefik.http.services.heimdall-svc.loadbalancer.server.port=80"

Enable and customize the ports and environment sections as needed.

Note the usage of <<: *default-tz-puid-pgid docker extension to pass $TZ, $PUID, and $PGID environmental variables. Save and update your stack (shortcut dcup2 with my bash_aliases example).

Why did I include Heimdall as an Example?

Notice the following line:

      - "traefik.http.routers.heimdall-rtr.rule=Host(`$DOMAINNAME_CLOUD_SERVER`,`www.$DOMAINNAME_CLOUD_SERVER`)"

We are setting up Heimdall to be served on my root domain (example.com or www.example.com). So that will be the face of my domain. From there, we have links to all my apps.

Guacamole - HTML 5 based Remote desktop, SSH, on Telnet Gateway

Apache Guacamole is a clientless remote desktop gateway. It supports standard protocols like VNC, RDP, and SSH. It needs no plugins or software and works from any modern browser. In my opinion, it is a must-have for admins. [Read: Install Guacamole on Docker โ€“ VNC, SSH, SFTP, and RDP like a Boss!]

It is not easy to set up Guacamole as an app on host systems. But docker has made this very easy. We have published a detailed Guacamole Docker Compose guide. But I will include just a few labels here as it is a special case example.

....
      ## Middlewares
      - "traefik.http.routers.guacamole-rtr.middlewares=chain-oauth@file,add-guacamole" 
      - "traefik.http.middlewares.add-guacamole.addPrefix.prefix=/guacamole"
...

Why did I include Guacamole as an Example?

The main reason I include Guacamole as an example is to showcase the addPrefix.prefix middleware. If I visit guac.example.com, it will take me to the page shown below, which is a generic page with some information and not the Guacamole login page I want to reach.

Guacamole Without Addprefix Traefik Middleware
Guacamole Without Addprefix Traefik Middleware

The login page is available at guac.example.com/guacamole. Instead of typing /guacamole manually I can define as addPrefix.prefix middleware:

      - "traefik.http.middlewares.add-guacamole.addPrefix.prefix=/guacamole"

Then you can activate this middleware by adding add-guacamole to the middlewares label as shown in the Guacamole docker-compose example above. This causes guac.example.com/guacamole to be served through guac.example.com.

Another app that benefits from the addPrefix.prefix middleware is PiHole (see my GithHub). However, it does not work for all apps. For example, this does not work for ZoneMinder, which needs /zm/ prefix. [Read: ZoneMinder Docker Guide for Beginners: Best Free Video Surveillance]

Be the 1 in 200,000. Help us sustain what we do.
25 / 150 by Dec 31, 2024
Join Us (starting from just $1.67/month)

NextCloud โ€“ Your Own Cloud Storage

NextCloud (like OwnCloud) offers your own cloud storage to manage files and documents (like Google Drive/Docs). Nextcloud Docker compose setup has already been described in detail. So we won't go into the details.

If you choose to not enable the --serversTransport.insecureSkipVerify=true CLI argument (comment it out) you will need a slightly different setup. Nextcloudโ€™s WebUI is only accessible using an HTTPS port, and while Traefik communicates externally to clients using the LetsEncrypt cert, it communicates to services on the back-end using HTTP.

In the past, I needed to use the InsecureSkipVerify option, but we want to keep our reverse proxy secure, so letโ€™s find another way.

All of the configurations that weโ€™ve worked with so far have been for Traefikโ€™s โ€˜HTTPโ€™ routers, but Traefik v2 also offers the option to configure TCP routers. Traefikโ€™s TCP service has a pass-through option that will allow us to "pass-through" our secure connection to Nextcloud.

There are other similar apps (e.g. Unifi Controller) that will also need TCP routers to work behind Traefik.

Other Apps

There are several more apps in our Docker Traefik 2 server setup. The above examples show most of the possibilities.

For docker-compose examples for over 50 apps, check my current GitHub Repo. The repo includes apps such as Radarr, Sonarr, Lidarr, SABnzbd, and more.

Usenet is Better Than Torrents:
For apps like Sonarr, Radarr, SickRage, and CouchPotato, Usenet is better than Torrents. Unlimited plans from Newshosting (US Servers), Eweka (EU Servers), or UsenetServer, which offer >3000 days retention, SSL for privacy, and VPN for anonymity, are better for HD content.

Adding non-docker or external apps behind Traefik

We have Traefik docker compose working well for all of our services within our Docker network, but what about connecting to other hosts outside of Docker? For example, Home Assistant, PiHole on a separate Raspberry pi, etc. Traefik can also reverse proxy them and it is easy to implement.

Traefikโ€™s File provider allows us to add dynamic routers, middlewares, and services. Earlier we only used our rules directory to add middlewares, but we can easily add an external host by adding a new file to this directory. Traefik will auto-detect and update its configurations.

For this example, we will create a new route to an external PiHole running on a Raspberry Pi. Let us create a file called app-pihole.yml in the rules folder and add the following content to it.

http:
  routers:
    pihole-rpi-rtr:
      rule: "Host(`pihole.{{env "DOMAINNAME_CLOUD_SERVER"}}`)" 
      entryPoints:
        - https
      middlewares:
        - chain-basic-auth
        - pihole-rpi-add-admin
      service: pihole-rpi-svc
      tls:
        certResolver: dns-cloudflare
  middlewares:
    pihole-rpi-add-admin:
      addPrefix: 
        prefix: "/admin"
  services:
    pihole-rpi-svc:
      loadBalancer:
        servers:
          - url: "http://192.168.1.126"  # or whatever your external host's IP:port is 

Here is an explanation of the above file provider:

Restricted Content

Additional explanations and bonus content are available exclusively for the following members only:

Silver - Monthly, Silver - Yearly, Gold - Monthly, Gold - Yearly, Diamond - Monthly, Diamond - Yearly, and Platinum Lifetime (All-Inclusive)

Please support us by becoming a member to unlock the content.
Join Now

Note: For other apps that do not require additional middlewares lik PiHole, just delete pihole-rpi-add-admin under routers->middlewares. In addition, also delete the whole middlewares: block. You an find examples of other apps in my Github Repo.

Troubleshooting

Here are some issues either I ran into or others have mentioned. If none of the solutions listed below works, then feel free to join our Discord Server and ask for help.

Traefik 2 Not Pulling SSL Certificates

One of the most common reasons for this is that DNS changes have not propagated yet. Depending on the registrar, this can take several minutes.

If your staging was successful and fetching real certificates fail, wait a few more minutes and try again.

If after that Traefik 2 continues to fail to fetch certificates for some of the services, try adding the following label to each of the services.

      - "traefik.http.routers.jdownloader-rtr.tls.certresolver=dns-cloudflare"

Remember that we added this certresolver router to our Traefik 2 service and commented it out once wildcard certificates were pulled. We did not add this to any other service. Adding this seems to resolve the problem for some.

The downside to adding the certresolver router label to each service is that Traefik will fetch separate certificates for each subdomain instead of using the wildcard certificate.

Typos and Misnaming

Typos and misnaming are two of the most common mistakes people do. For example, when I was creating Portainer, I had a typo in a middleware name. This caused Portainer to not start.

Checking Traefik 2 logs can help you figure out what the problem is. In addition, check Traefik 2 dashboard for more information (see below).

Traefik V2 Router Error On Dashboard
Traefik V2 Router Error On Dashboard

In the case of Portainer, upon digging deeper, Traefik 2 dashboard revealed that Traefik 2 could not find the middlewares.chain-no-auth.

Traefik 2.0 Router Error Details
Traefik 2.0 Router Error Details

This should have been middlewares-chain-no-auth (- instead of .). Once I fixed this issue, Portainer started right up.

404 or Cloudflare 502

The most common reason for these errors are:

  1. The service did not start properly
  2. The service port defined under Traefik labels is wrong

For 1) check the logs for service to ensure it starts without problems. Sometimes improper permissions for the app data folder can cause the service to not start. If the service started without problems, then check that you specified the right port for the service under Traefik labels (commonly overlooked during copy-pasting).

Cloudflare 522 Error - Connection Timed Out

This means that cloudflare can't even reach your Traefik host. This is much worse than a 404 or a Cloudflare 502, where Cloudflare could get in but just not find your service. The most common cause for the 522 error is probably a firewall (e.g. Router, UFW, etc.) blocking (or not allowing) ports 80 and 443, which are needed for Traefik to work. This includes missing or improper port forwarding ports 80 and 443 to the Traefik host.

Tools to Check Logs

Dozzle allows you to monitor Docker logs of all your containers in real-time. This helps to troubleshoot and fix issues.

Viewing Docker Logs In Dozzle
Viewing Docker Logs In Dozzle

You can find the docker-compose example for Dozzle in my GitHub Repo.

Tip to Identify Copy-Paste Errors

Another tip is to use one of the many available "Diff Checkers" online. Copy your code on one side and copy-paste the docker-compose examples from this guide or my Github repo on the other side. The differences between the two will be highlighted for you.

Note: An updated version of this guide is available: Ultimate Traefik Docker Compose Guide [2024]: LE, SSL, Reverse Proxy

Docker Compose Traefik v2 Stack - Final Thoughts

This Traefik reverse proxy Docker guide is an addon guide to my Docker media server guide and is an upgraded version of the guide previously published for Ubuntu 20.04. As you can see it turned out to be a lengthy one. But it provides a one-stop solution for implementing Traefik 2 reverse proxy for Docker services.

As mentioned before, one of the main advantages of a reverse proxy is the ability to expose the app to the internet without exposing its ports. If you have successfully implemented reverse proxy for docker then at this point I strongly recommend disabling port forwards on your router (except 80 and 443 that Traefik needs). You would still be able to access your apps using IP-ADDRESS:port from your home network.

Please remember that this is just the start of the journey. From here, you can consider making your Docker Traefik setup more secure, implement Cloudflare tweaks for Traefik, and improve your authentication with either Google OAuth.

If you prefer a more private and self-hosted solution to Google OAuth, then try Authelia.

In addition, a strong intrusion prevention system with CrowdSec is highly recommended.

Hopefully, this guide broke Traefik 2 concepts down enough for you to understand. I am still learning and by no means an expert on this topic. So if you have a better way of doing what I did, please feel free to share in the comments and I will respond and update the guide if needed. Or, hop on to our Discord server to chat about it.

Otherwise, we hope that this Traefik 2 Docker Home Server setup tutorial guide helped you accomplish what you set out to do.

Be the 1 in 200,000. Help us sustain what we do.
25 / 150 by Dec 31, 2024
Join Us (starting from just $1.67/month)

Anand

Anand is a self-learned computer enthusiast, hopeless tinkerer (if it ain't broke, fix it), a part-time blogger, and a Scientist during the day. He has been blogging since 2010 on Linux, Ubuntu, Home/Media/File Servers, Smart Home Automation, and related HOW-TOs.