How to use Docker and Traefik to get started with self-hosting single sign-on with Keycloak.
Joey Miller • Last updated August 17, 2025
This guide is the first part in a multi-part series of guides:
There are plenty of great services to self-host, including Nextcloud, and Tandoor Recipes. If you've ever tried self-hosting more than a few services you'll understand the frustration of remembering many different passwords and continuously having to log in. After doing some research, I realized my homelab needed "single sign-on" (SSO). SSO is an authentication method (typically viewed as an enterprise feature) that allows secure authentication with many services using "just one set of credentials". Using SSO I would be able to achieve my dream of needing only a single-login event to access all of my services.
There are many tools that we can use for SSO, such as Authelia, Authentik, or Keycloak.
Although some of the aforementioned SSO tools may be easier to set up, I decided to go with Keycloak. Keycloak is an enterprise-level tool that is supported by Redhat. Using Keycloak will give us a lot of flexibility, and ticks the boxes for acceptable memory usage, theme-ability, and multi-factor authentication support.
Keycloak supports:
Traefik is an open-source reverse proxy and load balancer designed for containerized environments (such as Docker or Kubernetes).
I have earlier guides that cover a similar setup using Nginx Proxy Manager - there is some overlap. For self-hosting, the benefits of using Traefik become clear:
git repository.When we are done setting up, the project will be structured as follows:
├── conf
│ └── traefik
│ ├── conf.d
│ │ ├── traefik_dynamic_default.yml
│ └── traefik.yml
└── docker-compose.yml
To get started with setting up Traefik, let's create a static configuration file. This file will accomplish a few things including:
HTTP requests to HTTPSconf.d directoryconf/traefik/traefik.yml
entryPoints:
web:
address: ':80'
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ':443'
http:
tls:
certresolver: lets-encrypt
domains:
- main: example.com
sans:
- '*.example.com'
middlewares:
- default@file
api:
dashboard: true
certificatesResolvers:
lets-encrypt:
acme:
email: admin@example.com
storage: acme.json
dnsChallenge:
provider: namecheap
providers:
docker:
exposedByDefault: false
file:
directory: conf.d
Then, let's create a dynamic configuration file that sets some sane and secure defaults. This configuration is inspired by Benjamin Rancourt's Traefik configuration, with a few minor changes such as the prevention of search engine indexing (a good idea when self-hosting).
conf/traefik/conf.d/traefik_dynamic_default.yml
http:
middlewares:
# Inspired by
# https://www.benjaminrancourt.ca/a-complete-traefik-configuration/
# Recommended default middleware for most of the services
default:
chain:
middlewares:
- default-security-headers
- gzip
# Add automatically some security headers
default-security-headers:
headers:
browserXssFilter: true # X-XSS-Protection=1; mode=block
contentTypeNosniff: true # X-Content-Type-Options=nosniff
customResponseHeaders:
X-Robots-Tag: "noindex, nofollow"
forceSTSHeader: true # Add the Strict-Transport-Security header even when the connection is HTTP
frameDeny: true # X-Frame-Options=deny
referrerPolicy: "strict-origin-when-cross-origin"
stsIncludeSubdomains: true # Add includeSubdomains to the Strict-Transport-Security header
stsPreload: true # Add preload flag appended to the Strict-Transport-Security header
stsSeconds: 63072000 # Set the max-age of the Strict-Transport-Security header (63072000 = 2 years)
# Enable GZIP compression
gzip:
compress: {}
tls:
options:
default:
sniStrict: true
Add the following to the services: section of your docker-compose.yml file:
traefik:
image: traefik:latest
ports:
- '80:80/tcp'
- '443:443/tcp'
environment:
NAMECHEAP_API_USER: '< username >'
NAMECHEAP_API_KEY: '< key >'
volumes:
- ./conf/traefik/traefik.yml:/traefik.yml
- ./conf/traefik/conf.d:/conf.d
- ./data/traefik/acme.json:/acme.json
- /var/run/docker.sock:/var/run/docker.sock
labels:
- "traefik.enable=true"
- "traefik.http.routers.route-reverseproxy.rule=Host(`traefik.example.com`)"
- "traefik.http.routers.route-reverseproxy.service=api@internal"
restart: unless-stopped
Note: Exposing
/var/run/docker.sockcan be dangerous. Consider using (docker-socket-proxy)[https://github.com/Tecnativa/docker-socket-proxy] or running a rootless Docker installation.
Make sure to replace example.com with your domain name in all the above config files.
After running docker compose up from the directory that contains docker-compose.yml you will be able to access the Traefik dashboard at https://traefik.example.com. Initially, you may experience some SSL errors for a few minutes until Traefik retrieves the certificates from Lets Encrypt.
To get started with Keycloak, add the following keycloak entry to the services: section of your docker-compose.yml file:
keycloak:
# internal: keycloak on port 8080
image: quay.io/keycloak/keycloak:latest
command: start
environment:
KC_HOSTNAME: 'auth.example.com'
KC_PROXY: 'edge'
KEYCLOAK_ADMIN: 'admin'
KEYCLOAK_ADMIN_PASSWORD: 'admin'
volumes:
- ./data/keycloak:/opt/keycloak/data
labels:
- "traefik.enable=true"
- "traefik.http.routers.route-auth.rule=Host(`auth.example.com`)"
- "traefik.http.services.route-auth.loadbalancer.server.port=8080"
# Redirect '/' to '/admin'
- "traefik.http.middlewares.custom-redirect.redirectregex.regex=^https:\\/\\/([^\\/]+)\\/?$$"
- "traefik.http.middlewares.custom-redirect.redirectregex.replacement=https://$$1/admin"
- "traefik.http.routers.route-auth.middlewares=default@file,custom-redirect"
restart: unless-stopped
Note: The above example will have Keycloak running with the built-in H2 database. It is recommended in production to use an external database (i.e. MySQL, PostgreSQL, etc).
The labels: section in the above snippet is what configures Traefik to serve the Keycloak Administration Console at https://auth.example.com via Traefik (after re-running docker compose up).
We don't always want every user to be able to access every service. For example, you may want to give a friend access to your recipe application but not to the Pihole admin console.
If you have a compliant OIDC Client Application, setting Client authentication and Authorization to ON will be enough to access the Authorization tab for the client. Then you will be able to create a Group-based policy from the Policies sub-tab.
Unfortunately, this isn't always suitable - such as if the client uses SAML for authentication/authorization. In this case, another way to handle this in Keycloak (without needing any extensions) is to duplicate the default browser flow and add a condition that denies access if the user doesn't have the required role.
Note: This method requires a separate browser flow for each unique set of roles you would like to mandate for users of clients/services.
For more details see the answers by @Stuck and @heilerich on StackOverflow.
To increase the security of your authentication process, Keycloak allows enabling two-factor authentication (2FA) for users. This requires users to provide a valid one-time-pass (OTP) from an authenticator app on their smartphone.
Go to the Keycloak Administration Console
Go to Authentication > Required actions and for Configure OTP toggle Set as default action to On.
Go to the Users page. By clicking on each existing user, add Configure OTP to Required user actions from the User details > Details tab.
After these steps - on the next login users will be required to set up their authenticator for 2FA.
We've successfully set up a basic SSO implementation with Keycloak.
If all the services you are using are capable of authentication via OAuth, SAML, or Keycloak - the journey ends here for you. This implementation will be sufficient.
If you have other services that expect HTTP Header Auth or manage their own login flow (i.e. via LDAP) - continue reading. This guide continues in:
Tags
If you found this post helpful, please share it around: