How to set up Traefik and Authentik with Docker Compose

In this guide I will show you how to set up Traefik and Authentik in docker compose with both of them as services.

All with Let's Encrypt support so you can use https for the services behind Traefik.

Set up Docker

If you already have docker and docker compose installed click here to skip to the next step.

I'm using Ubuntu 22.04 so these steps will reflect that.

First let's set up the Docker repository as described on their website.
Run the following commands:

sudo apt-get update
sudo apt-get install ca-certificates curl gnupg
sudo install -m 0755 -d /etc/apt/keyrings

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg

sudo chmod a+r /etc/apt/keyrings/docker.gpg

echo \
  "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
  "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt-get update

Now, install docker and the docker compose plugin with:
sudo apt-get install docker.io
sudo apt-get install docker-compose-plugin

Verify that both have been installed correctly:

$ docker -v
Docker version 20.10.21, build 20.10.21-0ubuntu1~22.04.3

$ docker compose version
Docker Compose version v2.19.1

If either of these respond with "Command not found" then something isn't right. Try to repeat the steps above or check Docker's website for updated steps.

Tip: if you want to run docker compose commands without sudoing every time run sudo usermod -aG docker $USER to add your user to the docker group.

Docker Compose

📖
"Compose is a tool for defining and running multi-container Docker applications. With Compose, you use a YAML file to configure your application’s services. Then, with a single command, you create and start all the services from your configuration." - docker.com

This is the yaml file we're going to use. Name it compose.yaml.

services:
  traefik:
    image: traefik:2.10.3
    container_name: traefik
    restart: unless-stopped
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./shared/traefik/static.yaml:/etc/traefik/traefik.yaml:ro
      - ./shared/traefik/dynamic.yaml:/etc/traefik/dynamic.yaml:ro
      - letsencrypt:/letsencrypt
    ports:
      - 80:80
      - 443:443

  authentik_server:
    image: ghcr.io/goauthentik/server:2023.6.0
    restart: unless-stopped
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: authentik_redis
      AUTHENTIK_POSTGRESQL__HOST: authentik_postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASS}
    volumes:
      - ./shared/authentik/media:/media
    env_file:
      - .env
    depends_on:
      - authentik_postgresql
      - authentik_redis

  authentik_worker:
    image: ghcr.io/goauthentik/server:2023.6.0
    restart: unless-stopped
    command: worker
    environment:
      AUTHENTIK_REDIS__HOST: authentik_redis
      AUTHENTIK_POSTGRESQL__HOST: authentik_postgresql
      AUTHENTIK_POSTGRESQL__USER: authentik
      AUTHENTIK_POSTGRESQL__NAME: authentik
      AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_DB_PASS}
    user: root
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./shared/authentik/media:/media
    env_file:
      - .env
    depends_on:
      - authentik_postgresql
      - authentik_redis

  authentik_postgresql:
    image: docker.io/library/postgres:12-alpine
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 5s
    volumes:
      - ./shared/authentik/database:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${AUTHENTIK_DB_PASS:?database password required}
      POSTGRES_USER: authentik
      POSTGRES_DB: authentik
    env_file:
      - .env

  authentik_redis:
    image: docker.io/library/redis:alpine
    command: --save 60 1 --loglevel warning
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      start_period: 20s
      interval: 30s
      retries: 5
      timeout: 3s
    volumes:
      - authentik_redis:/data

volumes:
  letsencrypt:
    driver: local
  authentik_redis:
    driver: local

As you can see we set up five services, four of which are for authentik. Two even use the same image, though if you look closely one runs as a "server" and the other as a "worker". This is authentik specific lingo so don't worry too much about that.

In addition to those we also run an instance of postgres, where authentik will store its data and another for redis that it uses for caching.

You may also have noticed that we reference an environment variable: AUTHENTIK_DB_PASS . Let's address that too.

Create a file next to compose.yaml named .env and run the following commands to generate your database password and secret key:

echo "AUTHENTIK_DB_PASS=$(pwgen -s 40 1)" >> .env
echo "AUTHENTIK_SECRET_KEY=$(pwgen -s 50 1)" >> .env

If pwgen is not available you'll need to install it by running

sudo apt install pwgen

Optional: You can replace other values in the compose.yaml file with variables gathered from the .env file just like we did for the database password.

Next, we'll be mounting some of our container's folders as volumes within a folder called shared (notice the volumes directives). So go ahead and create it.

At this point your file structure should look something like this:

/home/biscuit/tutorial/
├── compose.yaml
├── .env
├── shared/

Traefik

I will be showing you how to configure the Traefik reverse proxy without using labels. Most of the examples I was seeing online for similar setups were using labels but I ended up favoring standalone configuration files for a few reasons:

  • Traefik's documentation favors this format;
  • It's way more readable, especially as you add more services;
  • The Nextcloud AIO image which I am also using (though not covered here) does not support Traefik configuration as labels because it has a setup where that container creates and manages other containers. I think it's fair to chalk it up as a +1 for better compatibility.

Configuration files

📖
"Elements in the static configuration set up connections to providers and define the entrypoints Traefik will listen to (these elements don't change often).

The dynamic configuration contains everything that defines how the requests are handled by your system. This configuration can change and is seamlessly hot-reloaded, without any request interruption or connection loss." - traefik.io

We'll be creating two configuration files under ./shared/traefik: static.yaml and dynamic.yaml:

/home/biscuit/tutorial/
├── compose.yaml
├── .env
├── shared/
    ├── traefik/
        ├── static.yaml
        ├── dynamic.yaml

static.yaml

api: {}

entryPoints:
  web:
    address: :80
    http:
      redirections:
        entryPoint:
          to: websecure
          scheme: https
  websecure:
    address: :443

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false
  file:
    filename: "/etc/traefik/dynamic.yaml"
    watch: true

certificatesResolvers:
  leresolver:
    acme:
      email: "[email protected]"
      storage: "/letsencrypt/acme.json"
      tlsChallenge: {} # Used for tlsChallenge 

Some notes here:

  • api {} will enable access to the traefik dashboard;
  • We'll be using Let's Encrypt to issue SSL certificates for the domains and sub domains that will be served by Traefik. Replace [email protected] with your own e-mail address. This is only used by Let's Encrypt to send you notices about your certificates;
  • Good to know: We're redirecting all http traffic to https, notice how this is achieved with the redirections block inside http. The names I used for the entrypoints web and websecure are arbitrary. You can name them whatever, the important part is the ports they bind to.

dynamic.yaml

http:
  middlewares:
    authentik:
      forwardAuth:
        address: "http://authentik_server:9000/outpost.goauthentik.io/auth/traefik"
        trustForwardHeader: true
        authResponseHeaders: 
          - "X-authentik-username"
          - "X-authentik-groups"
          - "X-authentik-email"
          - "X-authentik-name"
          - "X-authentik-uid"
          - "X-authentik-jwt"
          - "X-authentik-meta-jwks"
          - "X-authentik-meta-outpost"
          - "X-authentik-meta-provider"
          - "X-authentik-meta-app"
          - "X-authentik-meta-version"
          
  services:
    authentik-srv:
      loadBalancer:
        servers:
          - url: "http://authentik_server:9000"

  routers:    
    traefik:
      rule: "Host(`traefik.example.com`) && (PathPrefix(`/`) || PathPrefix(`/api`))"
      service: "api@internal"
      middlewares:
        - "authentik"
      tls:
        certresolver: "leresolver"
      entryPoints:
        - "websecure"
    authentik-rtr:
      rule: "Host(`auth.example.com`)"
      service: "authentik-srv"
      tls:
        certresolver: "leresolver"
      entryPoints:
        - "websecure"

A few things I'd like to highlight here:

  • With Docker the hostname will default to match the service name in the compose file. That's why we can proxy to http://authentik_server:9000 ;
  • We added a middleware for authentik which we then use for the traefik dashboard. For this to work we still need to go into authentik and do some configuration;
  • Replace example.com with your own domain.

Authentik

Now with Traefik configured you should be able to run the command docker compose up -d  in the same directory where compose.yaml is to build and start your services. Give it a minute or two and you'll see Authentik running at https://auth.example.com/if/flow/initial-setup/:

Type  your e-mail and desired password and press continue.

Now you'll land on https://auth.example.com/if/user/#/library with an empty list of applications:

Press "Create a new application" to create your first application behind Authentik's auth. In this case, that will be Traefik's dashboard. Set the name to "Traefik Dashboard" and the slug to "traefik-dashboard" and press "Create".

Then go to Provider settings (https://auth.example.com/if/admin/#/core/providers) and create a new Provider:

Then go back to Application settings and set this provider for the Traefik Dashboard application:

Lastly, go to Outpost settings (https://auth.example.com/if/admin/#/outpost/outposts) and make sure to:

  • Select "Traefik Dashboard" in the list;
  • Change the value of "authentik_host" to https://auth.example.com

Update it and you're done!

When you now go to https://traefik.example.com you should see a screen like this:

Press "Continue" and

Done!

If you try to go to https://traefik.example.com in an incognito window you'll see the login screen again:

You now have a service that requires you to be authenticated before you can access it!