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.
Why Authentik?
Section titled “Why Authentik?”- 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.
Challenges you should be aware of
Section titled “Challenges you should be aware of”- The learning curve: flows, stages and policies are extremely powerful, but day-one can feel overwhelming.
- 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.
- 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.
My deployment at a glance
Section titled “My deployment at a glance”- 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).
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: trueAfter 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).
Complete step-by-step deployment guide
Section titled “Complete step-by-step deployment guide”Prerequisites
Section titled “Prerequisites”| Item | Note |
|---|---|
| A host (VM/LXC/bare-metal) | 2 vCPU, 2 GB RAM, 5 GB disk minimum |
| Docker + docker-compose v2 | sudo apt install docker.io docker-compose-plugin (Debian/Ubuntu) |
| A PostgreSQL password | Put into .env as POSTGRES_PASSWORD=•••• |
| A random secret key | openssl rand -base64 32 → AUTHENTIK_SECRET_KEY=•••• |
| Public domain + DNS control | auth.example.com A/AAAA or proxied CNAME |
| SMTP account (optional) | Enables e-mail verification & password reset |
| Cloudflare Tunnel (optional) | cloudflared tunnel create authentik |
Folder layout
Section titled “Folder layout”/opt/authentik ├─ docker-compose.yml ├─ .env ├─ database/ ├─ redis/ ├─ media/ └─ custom-templates/touch .env and add:
POSTGRES_USER=authentikPOSTGRES_PASSWORD=<strong-db-password>POSTGRES_DB=authentikAUTHENTIK_SECRET_KEY=<32-byte-random>AUTHENTIK_EMAIL__HOST=<smtp.example.com>AUTHENTIK_EMAIL__PORT=587AUTHENTIK_EMAIL__USERNAME=<smtp-user>AUTHENTIK_EMAIL__PASSWORD=<smtp-pass>AUTHENTIK_EMAIL__FROM="Auth <[email protected]>"AUTHENTIK_EMAIL__USE_TLS=trueAUTHENTIK_EMAIL__USE_SSL=falseTip — Protect secrets
Section titled “Tip — Protect secrets”chmod 600 .envsudo chown root:root .envDeploy
Section titled “Deploy”docker compose up -ddocker compose logs -f server # watch until "Listening at :9000"TLS & exposure
Section titled “TLS & exposure”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.
Initial configuration wizard
Section titled “Initial configuration wizard”- Login with the super-user you just created.
- Under Admin → Settings → Branding upload a logo and define organisation name and default UI language.
- Under Admin → System → E-mail click “Test” to confirm SMTP works.
- 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.
Backup strategy
Section titled “Backup strategy”| Component | Recommendation |
|---|---|
| PostgreSQL | pg_dumpall daily, keep 7 days |
| Redis | Not critical – authorisation cache only |
media/ | Stores issued certificates & branding images |
custom-templates/ | Keep in VCS or rsync |
Compose file & .env | Encrypt at rest (e.g. restic + gpg) |
Example cron job:
0 3 * * * docker exec authentik_postgres pg_dump -U $POSTGRES_USER $POSTGRES_DB | gzip > /backups/pg_$(date +\%F).sql.gzHigh-availability (optional)
Section titled “High-availability (optional)”- 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 forcloudflaredtunnel. Loadbalancing can be configured via the Pangolin dashboard due to its tight integration withtraefik. - Keep at least one
workernode with access to Docker socket for outpost auto-deployment.
Disaster-recovery checklist
Section titled “Disaster-recovery checklist”- Restore PostgreSQL dump.
- Re-create
media/(contains uploaded certs). - Restore the secret key from
.env. - 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.
Next steps
Section titled “Next steps”- 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).