Self-Hosting Authentik for Single Sign-On Across All Your Docker Apps
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
If you're running ten or fifteen self-hosted apps, managing separate usernames and passwords for each one becomes a genuine security liability — not just an annoyance. Authentik is the identity provider I now put in front of everything: Gitea, Grafana, Nextcloud, Jellyfin, you name it. One login, one place to revoke access, full audit logs. In this tutorial I'll walk through deploying Authentik with Docker Compose and wiring up your first application via OpenID Connect (OIDC).
What Authentik Actually Does
Authentik is an open-source Identity Provider (IdP) that speaks SAML 2.0, OAuth 2.0, OpenID Connect, LDAP, and its own forward-auth proxy protocol. I prefer it over Authelia for complex setups because it has a proper admin UI, per-application policies, built-in MFA enrollment flows, and a much richer user management experience. The trade-off is that it's heavier — plan for at least 1 GB of RAM dedicated to Authentik and its backing services.
If you're running this on a VPS, a 2 vCPU / 2 GB RAM Droplet on DigitalOcean is the sweet spot. The $12/month basic Droplet handles Authentik plus a handful of other services without sweating.
Prerequisites
- Docker Engine 24+ and Docker Compose v2
- A domain name with DNS pointed at your server (e.g.,
auth.example.com) - A reverse proxy already running (Caddy or Traefik — I'll use Caddy labels here)
- Port 443 open and an existing
proxyDocker network
Step 1 — Create the Docker Compose Stack
Authentik requires PostgreSQL and Redis. I keep everything in one Compose file so they start and stop together. Create a directory, drop in a .env file with your secrets, then write the Compose file.
First, generate the required secret key and password values. Run this once and paste the output into your .env:
# Generate a strong secret key for Authentik
openssl rand -base64 50 | tr -d '\n'
# You'll need three values — run it three times or split the output.
# Store them as:
# AUTHENTIK_SECRET_KEY=...
# PG_PASSWORD=...
# AUTHENTIK_BOOTSTRAP_PASSWORD=... (your initial admin password)
Now create docker-compose.yml inside ~/authentik/:
---
name: authentik
services:
postgresql:
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d authentik -U authentik"]
interval: 30s
timeout: 5s
retries: 5
volumes:
- pg_data:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASSWORD}
POSTGRES_USER: authentik
POSTGRES_DB: authentik
networks:
- internal
redis:
image: docker.io/library/redis:7-alpine
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 3s
retries: 3
volumes:
- redis_data:/data
networks:
- internal
server:
image: ghcr.io/goauthentik/server:2024.12
restart: unless-stopped
command: server
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD}
AUTHENTIK_BOOTSTRAP_EMAIL: [email protected]
volumes:
- ./media:/media
- ./custom-templates:/templates
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- internal
- proxy
labels:
caddy: auth.example.com
caddy.reverse_proxy: "{{upstreams 9000}}"
worker:
image: ghcr.io/goauthentik/server:2024.12
restart: unless-stopped
command: worker
environment:
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASSWORD}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./custom-templates:/templates
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- internal
volumes:
pg_data:
redis_data:
networks:
internal:
proxy:
external: true
worker service mounts the Docker socket and runs as root so it can execute outbound flows and manage certificates. If this concerns you, restrict the worker's capabilities with security_opt: no-new-privileges:true and consider using a Docker socket proxy like tecnativa/docker-socket-proxy instead of the raw socket.Bring the stack up:
cd ~/authentik
docker compose up -d
# Watch the logs until you see "Starting server"
docker compose logs -f server
Give it about 60–90 seconds on first boot while it runs database migrations. Then navigate to https://auth.example.com/if/flow/initial-setup/ to finish the bootstrap wizard and set your admin credentials.
Step 2 — Create an OIDC Provider for an App
I'll use Grafana as the example since it has first-class OIDC support and most people running a homelab already have it deployed.
In the Authentik admin panel (https://auth.example.com/if/admin/):
- Go to Applications → Providers → Create
- Choose OAuth2/OpenID Provider
- Name it
Grafana, set the Authorization flow todefault-authorization-flow - Set Redirect URIs to
https://grafana.example.com/login/generic_oauth - Copy the generated Client ID and Client Secret
- Go to Applications → Create, link it to the Grafana provider, and set the slug to
grafana
Now update your Grafana environment variables in its Compose file:
environment:
GF_SERVER_ROOT_URL: https://grafana.example.com
GF_AUTH_GENERIC_OAUTH_ENABLED: "true"
GF_AUTH_GENERIC_OAUTH_NAME: "Authentik"
GF_AUTH_GENERIC_OAUTH_CLIENT_ID: "YOUR_CLIENT_ID"
GF_AUTH_GENERIC_OAUTH_CLIENT_SECRET: "YOUR_CLIENT_SECRET"
GF_AUTH_GENERIC_OAUTH_SCOPES: "openid profile email"
GF_AUTH_GENERIC_OAUTH_AUTH_URL: "https://auth.example.com/application/o/authorize/"
GF_AUTH_GENERIC_OAUTH_TOKEN_URL: "https://auth.example.com/application/o/token/"
GF_AUTH_GENERIC_OAUTH_API_URL: "https://auth.example.com/application/o/userinfo/"
GF_AUTH_GENERIC_OAUTH_ROLE_ATTRIBUTE_PATH: "contains(groups, 'authentik Admins') && 'Admin' || 'Viewer'"
GF_AUTH_SIGNOUT_REDIRECT_URL: "https://auth.example.com/application/o/grafana/end-session/"
GF_AUTH_OAUTH_AUTO_LOGIN: "true"
GF_AUTH_OAUTH_AUTO_LOGIN: "true" flag skips Grafana's own login page entirely and redirects straight to Authentik. This is what makes SSO feel seamless — users never see a second login prompt.Restart Grafana and try visiting it in an incognito window. You should be sent straight to Authentik's login page, and after authenticating, land in Grafana without touching a Grafana-specific password.
Step 3 — Forward Auth for Apps Without OIDC Support
Not every app supports OIDC natively. Authentik's Proxy Provider mode lets you put authentication in front of any web app via your reverse proxy's forward-auth feature. Apps like Portainer, Uptime Kuma running without built-in auth, or any static dashboard work perfectly this way.
In the Authentik admin panel, create a new provider but choose Proxy Provider instead of OAuth2. Set the mode to Forward auth (single application) and enter the external URL of the target app. Then in Caddy, add the following to that app's block:
# Caddyfile snippet for forward auth with Authentik
uptime.example.com {
forward_auth authentik_server:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
}
reverse_proxy uptime-kuma:3001
}
The authentik_server hostname works if your Caddy container is on the same Docker network as the Authentik server container. Otherwise use http://auth.example.com as the upstream and make sure Caddy trusts itself as an internal authority.
Enrolling Users and Enforcing MFA
Once SSO is running, user management becomes much simpler. Create users under Directory → Users. For MFA, go to Flows & Stages → Stages and add an Authenticator Validation Stage to your default authorization flow. Authentik supports TOTP (Google Authenticator, Aegis), WebAuthn hardware keys, and static backup codes out of the box.
I enforce TOTP for all users with access to admin-level apps by creating a separate authorization flow that includes the MFA validation stage, then binding that flow specifically to sensitive applications like Gitea admin and Portainer. Regular users hitting Jellyfin or Nextcloud get a frictionless password-only flow.
Staying Up to Date
Authentik releases updates frequently. I use Watchtower in monitor-only mode and update manually by changing the image tag in the Compose file and running docker compose pull && docker compose up -d. Always read the Authentik upgrade notes before bumping the version — some releases include breaking changes to the database schema that require the worker to finish migrations before the server comes back up.
If you want a cloud environment that makes managing multiple containers like this easier, DigitalOcean Droplets give you dependable 99.99% uptime SLA with predictable monthly pricing — ideal when Authentik is a critical piece of your infrastructure that everything else depends on.
Wrapping Up
Getting Authentik deployed and your first OIDC app connected takes maybe 45 minutes, and the payoff is immediate. Centralised user management, MFA enforcement, full audit trails, and the ability to instantly revoke access to every app at once by disabling a single account — that's homelab security that actually scales. My suggested next steps are to wire up Nextcloud via OIDC (it has a Social Login app that makes this straightforward) and then set up an LDAP outpost if you have apps that only speak LDAP, like some older selfhosted services or network gear. The Authentik docs on outposts are excellent and the community Discord is genuinely helpful when you hit edge cases.
Discussion