Self-Hosting Authentik: Add SSO and MFA to All Your Apps Behind a Reverse Proxy
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
If you're running a stack of self-hosted apps — Jellyfin, Nextcloud, Gitea, Uptime Kuma, Portainer — you already know the pain: every service has its own login screen, its own password, and absolutely no shared session. Authentik fixes all of that. It's an open-source Identity Provider (IdP) that gives you Single Sign-On (SSO), LDAP, SAML, OAuth2/OIDC, and rock-solid MFA, all from a single Docker Compose deployment you fully control.
I've been running Authentik in front of my entire homelab for about a year now. Once it clicks, you'll wonder how you lived without it. This tutorial walks you through deploying Authentik with Docker Compose, wiring it up behind a Caddy reverse proxy, and protecting a real app (Portainer) with a forward-auth proxy provider — including enforcing TOTP-based MFA for every login.
What You'll Need
- A Linux server or VPS running Docker and Docker Compose v2. I do most of my work on a DigitalOcean Droplet
(4 vCPU / 8 GB RAM is comfortable for Authentik plus a couple of other services).
- A domain name with DNS pointing to your server (Authentik needs HTTPS — no workarounds).
- Caddy already installed or running in Docker. The approach works similarly with Nginx Proxy Manager; I'll note the differences.
- Basic comfort with Docker Compose and editing YAML.
Step 1 — Deploy Authentik with Docker Compose
Authentik needs PostgreSQL and Redis alongside its own server and worker containers. The project ships an official docker-compose.yml you can download directly, but I prefer maintaining my own so I keep it in a single docker-compose.yml alongside my other services. Below is a clean, production-ready version.
First, generate the two secrets you'll need before touching any YAML:
# Generate a secure secret key for Authentik
openssl rand -base64 36
# Generate a strong PostgreSQL password
openssl rand -base64 24
Save those values — you'll drop them into the Compose file in a moment. Now create your project directory and write the Compose file:
mkdir -p /opt/authentik && cd /opt/authentik
cat > docker-compose.yml <<'EOF'
version: "3.9"
services:
postgresql:
image: docker.io/library/postgres:16-alpine
container_name: authentik-postgres
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "pg_isready -d authentik -U authentik"]
start_period: 20s
interval: 30s
retries: 5
timeout: 5s
volumes:
- database:/var/lib/postgresql/data
environment:
POSTGRES_PASSWORD: ${PG_PASS}
POSTGRES_USER: authentik
POSTGRES_DB: authentik
networks:
- authentik-internal
redis:
image: docker.io/library/redis:7-alpine
container_name: authentik-redis
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:
- authentik-internal
server:
image: ghcr.io/goauthentik/server:2024.12
container_name: authentik-server
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_PASS}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
volumes:
- ./media:/media
- ./custom-templates:/templates
ports:
- "9000:9000"
- "9443:9443"
depends_on:
postgresql:
condition: service_healthy
redis:
condition: service_healthy
networks:
- authentik-internal
- proxy
worker:
image: ghcr.io/goauthentik/server:2024.12
container_name: authentik-worker
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_PASS}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
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:
- authentik-internal
volumes:
database:
redis:
networks:
authentik-internal:
internal: true
proxy:
external: true
EOF
Then create your .env file with the secrets you generated:
cat > .env <<'EOF'
PG_PASS=REPLACE_WITH_YOUR_POSTGRES_PASSWORD
AUTHENTIK_SECRET_KEY=REPLACE_WITH_YOUR_SECRET_KEY
EOF
# Lock down the .env file
chmod 600 .env
proxy network must already exist before you run docker compose up. If your Caddy container uses an external network called proxy, create it with docker network create proxy if it doesn't exist yet. If you named it differently, update the Compose file to match.Now bring everything up:
docker compose up -d
# Watch the logs until the server is healthy
docker compose logs -f server
After 30–60 seconds you should see Starting HTTP server… in the server logs. Authentik listens on port 9000 (HTTP) and 9443 (HTTPS).
Step 2 — Point Caddy at Authentik
I prefer Caddy because automatic HTTPS with Let's Encrypt is zero-configuration. Add this block to your Caddyfile:
auth.yourdomain.com {
reverse_proxy authentik-server:9000
}
# Forward-auth snippet — import this in any site block you want protected
(authentik_forward_auth) {
forward_auth authentik-server:9000 {
uri /outpost.goauthentik.io/auth/caddy
copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email
trusted_proxies private_ranges
}
}
To protect a service — say Portainer running on port 9001 — add the import to its site block:
portainer.yourdomain.com {
import authentik_forward_auth
reverse_proxy portainer:9001
}
Reload Caddy with docker exec caddy caddy reload --config /etc/caddy/Caddyfile and Caddy will automatically obtain certificates for both subdomains.
auth_request pointing to http://authentik-server:9000/outpost.goauthentik.io/auth/nginx. Authentik's documentation has a copy-paste NPM snippet — it works exactly as described there.Step 3 — First-Time Setup and Creating Your Admin Account
Navigate to https://auth.yourdomain.com/if/flow/initial-setup/. Authentik will prompt you to create the default akadmin user and set a password. Do this immediately after deployment — the endpoint is unprotected until you complete it.
Once you're in the admin interface at https://auth.yourdomain.com/if/admin/, the key areas to explore are:
- Applications — one entry per app you want to protect or provide SSO to.
- Providers — the protocol layer (Proxy, OIDC, SAML, LDAP). For forward-auth, you create a Proxy Provider.
- Outposts — the embedded outpost handles forward-auth requests. By default Authentik spins one up inside its own container; for production I recommend deploying a separate outpost container for cleaner separation.
- Flows — the logic that runs when a user authenticates. This is where you enforce MFA.
Step 4 — Create a Proxy Provider and Application for Portainer
In the admin panel:
- Go to Providers → Create → Proxy Provider.
- Name it Portainer Proxy, set mode to Forward auth (single application).
- Set External host to
https://portainer.yourdomain.com. - Leave the authentication flow as the default default-authentication-flow for now.
- Save, then go to Applications → Create.
- Name it Portainer, set the provider to the one you just created, and set Launch URL to
https://portainer.yourdomain.com. - Finally, go to Outposts, edit the default embedded outpost, and add the Portainer application to it.
Now visit https://portainer.yourdomain.com. You'll be redirected to the Authentik login screen. After authenticating, you land on Portainer — single sign-on working in under five minutes of UI work.
Step 5 — Enforce TOTP MFA for All Users
Go to Flows & Stages → Stages → Create and add an Authenticator TOTP Stage. Name it totp-setup. Then edit the default authentication flow (default-authentication-flow) and insert a Authenticator Validation Stage between the password stage and the consent stage. Set it to require any configured TOTP device.
The next time a user logs in without a TOTP device enrolled, Authentik automatically walks them through the enrollment flow — it displays a QR code compatible with Google Authenticator, Aegis, or Bitwarden Authenticator. Once enrolled, every subsequent login requires the six-digit code. There's no per-app configuration needed; the flow applies to every application in your Authentik instance.
Hosting This on a VPS?
Authentik genuinely needs memory — plan for at least 2 GB RAM dedicated to it under light load, 4 GB if you're running several concurrent users. I run mine on a DigitalOcean Droplet alongside Caddy and a handful of other services without issues. DigitalOcean's $24/month 4 vCPU / 8 GB Droplet is the sweet spot for a full homelab-in-the-cloud setup.
Gotchas I Hit Along the Way
- Clock drift kills TOTP. If your server clock drifts more than 30 seconds, TOTP codes will silently fail. Install
chronyorsystemd-timesyncdand verify withtimedatectl status. - The embedded outpost needs the server hostname. When configuring the outpost, set Authentik Host to your public URL (
https://auth.yourdomain.com), notlocalhost. Otherwise the redirect after login will send users to an internal address. - PostgreSQL version matters. Authentik 2024.x requires Postgres 14+. The Alpine image
postgres:16-alpinein my Compose file is solid and keeps the footprint small. - Don't expose port 9443 directly. Let Caddy handle TLS. Authentik's built-in HTTPS is fine for dev but Caddy gives you automatic cert renewal and HTTP/2 without any extra effort.
What to Do Next
With the core setup working, the next logical step is wiring up OAuth2/OIDC for apps that support it natively — Gitea, Nextcloud, and Grafana all have first-class OIDC support and will respect group memberships from Authentik, letting you control access at the group level from one place. After that, look at Authentik's LDAP outpost: it lets legacy apps that only speak LDAP authenticate against Authentik users without any code changes on the app side.
If you want to lock things down further, pair Authentik with a Cloudflare Tunnel so none of your ports are exposed to the public internet at all — Authentik handles authentication, Cloudflare handles the network edge, and you sleep better at night.
Discussion