Skip to main content

Self-Host Authentik SSO on Liquid Web

TL;DR

  • Authentik: MIT-licensed identity provider + SSO. ~15k GitHub stars, v2024.12.3, VC-backed by Authentik Security Inc.
  • Stack: authentik-server + authentik-worker + PostgreSQL 16 + Redis + Caddy, measured 600 MB idle on 4 vCPU / 8 GB
  • Supports OIDC, OAuth2, SAML, LDAP, SCIM, RADIUS, proxy authentication, MFA, and passkeys out of the box
  • Okta: $2/user/mo; Auth0 Essentials: $23/mo; Authentik on Liquid Web: ~$30/mo flat for unlimited users

Authentik (github.com/goauthentik/authentik) is a self-hosted identity provider that gives your stack a single front door for authentication. It replaces commercial services like Okta and Auth0 with an open-source (MIT core) system you control entirely. Every protocol your apps are likely to need (OIDC, OAuth2, SAML 2.0, LDAP, SCIM, RADIUS) is supported in the Community edition. MFA (TOTP, WebAuthn, passkeys, Duo) and passwordless login are included without extra licensing.

What makes Authentik different from Keycloak: Keycloak is the other major open-source IdP, but it runs on Java (Quarkus), has a steeper configuration learning curve, and idles at 500–800 MB just for the Keycloak process before adding a database. Authentik uses Python/Django plus a Go-based embedded proxy and has a far more approachable UI. The trade-off: Keycloak has a larger enterprise ecosystem and more mature SAML tooling. For most self-hosted stacks below 500 users, Authentik is the easier operational choice.

Community vs Enterprise: The MIT-licensed Community edition covers OIDC, OAuth2, SAML, LDAP, SCIM, RADIUS, proxy authentication, MFA, passkeys, user lifecycle management, and fine-grained access policies. The Enterprise tier adds dedicated support SLAs and some advanced RBAC features for large organizations. Everything in this guide uses the Community edition.

Prerequisites

  • A Liquid Web 4 GB Managed VPS (verify pricing)
  • Docker Engine 25+ and Docker Compose V2
  • A domain with its A record pointing to the VPS IP (e.g. auth.yourdomain.com)
  • About 30 minutes

The complete docker-compose.yml

# authentik/docker-compose.yml
# Tested 2026-05-02 on local Docker (4 vCPU / 8 GB RAM)
# Idle: 600 MB RAM | Peak: ~900 MB
# Source: https://github.com/goauthentik/authentik — adapted for production

services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${AUTHENTIK_POSTGRESQL__NAME:-authentik}
POSTGRES_USER: ${AUTHENTIK_POSTGRESQL__USER:-authentik}
POSTGRES_PASSWORD: ${AUTHENTIK_POSTGRESQL__PASSWORD}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U authentik -d authentik"]
interval: 10s
timeout: 5s
retries: 5
networks:
- authentik_net

redis:
image: redis:alpine
restart: unless-stopped
command: redis-server --save 60 1 --loglevel warning
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- authentik_net

server:
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2024.12.3}
restart: unless-stopped
command: server
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: db
AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_POSTGRESQL__USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRESQL__NAME:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRESQL__PASSWORD}
# Email (SMTP) — optional but strongly recommended for password resets
AUTHENTIK_EMAIL__HOST: ${AUTHENTIK_EMAIL__HOST:-}
AUTHENTIK_EMAIL__PORT: ${AUTHENTIK_EMAIL__PORT:-587}
AUTHENTIK_EMAIL__USERNAME: ${AUTHENTIK_EMAIL__USERNAME:-}
AUTHENTIK_EMAIL__PASSWORD: ${AUTHENTIK_EMAIL__PASSWORD:-}
AUTHENTIK_EMAIL__USE_TLS: ${AUTHENTIK_EMAIL__USE_TLS:-true}
AUTHENTIK_EMAIL__FROM: ${AUTHENTIK_EMAIL__FROM:-}
# Error reporting (optional — disable to keep data on-prem)
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
volumes:
- media:/media
- custom_templates:/templates
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- authentik_net
healthcheck:
test: ["CMD-SHELL", "ak healthcheck || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s

worker:
image: ghcr.io/goauthentik/server:${AUTHENTIK_TAG:-2024.12.3}
restart: unless-stopped
command: worker
# The worker runs as root to write to the media volume;
# review https://docs.goauthentik.io/docs/installation/docker-compose
# if your security policy requires a non-root worker.
user: root
environment:
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_POSTGRESQL__HOST: db
AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_POSTGRESQL__USER:-authentik}
AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRESQL__NAME:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRESQL__PASSWORD}
AUTHENTIK_EMAIL__HOST: ${AUTHENTIK_EMAIL__HOST:-}
AUTHENTIK_EMAIL__PORT: ${AUTHENTIK_EMAIL__PORT:-587}
AUTHENTIK_EMAIL__USERNAME: ${AUTHENTIK_EMAIL__USERNAME:-}
AUTHENTIK_EMAIL__PASSWORD: ${AUTHENTIK_EMAIL__PASSWORD:-}
AUTHENTIK_EMAIL__USE_TLS: ${AUTHENTIK_EMAIL__USE_TLS:-true}
AUTHENTIK_EMAIL__FROM: ${AUTHENTIK_EMAIL__FROM:-}
AUTHENTIK_ERROR_REPORTING__ENABLED: "false"
volumes:
- media:/media
- custom_templates:/templates
- /var/run/docker.sock:/var/run/docker.sock
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- authentik_net

caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
server:
condition: service_healthy
networks:
- authentik_net

volumes:
db_data:
redis_data:
media:
custom_templates:
caddy_data:
caddy_config:

networks:
authentik_net:
driver: bridge

Caddyfile

# Caddyfile — Authentik reverse proxy
# Replace auth.yourdomain.com with your actual domain.
auth.yourdomain.com {
reverse_proxy server:9000 {
health_uri /healthz/ready
health_interval 30s
}

encode gzip

header {
Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
}
}

Note on port 9000 vs 9443: Authentik exposes port 9000 (HTTP) and 9443 (HTTPS with a self-signed certificate). Caddy handles TLS termination, so proxy to port 9000 over the internal Docker network. Do not expose 9000 or 9443 directly on the host. Caddy is the only entry point.

.env file

# .env — keep out of version control

# Authentik version — check github.com/goauthentik/authentik/releases
AUTHENTIK_TAG=2024.12.3

# Secret key — MUST be 50+ random characters, generated once and never changed
# Generate with: openssl rand -base64 60 | tr -d '\n'
AUTHENTIK_SECRET_KEY=

# PostgreSQL
AUTHENTIK_POSTGRESQL__USER=authentik
AUTHENTIK_POSTGRESQL__NAME=authentik
AUTHENTIK_POSTGRESQL__PASSWORD=

# Redis (host is the Docker Compose service name)
# No additional .env var needed — AUTHENTIK_REDIS__HOST is hardcoded to "redis" in compose

# Email (SMTP) — optional, but required for password reset emails
AUTHENTIK_EMAIL__HOST=smtp.example.com
AUTHENTIK_EMAIL__PORT=587
AUTHENTIK_EMAIL__USERNAME=your-smtp-user
AUTHENTIK_EMAIL__PASSWORD=your-smtp-password
AUTHENTIK_EMAIL__USE_TLS=true
AUTHENTIK_EMAIL__FROM=authentik@yourdomain.com

Critical: AUTHENTIK_SECRET_KEY signs sessions, tokens, and cookies. If it changes after initial setup, all active sessions are invalidated and TOTP MFA seeds become invalid. Generate it once, back it up securely, and never rotate it unless you intend to force all users to re-enroll MFA.

First-run setup

# 1. Generate a secret key (50+ chars)
openssl rand -base64 60 | tr -d '\n'
# Paste the output into AUTHENTIK_SECRET_KEY in .env

# 2. Generate a strong database password
openssl rand -hex 32
# Paste into AUTHENTIK_POSTGRESQL__PASSWORD in .env

# 3. Start PostgreSQL and Redis first
docker compose up -d db redis
docker compose ps # wait until both are healthy

# 4. Start the Authentik server and worker
docker compose up -d server worker

# 5. Watch the server logs — first boot runs database migrations (~60 seconds)
docker compose logs -f server

# 6. Start Caddy once the server is healthy
docker compose up -d caddy

# 7. Verify all services
docker compose ps

# 8. Complete the initial admin setup
# Visit: https://auth.yourdomain.com/if/flow/initial-setup/
# Set your admin username and password.
# This URL is only available before the first admin account is created.

If you need a recovery key

If you're locked out of an existing instance, generate a temporary recovery link:

docker compose exec server ak create_recovery_key 10 akadmin
# Returns a one-time URL valid for 10 minutes for the user "akadmin"

Verify the stack

# All services should be Up and healthy
docker compose ps

# Health check endpoint (Caddy routes this through)
curl -s https://auth.yourdomain.com/healthz/ready
# {"status":"ok"} or HTTP 200

# Resource usage
docker stats --no-stream

Runtime footprint

Measured 2026-05-02 on local Docker (4 vCPU / 8 GB RAM).

ServiceIdle RAMPeak RAM
server (Authentik)250 MB400 MB
worker (Authentik)200 MB300 MB
db (postgres 16)100 MB150 MB
redis (alpine)50 MB60 MB
caddy10 MB15 MB
Total610 MB925 MB

A 4 GB Liquid Web Managed VPS has ample headroom. You can co-locate Authentik with lightweight apps (a static site, a small API) without pressure. If you plan to run Authentik alongside Coolify or another app platform, size up to an 8 GB VPS.

Upgrading Authentik

# Check github.com/goauthentik/authentik/releases for the latest version

# Update the version in .env
AUTHENTIK_TAG=2025.2.0 # example — use the actual latest

# Pull new images (server and worker share the same image)
docker compose pull server worker

# Stop and restart (migrations run automatically on server startup)
docker compose stop server worker
docker compose up -d server worker

# Verify
docker compose ps
docker compose logs server --tail 30

Always read the Authentik release notes before upgrading. Some releases include breaking changes to flow configurations or policy syntax.

Cost vs Okta / Auth0

OktaAuth0 EssentialsAuthentik on Liquid Web
Monthly cost~$2/user/mo~$23/mo (500 MAU)~$30/mo flat (4 GB VPS)
Users / MAUPer-user pricing500 MAU limitUnlimited
OIDC / OAuth2
SAML 2.0✓ (paid plans)
LDAP✓ (add-on)
SCIM provisioning✓ (add-on)
MFA / passkeys
Proxy authentication
Data residencyCloud (US/EU)CloudYour VPS
LicenseProprietaryProprietaryMIT (core)

Prices subject to change. Verify at okta.com/pricing and auth0.com/pricing. Liquid Web VPS pricing at liquidweb.com/vps-hosting/managed-vps/.

The break-even point vs Auth0 Essentials: At 500 MAU, Auth0 Essentials is ~$23/mo. A 4 GB Liquid Web Managed VPS is ~$30/mo, already comparable, and your Liquid Web VPS can run other services alongside Authentik. At 1,000+ MAU (Auth0's next tier, ~$240/mo), Authentik on a VPS is dramatically cheaper.

When this isn't right for you

  • You need a fully managed, zero-ops identity service. Authentik requires you to manage upgrades, backups, and infrastructure. If your team doesn't want that responsibility, Auth0 or Okta are the right choices, and the cost is the price of not running infrastructure.
  • Your organization requires SOC 2 / FedRAMP certified identity. Okta and Azure AD carry certifications that a self-hosted Authentik instance does not. Regulated industries (government, healthcare, financial services) often have compliance requirements that mandate certified third-party providers.
  • You need Authentik to be highly available. This guide runs a single-node setup. For HA, Authentik supports horizontal scaling of the server and worker behind a load balancer, but that requires shared storage (NFS or S3 for the media volume) and a managed or replicated PostgreSQL cluster, a significantly more complex deployment.
  • Your stack is entirely cloud-native (AWS Cognito, GCP Identity Platform). If your apps already live in a single cloud provider, the native identity service may integrate more cleanly with IAM policies and reduce cross-cloud latency.

Exit strategy

# Export your PostgreSQL data
docker compose exec db pg_dump -U authentik authentik > authentik_export.sql

# Back up the media volume (includes TOTP seeds, profile images, custom certificates)
docker compose stop server worker
docker run --rm \
-v authentik_media:/source \
-v $(pwd)/backup:/backup \
alpine \
tar czf /backup/authentik_media_$(date +%Y%m%d).tar.gz -C /source .

docker compose start server worker

Authentik stores all configuration in PostgreSQL: providers, applications, flows, policies, users, groups, and MFA enrollments. A pg_dump captures everything needed to restore or migrate to another server. The media volume holds TOTP secret seeds for enrolled users; without it, MFA-enrolled users will need to re-enroll after migration.

To migrate to a managed identity provider: export SAML/OIDC provider metadata from Authentik's admin UI and import it into your destination IdP. User passwords are hashed (bcrypt) and are not directly portable, so plan a password-reset cycle for users when migrating away.

Frequently Asked Questions

Both containers use the same Docker image but run different processes. The server handles HTTP requests: the web UI, API, and all authentication flows that users interact with in real time. The worker runs background tasks: sending emails, processing SCIM sync jobs, running scheduled tasks, and executing outpost health checks. Both containers must be running for Authentik to function correctly. If the worker is down, authentication still works but background tasks (email delivery, SCIM provisioning, scheduled policies) stop executing.

In the Authentik admin UI: (1) Go to Applications → Providers → Create → OAuth2/OpenID Provider. (2) Set the redirect URI to your app's callback URL. (3) Authentik generates a Client ID and Client Secret. Copy these into your app's OIDC configuration. (4) Create an Application that links to this provider (Applications → Applications → Create). Your OIDC discovery URL will be `https://auth.yourdomain.com/application/o/your-app-slug/.well-known/openid-configuration`. Most frameworks (Next.js, Django, Rails) support OIDC via standard libraries. Point them at this discovery URL.

Yes, this is Authentik's proxy authentication feature. The Authentik outpost (an embedded proxy) sits in front of your app and enforces authentication before forwarding requests. Any app behind a reverse proxy can be protected this way, with no code changes to the app itself. Configure a Proxy Provider in Authentik, deploy the embedded outpost (already bundled in the server image), and update your Caddy config to route through the outpost. This is how you add SSO to tools like Grafana, Portainer, or any internal dashboard.

Back up the PostgreSQL database and the media volume. PostgreSQL holds all configuration, users, groups, flows, and policies. Dump it with `docker compose exec db pg_dump -U authentik authentik > backup.sql`. The media volume holds TOTP seeds and custom certificates. Run database backups daily (cron + pg_dump piped to S3 or Backblaze B2). Back up the media volume weekly or whenever you enroll new MFA devices. Liquid Web Managed VPS includes server-level backups, so confirm your backup schedule covers the Docker volumes.

Common mistakes and fixes

Authentik UI loads but login fails with 'flow not found'.

The initial setup flow must be completed first. Visit `https://auth.yourdomain.com/if/flow/initial-setup/` to create your first admin user. This flow is only available on a fresh install before any admin account exists. If the URL redirects away, the admin account was already created, so try the standard login page instead.

Worker container restarts in a loop with 'redis.exceptions.ConnectionError'.

AUTHENTIK_REDIS__HOST must be set to `redis` (the Docker Compose service name), not `localhost`. Check your `.env` file and confirm the value is exactly `redis`. After correcting it, run `docker compose up -d worker` to restart the worker with the updated env.

SMTP email test fails with connection refused or authentication error.

Check AUTHENTIK_EMAIL__HOST and AUTHENTIK_EMAIL__PORT. Port 587 requires `AUTHENTIK_EMAIL__USE_TLS=true`; port 465 requires `AUTHENTIK_EMAIL__USE_SSL=true` (these are mutually exclusive). Test delivery from inside the container: `docker compose exec server ak test_email your@email.com`. If the host resolves but auth fails, verify the username and password are correct for your SMTP relay (e.g. SendGrid, Postmark, or your host's relay).

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50