Self-host Authentik for SSO in 2026: Docker-compose stack + first integration
Authentik is the open-source identity provider that sits between your apps and
your users. It speaks OIDC, SAML, LDAP, SCIM, RADIUS, and proxy-outpost forward
auth in one binary. As of writing, the current release line is 2026.5.x
(latest tag 2026.5.4, released 2026-07-08), 3-month release cadence, calendar
versioning (YYYY.M.patch). This post walks a fresh Docker stack from zero to
your first protected app, then the four things that will burn you on day two.
What it is, who it’s for
Authentik is an IdP / SSO server built by Jens Langhammer (BeryJu) and a
substantial contributor community. The codebase is mostly Python (Django 5.2 +
DRF + Channels) with a TypeScript / Lit frontend bundled as a standalone SPA,
plus a Go-based proxy outpost for edge deployments. The whole thing is MIT
licensed (with the authentik/enterprise/ directory being a proprietary
add-on for paid support).
Vs the other options in the space:
- Authelia — single-binary Go forward-auth proxy, narrow scope, no admin
UI flow editor. Pick it for ~5 users and a handful of apps. - Keycloak — JVM stack, more enterprise features (fine-grained authz,
identity brokering), but heavier to run and older UI. Pick it when you need
UMA or FAPI conformance. - Authentik — modern Lit-based UI with a drag-and-drop flow editor,
OIDC/SAML/SCIM/LDAP/RADIUS in one box, proxy outposts for legacy apps.
Pick it for homelab to mid-size self-hosted shops.
The 2026.5 line brought several enterprise-only-tier features down to the
OSS core (JWE for OAuth2, SCIM source property mappings, RADIUS custom
attributes, SAML encryption), so the gap to commercial identity stacks is
notably narrower than it was a year ago.
The Docker stack — compose.yaml
Since 2025.10, Authentik is Postgres-only. Redis (previously used for
caching, the task queue, and WebSocket session storage) is no longer needed.
The default compose file ships three services plus a database volume:
services:
postgresql:
image: docker.io/library/postgres:16-alpine
env_file:
- .env
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_USER: ${PG_USER:-authentik}
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
healthcheck:
test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
volumes:
- database:/var/lib/postgresql/data
networks: [auth-net]
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.4}
command: server
env_file:
- .env
ports:
- "${AUTHENTIK_HTTP_PORT:-9000}:9000"
depends_on:
postgresql:
condition: service_healthy
volumes:
- ./data:/data
- ./custom-templates:/templates
networks: [auth-net]
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2026.5.4}
command: worker
depends_on:
postgresql:
condition: service_healthy
server:
condition: service_started
env_file:
- .env
volumes:
- ./data:/data
- ./custom-templates:/templates
- /var/run/docker.sock:/var/run/docker.sock
networks: [auth-net]
networks:
auth-net:
driver: bridge
volumes:
database:Three things that catch people:
- The server image and worker image are the same. Only the
command: server
vscommand: workerdiffers. Don’t try to swap them. - The worker mounts
/var/run/docker.sock. It uses this to spawn proxy
outposts (lightweight Go reverse proxies) on the same Docker network. If
you’re paranoid about that, run outposts separately and remove the mount. AUTHENTIK_TAGmust be a specific version. The:latesttag was frozen
at 2025.2 back in early 2026; pinning is now mandatory for security
patching.
The matching .env:
# Always required
PG_PASS=please-change-me
AUTHENTIK_SECRET_KEY=opensl rand -hex 32
AUTHENTIK_BOOTSTRAP_EMAIL=admin@yourdomain.com
AUTHENTIK_BOOTSTRAP_PASSWORD=please-change-me
# Recommended
AUTHENTIK_TAG=2026.5.4
AUTHENTIK_HTTP_PORT=9000
AUTHENTIK_LOG_LEVEL=info
# Optional: turn off error reporting (default sends stack traces to a hosted Sentry)
AUTHENTIK_ERROR_REPORTING_ENABLED=falseAUTHENTIK_SECRET_KEY is used to sign session tokens and encryption keys — set
it to a 32-byte random hex string. Rotating it invalidates all sessions.
First-run: the initial-setup flow
Authentik ships a built-in initial-setup flow that runs on first browser hit.
The URL must end with a trailing slash:
http://your-server:9000/if/flow/initial-setup/A bare http://your-server:9000/if/flow/initial-setup (no trailing slash)
returns “Not Found” — the redirect target is the slash-suffixed path. This
trips people every six months on the project discussion forums.
The flow prompts for an email and password for the default akadmin user,
automatically grants the superuser flag, and logs you in. From there:
- Log in as akadmin.
- Create a non-superuser user (real you, regular account).
- The Application wizard walks you through creating an application + its
OAuth2/OIDC provider in one go. This is the fastest way to add a new app. - Bind the provider to an authentication flow (or accept the default
default-authentication-flow).
Recovery, if you lock yourself out: docker compose exec server ak create_recovery_key prints a one-time recovery URL. Default validity is 60
minutes as of 2025.10 (it used to be years — that was a security footgun).
Wiring up your first app — Nextcloud via OIDC
Most self-hosted Nextcloud setups get here from “I want SSO so my partner
doesn’t have to remember another password.” Concretely:
# In Authentik admin:
# 1. Applications → Create with provider (wizard)
# Name: Nextcloud
# Slug: nextcloud
# Provider type: OAuth2/OpenID Connect
# Redirect URI: https://cloud.example.com/apps/user_oidc/oauth/oidc
# Signing Key: auto-generated
# 2. Save → note the Client ID and Client Secret# On the Nextcloud server (via occ or admin UI):
sudo -u www-data php /var/www/nextcloud/occ app:install user_oidc
sudo -u www-data php /var/www/nextcloud/occ config:app:set
user_oidc oidc_client_id --value="<from authentik>"
sudo -u www-data php /var/www/nextcloud/occ config:app:set
user_oidc oidc_secret --value="<from authentik>"
sudo -u www-data php /var/www/nextcloud/occ config:app:set
user_oidc oidc_provider_url
--value="https://auth.example.com/application/o/nextcloud/"A few Nextcloud-specific gotchas from the verified integration docs:
- HTTPS required. user_oidc refuses to talk to plain-HTTP providers.
- Trim the client secret to 64 chars. The Nextcloud user_oidc app has a
bug truncating longer secrets; Authentik generates 80+ char ones by default. - Set attribute mapping. On the Authentik provider, map
nextcloud_user_idtousernameandnextcloud_quotato a static value
(or leave unset to inherit from Nextcloud defaults).
Other commonly wired apps (all OIDC unless noted): Gitea (built-in),
Grafana (env vars GF_AUTH_GENERIC_OAUTH_*), Jellyfin (community
OIDC plugin), GitLab (OmniAuth OIDC), OpenVPN Access Server (SAML),
Home Assistant, Portainer, Snipe-IT, Affine, Arcane,
Zammad, Elastic Cloud.
Pitfalls (2026 edition)
A handful of things that bite everyone once:
- The
:latesttag is frozen at 2025.2. Pin a specific version (2026.5.4
as of writing) in your compose file. If you don’t, you miss every security
update after 2025.2. - Trailing slash on the initial-setup URL. Without it you get 404. Add it,
bookmark it. - *`AUTHENTIKBOOTSTRAP
env vars are silently ignored if you mount an empty/blueprints` directory.** This is the active wontfix/legacy issue
7546. Either include at least one blueprint, or use the browser-based
initial flow.
- Recovery keys only last 60 minutes as of 2025.10. Save the URL
immediately, don’t store it in a 1Password note you only check next week. - LDAP TLS verification has an opt-in “TLS Verification Certificate”
field. Leave it blank to skip cert validation (insecure default). For
real verification, point it at your CA bundle. - Outpost Docker socket. The worker uses
/var/run/docker.sockto spawn
proxy outposts automatically. If that mount worries you, deploy outposts
manually with the standaloneghcr.io/goauthentik/proxyimage and remove
the socket mount from the worker. - Calendar versioning, not semver.
2026.5.4is not the same as
upstream SemVer5.4. Major-version migrations betweenYYYY.Mseries
sometimes require reading the release notes.
When to pick something else
The rough rule of thumb for self-hosted SSO in 2026:
| Need | Pick |
|---|---|
| 1-10 apps, single user/partner, no flow customization | Authelia |
| OIDC + SAML + SCIM + LDAP/RADIUS + custom flows + homelab UI polish | Authentik |
| Hundreds of users, fine-grained authorization, identity brokering, FAPI/UMA | Keycloak |
| Enterprise SSO with no self-hosting | Microsoft Entra / Okta |
For a single Nextcloud at home, Authelia or even oauth2-proxy in front of
Nextcloud is plenty. You bring out Authentik when you have 3+ apps that all
need their own user store, or you need to enforce MFA centrally.
TL;DR
mkdir ~/authentik && cd $_
curl -fsSL https://docs.goauthentik.io/compose.yaml > compose.yaml
curl -fsSL https://docs.goauthentik.io/.env > .env
$EDITOR .env # set PG_PASS, AUTHENTIK_SECRET_KEY, BOOTSTRAP_EMAIL, BOOTSTRAP_PASSWORD
docker compose up -d
# Visit http://your-server:9000/if/flow/initial-setup/ (NOTE trailing slash)That’s it. Browser → set admin password → add your first Application via
wizard → done.
Sources
- Project: https://github.com/goauthentik/authentik
- Documentation: https://docs.goauthentik.io/
- Integrations: https://integrations.goauthentik.io/
- Latest release: https://github.com/goauthentik/authentik/releases/tag/version/2026.5.4
Comments