Skip to content

Setting up a self-hosted SSO service with Authentik

As my collection of self-hosted services kept growing, maintaining local user databases for each application quickly became a chore. The moment you want to share those services with family or friends—and especially if any of them are reachable from the public Internet—you need a proper, centralised authentication flow.

Some popular self-hosted apps (Jellyfin, Calibre-Web, …) still do not ship with 2-Factor Authentication (2FA) out of the box. Authentik solves all of these problems in one neat, open-source package.

  • Self-hosted Identity Provider – keep your users and their data on your own hardware.
  • Single-Sign-On (SSO) – log in once, access everything.
  • Standards first – SAML 2.0, OAuth 2, OpenID Connect, LDAP.
  • Flows & Policies – build completely custom registration, MFA or password-reset journeys.
  • Strong security – TOTP, WebAuthn, FIDO2, GeoIP policies, session binding, optional FIPS mode.
  • Flexible deployment – Docker Compose, Kubernetes, Nomad, bare-metal.
  • Scales up nicely – a single PostgreSQL/Redis backend can serve thousands of identities.
  1. The learning curve: flows, stages and policies are extremely powerful, but day-one can feel overwhelming.
  2. Single point of failure: if Authentik is down and the target application has no local admin user, everyone is locked out. Always keep a break-glass account on each service.
  3. Open registration vs. invitation-only: you can allow self-service sign-up, but you probably want to restrict it by e-mail domain or an invitation token.
  • Public-facing VPS: blog, Mailcow, Vaultwarden.
  • Home-lab Proxmox nodes: most other services.
  • Authentik lives on a dedicated VM inside Proxmox and is exposed through a Cloudflared tunnel.
  • Minimum resources: 2 vCPU, 2 GB RAM (it will happily use more for reports and caching).
docker-compose.yml
services:
postgresql:
image: docker.io/library/postgres:16-alpine
container_name: authentik_postgres
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:
- ./database:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD} # .env variables
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_DB=${POSTGRES_DB}
networks:
cloudflared-network:
ipv4_address: 172.21.0.73 # static IP can be handy when using cloudflared.
redis:
image: docker.io/library/redis:alpine
container_name: authentik_redis
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:
- ./redis:/data
networks:
cloudflared-network:
ipv4_address: 172.21.0.72
server:
image: ghcr.io/goauthentik/server:2025.6.3
container_name: authentik_server
restart: unless-stopped
command: server
environment:
- AUTHENTIK_REDIS__HOST=redis
- AUTHENTIK_POSTGRESQL__HOST=postgresql
- AUTHENTIK_POSTGRESQL__USER=${POSTGRES_USER}
- AUTHENTIK_POSTGRESQL__NAME=${POSTGRES_DB}
- AUTHENTIK_POSTGRESQL__PASSWORD=${POSTGRES_PASSWORD}
- AUTHENTIK_ERROR_REPORTING__ENABLED=true
- AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}
- AUTHENTIK_EMAIL__HOST=${AUTHENTIK_EMAIL__HOST}
- AUTHENTIK_EMAIL__PORT=${AUTHENTIK_EMAIL__PORT}
- AUTHENTIK_EMAIL__USERNAME=${AUTHENTIK_EMAIL__USERNAME}
- AUTHENTIK_EMAIL__PASSWORD=${AUTHENTIK_EMAIL__PASSWORD}
- AUTHENTIK_EMAIL__FROM=${AUTHENTIK_EMAIL__FROM}
- AUTHENTIK_EMAIL__TIMEOUT=${AUTHENTIK_EMAIL__TIMEOUT}
- AUTHENTIK_EMAIL__USE_TLS=${AUTHENTIK_EMAIL__USE_TLS}
- AUTHENTIK_EMAIL__USE_SSL=${AUTHENTIK_EMAIL__USE_SSL}
volumes:
- ./media:/media
- ./custom-templates:/templates
# The following ports are the access points
#ports:
# - 9000:9000 # dashboard
# - 9443:9443 # proto-https
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
cloudflared-network:
ipv4_address: 172.21.0.70
worker:
image: ghcr.io/goauthentik/server:2025.6.3
container_name: authentik_worker
restart: unless-stopped
command: worker
environment:
- AUTHENTIK_REDIS__HOST=redis
- AUTHENTIK_POSTGRESQL__HOST=postgresql
- AUTHENTIK_POSTGRESQL__USER=${POSTGRES_USER}
- AUTHENTIK_POSTGRESQL__NAME=${POSTGRES_DB}
- AUTHENTIK_POSTGRESQL__PASSWORD=${POSTGRES_PASSWORD}
- AUTHENTIK_ERROR_REPORTING__ENABLED=true
- AUTHENTIK_SECRET_KEY=${AUTHENTIK_SECRET_KEY}
- AUTHENTIK_EMAIL__HOST=${AUTHENTIK_EMAIL__HOST}
- AUTHENTIK_EMAIL__PORT=${AUTHENTIK_EMAIL__PORT}
- AUTHENTIK_EMAIL__USERNAME=${AUTHENTIK_EMAIL__USERNAME}
- AUTHENTIK_EMAIL__PASSWORD=${AUTHENTIK_EMAIL__PASSWORD}
- AUTHENTIK_EMAIL__FROM=${AUTHENTIK_EMAIL__FROM}
- AUTHENTIK_EMAIL__TIMEOUT=${AUTHENTIK_EMAIL__TIMEOUT}
- AUTHENTIK_EMAIL__USE_TLS=${AUTHENTIK_EMAIL__USE_TLS}
- AUTHENTIK_EMAIL__USE_SSL=${AUTHENTIK_EMAIL__USE_SSL}
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
cloudflared-network:
ipv4_address: 172.21.0.71
networks:
cloudflared-network:
external: true

After start-up, visit:

http://<VM-IP>:9000/if/flow/initial-setup/

to create the first super-user (I keep two accounts: a day-to-day login and an “admin-service” account).

ItemNote
A host (VM/LXC/bare-metal)2 vCPU, 2 GB RAM, 5 GB disk minimum
Docker + docker-compose v2sudo apt install docker.io docker-compose-plugin (Debian/Ubuntu)
A PostgreSQL passwordPut into .env as POSTGRES_PASSWORD=••••
A random secret keyopenssl rand -base64 32AUTHENTIK_SECRET_KEY=••••
Public domain + DNS controlauth.example.com A/AAAA or proxied CNAME
SMTP account (optional)Enables e-mail verification & password reset
Cloudflare Tunnel (optional)cloudflared tunnel create authentik
/opt/authentik
├─ docker-compose.yml
├─ .env
├─ database/
├─ redis/
├─ media/
└─ custom-templates/

touch .env and add:

POSTGRES_USER=authentik
POSTGRES_PASSWORD=<strong-db-password>
POSTGRES_DB=authentik
AUTHENTIK_SECRET_KEY=<32-byte-random>
AUTHENTIK_EMAIL__HOST=<smtp.example.com>
AUTHENTIK_EMAIL__PORT=587
AUTHENTIK_EMAIL__USERNAME=<smtp-user>
AUTHENTIK_EMAIL__PASSWORD=<smtp-pass>
AUTHENTIK_EMAIL__FROM="Auth <[email protected]>"
AUTHENTIK_EMAIL__USE_TLS=true
AUTHENTIK_EMAIL__USE_SSL=false
Terminal window
chmod 600 .env
sudo chown root:root .env
Terminal window
docker compose up -d
docker compose logs -f server # watch until "Listening at :9000"

Option A — Cloudflared (no ports on your router): I host on my local network via a cloudflared tunnel.

Option B — on a VPS, put it behind your reverse proxy and set up ACME.

  1. Login with the super-user you just created.
  2. Under Admin → Settings → Branding upload a logo and define organisation name and default UI language.
  3. Under Admin → System → E-mail click “Test” to confirm SMTP works.
  4. Under Admin → Providers create at least one OAuth2 / OIDC provider (e.g. for Jellyfin) and/or a Proxy Provider for simple 2FA-protect-behind-reverse-proxy scenarios.

Creating reusable policies, flows, and stages

Section titled “Creating reusable policies, flows, and stages”

This is a rather complex topic and I will cover it in later posts.

ComponentRecommendation
PostgreSQLpg_dumpall daily, keep 7 days
RedisNot critical – authorisation cache only
media/Stores issued certificates & branding images
custom-templates/Keep in VCS or rsync
Compose file & .envEncrypt at rest (e.g. restic + gpg)

Example cron job:

Terminal window
0 3 * * * docker exec authentik_postgres pg_dump -U $POSTGRES_USER $POSTGRES_DB | gzip > /backups/pg_$(date +\%F).sql.gz
  • Use an external managed PostgreSQL and Redis cluster.
  • Run ≥ 2 server replicas behind a load-balancer. This can be easily done using Pangolin, which can be a drop-in replacement for cloudflared tunnel. Loadbalancing can be configured via the Pangolin dashboard due to its tight integration with traefik.
  • Keep at least one worker node with access to Docker socket for outpost auto-deployment.
  1. Restore PostgreSQL dump.
  2. Re-create media/ (contains uploaded certs).
  3. Restore the secret key from .env.
  4. Deploy the same Authentik image tag and start containers.

If AUTHENTIK_SECRET_KEY changed, cookies become invalid and all users must re-authenticate—but nothing is lost.

  • Play with Proxy Providers to front legacy web apps that don’t speak SAML/OIDC.
  • Configure Password reset and Self-service enrollment flows (invitation token + e-mail domain policy works great). This topic will be covered in the upcoming blog entry.
  • Enable Audit logging to Loki and forward to Grafana (optional).