Authentik: self-hosted SSO for your apps
Run Authentik on Docker Compose for one login across every app you host: the env values that matter, the akadmin bootstrap, and forward auth via Traefik.
One login for every app you host
Authentik is a self-hosted SSO (single sign-on) server: your users sign in once, and every app behind it accepts that session instead of asking for its own password. The install is an official Docker Compose file and two generated secrets. The part that takes real thought comes after: pointing a reverse proxy at it, and putting one existing app behind forward auth.
Authentik ships as three services in that Compose file: a PostgreSQL database, a server process, and a worker process. The server container also runs the embedded outpost, which is the component that answers "is this request signed in?" for every protected app. Version 2026.5 is the current release as of July 2026, and the project asks for a host with at least 2 CPU cores and 2 GB of RAM. Treat that as the floor. PostgreSQL and the worker both hold memory once the box has been up for a day.
What you need before you start
You need Docker Engine with the Compose v2 plugin, which you can confirm with docker compose version. If that prints an error instead of a version, install the plugin before going further; the basics are covered in running apps with Docker Compose on a VPS. You also need a DNS A record pointing at the server, auth.example.com in the examples below, because Authentik builds its redirect URLs from the hostname the browser used.
Run the stack as an ordinary user in the docker group rather than as root. Membership of that group is equivalent to root on the host, so give it to one deploy account and no one else, along the lines of least-privilege user accounts on a VPS.
Install with the official Compose file
sudo install -d -o "$USER" -g "$USER" /opt/authentik
cd /opt/authentik
wget https://docs.goauthentik.io/compose.yml
echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> .env
docker compose pull
docker compose up -ddocker compose ps should list three containers, with postgresql reporting healthy and server and worker reporting running. The first start runs the database migrations, so give it a minute before the web interface answers.
Both generated values matter, for different reasons. PG_PASS is the PostgreSQL password, and it has a hard limit of 99 characters. AUTHENTIK_SECRET_KEY signs sessions and tokens, so changing it later logs every user out and invalidates every API token you have issued. Keep .env at mode 600 and keep a copy somewhere safe, because a database restored without its matching secret key is a database nobody can log into.
The Compose file reads both values with the ${PG_PASS:?database password required} form, which means Compose refuses to start when the file is missing. Running docker compose up -d from the wrong directory prints required variable AUTHENTIK_SECRET_KEY is missing a value: secret key required and stops. That message is a path problem, not a config problem.
The environment values that matter
Everything else goes in the same .env file. Authentik maps a double underscore to a nested configuration key, so AUTHENTIK_EMAIL__HOST sets email.host. A single underscore is ignored with no warning, which is the most common reason a setting appears to do nothing.
AUTHENTIK_BOOTSTRAP_PASSWORDsets the password of the built-inakadminuser on first start, so you never type one into a public web form.AUTHENTIK_BOOTSTRAP_EMAILandAUTHENTIK_BOOTSTRAP_TOKENset that user's address and an API token the same way.COMPOSE_PORT_HTTPandCOMPOSE_PORT_HTTPSmove the published ports off the defaults of 9000 and 9443.AUTHENTIK_EMAIL__HOST,AUTHENTIK_EMAIL__PORT,AUTHENTIK_EMAIL__USERNAME,AUTHENTIK_EMAIL__PASSWORD,AUTHENTIK_EMAIL__USE_TLSandAUTHENTIK_EMAIL__FROMconfigure outbound mail. Without them Authentik trieslocalhoston port 25, so password-reset mails end as a connection error in the worker log.AUTHENTIK_LOG_LEVEL=debugturns on the detail you want while a login flow is misbehaving. Put it back toinfoafterwards.AUTHENTIK_ERROR_REPORTING__ENABLEDisfalseby default. Set it totrueonly if you are happy sending crash reports upstream.
These are secrets in a plain file, so treat the directory the way you treat any other credential store. A password manager such as a self-hosted Vaultwarden instance is a better home for the recovery copy than a note on your laptop.
First login and the admin account
Open http://SERVER_IP:9000 in a browser. Authentik shows its initial setup flow and asks you to set a password for the default akadmin user. If you already set AUTHENTIK_BOOTSTRAP_PASSWORD, that step is done and you go straight to the login page.
Create a normal admin user for yourself under Directory and then Users, add it to the authentik Admins group, and sign in as that account. Leave akadmin as a break-glass account with a long password stored offline. Everyday work under a shared built-in account destroys the audit log, because every event says akadmin and nothing says who.
Put Authentik behind your reverse proxy
Publishing port 9000 to the internet works, but you want TLS (transport layer security) and a real hostname. If you already run the setup from Traefik as a reverse proxy for multiple Compose apps, join Authentik to the same external proxy network with an override file. Create docker-compose.override.yml next to compose.yml:
services:
server:
networks:
- default
- proxy
labels:
traefik.enable: "true"
traefik.docker.network: proxy
traefik.http.routers.authentik.rule: Host(`auth.example.com`)
traefik.http.routers.authentik.entrypoints: websecure
traefik.http.routers.authentik.tls.certresolver: le
traefik.http.services.authentik.loadbalancer.server.port: "9000"
networks:
proxy:
external: trueApply it with docker compose up -d. Compose merges the override automatically, so the server service keeps everything from the official file and gains the labels. Check with curl -I https://auth.example.com/if/user/, which should answer HTTP/2 200. A 404 page not found from Traefik means the container is not on the proxy network, and Traefik cannot route to a container it cannot reach.
Once the hostname works, bind the published ports to 127.0.0.1 in the override, so the only way in is through the proxy.
Protect one app with forward auth
Authentik's proxy provider has three modes, and picking the wrong one costs an hour. Proxy means the outpost itself forwards traffic to the upstream app. Forward auth (single application) means your own reverse proxy still moves the traffic and only asks Authentik whether the request is signed in. Forward auth (domain level) protects every app under one parent domain with a single provider, at the cost of per-application authorization rules. With Traefik in front, you want forward auth (single application).
In the web interface, open Applications and then Providers, create a Proxy Provider, choose the forward auth single application mode, and set the external host to https://app.example.com. Create an Application that points at that provider. Then open Outposts, edit the authentik Embedded Outpost, and move the new application into its selected applications. The outpost only answers for applications it has been given, so skipping that last step is why a correctly configured provider still returns nothing.
Define the middleware once, on the Authentik container, and reference it from every protected app:
traefik.http.middlewares.authentik.forwardauth.address: http://server:9000/outpost.goauthentik.io/auth/traefik
traefik.http.middlewares.authentik.forwardauth.trustForwardHeader: "true"
traefik.http.middlewares.authentik.forwardauth.authResponseHeaders: X-authentik-username,X-authentik-groups,X-authentik-email,X-authentik-name,X-authentik-uid,X-authentik-jwt,X-authentik-meta-jwks,X-authentik-meta-outpost,X-authentik-meta-provider,X-authentik-meta-app,X-authentik-meta-versionauthResponseHeaders is the list of headers Traefik copies from Authentik's answer onto the request it sends upstream. Omit it and the app is still protected, but it never learns who the user is, so anything that reads X-authentik-username for automatic login stays logged out.
The protected app itself needs two routers, not one:
labels:
traefik.enable: "true"
traefik.http.routers.myapp.rule: Host(`app.example.com`)
traefik.http.routers.myapp.entrypoints: websecure
traefik.http.routers.myapp.tls.certresolver: le
traefik.http.routers.myapp.middlewares: authentik@docker
traefik.http.routers.myapp-auth.rule: Host(`app.example.com`) && PathPrefix(`/outpost.goauthentik.io/`)
traefik.http.routers.myapp-auth.entrypoints: websecure
traefik.http.routers.myapp-auth.tls.certresolver: le
traefik.http.routers.myapp-auth.priority: "15"
traefik.http.routers.myapp-auth.service: authentikThe second router is the part everyone leaves out. After a sign-in, Authentik sends the browser back to a path under /outpost.goauthentik.io/ on the app's hostname, not on auth.example.com. Without a router sending that path prefix to the Authentik service, the request lands on your app, which answers 404, and the login never finishes. The higher priority is what makes the specific path rule win over the plain Host() rule on the same domain.
Test it in a private browser window. You should be sent to auth.example.com, sign in, and come back to the app. docker compose logs -f server on the Authentik side prints an authorization event per attempt, which tells you whether the request reached Authentik at all.
The failures you will actually hit
Endless redirect loop between the app and the login page. The external host on the provider does not match what the browser uses, usually http:// in the provider against https:// in the address bar. The session cookie is then set for a different origin, so every trip back looks like a fresh anonymous request. Fix the external host and clear cookies for both domains before retesting.
404 at /outpost.goauthentik.io/start. The outpost router is missing, or its priority is lower than the catch-all router for that host.
The app loads without ever asking for a login. The middlewares label names a middleware that does not exist. Traefik does not warn about that, so a typo in authentik@docker simply means no middleware runs. Open the Traefik dashboard and confirm the router lists the middleware.
403 from Authentik after a successful login. The user is authenticated but not authorized: the application carries a policy binding, or a group requirement, that this user does not satisfy. The Events log in the admin interface names the policy that denied it.
When Keycloak is the better fit
Keycloak is the older project, backed by Red Hat, and it is the stronger choice for classic enterprise identity work: heavy SAML federation, brokering logins from several external identity providers at once, and realm export and import as a documented migration path. Commercial support behind it matters to some organisations on paper. The trade is that Keycloak has no proxy of its own, so protecting an app that speaks no OIDC (OpenID Connect) means running something like oauth2-proxy beside it. Authentik's built-in proxy provider is that piece, already integrated, which is why most self-hosters with a mixed pile of apps land here.
Backups and upgrades
Three things make a restore possible: the PostgreSQL database, the ./data directory, and .env.
cd /opt/authentik
docker compose exec -T postgresql pg_dump -U authentik authentik | gzip > authentik-$(date +%F).sql.gzStore that dump and .env together. The dump alone is not enough, because the secret key that protects session and token data lives in .env.
Upgrades are a tag change. Set AUTHENTIK_TAG in .env to the release you want, then run docker compose pull followed by docker compose up -d. Read the release notes first, because Authentik uses date-based versions and some releases carry migrations that expect you to arrive from the previous one. Take the database dump before the pull, not after.
FAQ
Is Authentik free to self-host?
The open source edition is free and covers everything above: the proxy provider, forward auth, OIDC (OpenID Connect), SAML, and the flows engine. A paid enterprise tier adds support and some enterprise features, but nothing here needs a licence.
Do I need Traefik to use Authentik?
No. Forward auth works with nginx through auth_request and with Caddy through forward_auth. The pattern is the same in every case: the reverse proxy asks Authentik about each request, and the path prefix /outpost.goauthentik.io/ on the protected hostname must route to Authentik instead of to the app.
Why does my protected app bounce between login and error forever?
The external host configured on the proxy provider does not match the URL the browser is using, most often http against https. The session cookie is issued for one origin and read on another, so Authentik sees an anonymous request every time. Correct the external host, then clear cookies for both hostnames before you test again.
How much RAM does Authentik need?
The documented minimum is 2 CPU cores and 2 GB of RAM as of July 2026, covering PostgreSQL, the server and the worker together. On a 2 GB box the worker is the first process the kernel kills under memory pressure, and the symptom is background tasks and outbound email stopping while the login page still works. Give it 4 GB if the same server also runs the apps you are protecting.