Skip to Content

Production Guide: Deploy Authentik with Docker Compose + Caddy + PostgreSQL + Redis on Ubuntu

Self-hosted SSO and Identity Provider with OIDC, SAML 2.0, and MFA

Managing user access across multiple self-hosted applications is one of the most error-prone operational tasks teams face. Hardcoding passwords, duplicating user databases across services, and manually revoking access when someone leaves are all symptoms of the same underlying problem: no centralized identity layer. Authentik solves this with a production-ready open-source Identity Provider (IdP) that brings SSO via OIDC and SAML 2.0, multi-factor authentication, fine-grained policy enforcement, and a visual flow editor to your self-hosted stack. This guide walks you through a production-grade deployment using Docker Compose, Caddy for automatic HTTPS, PostgreSQL for persistent storage, and Redis for session caching — the same stack used in real-world deployments protecting dozens of internal services.

Architecture and flow overview

Authentik runs as two cooperating processes: the server (Django application exposing the web UI, admin console, and OIDC/SAML endpoints) and the worker (Celery process that handles background tasks such as outpost synchronization, email delivery, and scheduled jobs). Both processes share a PostgreSQL database and a Redis instance. Caddy sits in front and terminates TLS, proxying traffic to the Authentik server container over HTTP on port 9000.

When a user attempts to log in to an application integrated with Authentik (such as Grafana or Nextcloud), the application redirects to the Authentik authorization endpoint. Authentik authenticates the user (checking password, TOTP, or hardware key), evaluates policy bindings, and issues an authorization code. The application exchanges the code for an ID token and access token, completing the OIDC flow. The user sees only a single login prompt; Authentik handles the rest transparently.

Prerequisites

  • Ubuntu 22.04 or 24.04 VPS with at least 2 CPU cores and 2 GB RAM (4 GB recommended for full feature use)
  • Docker Engine 24+ and Docker Compose v2 installed
  • A domain name with DNS A record pointing to the server (e.g., auth.example.com)
  • Caddy v2 installed and accessible on ports 80 and 443
  • UFW or equivalent firewall allowing ports 80, 443, and SSH only
  • SMTP relay credentials if you want password-reset and enrollment emails

Step-by-step deployment

1. Create the project directory and environment file

Create a dedicated directory and generate the required secrets. Authentik requires a secret key and a PostgreSQL password at minimum.

mkdir -p /opt/authentik && cd /opt/authentik

# Generate a strong secret key
openssl rand -hex 32 > .secret_key
echo "AUTHENTIK_SECRET_KEY=$(cat .secret_key)"

# Generate PostgreSQL password
openssl rand -hex 24 > .pg_pass
echo "PG_PASS=$(cat .pg_pass)"

Create the .env file with all runtime settings:

cat > /opt/authentik/.env << EOF
# Authentik
AUTHENTIK_SECRET_KEY=$(cat /opt/authentik/.secret_key)
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_EMAIL__HOST=smtp.example.com
AUTHENTIK_EMAIL__PORT=587
AUTHENTIK_EMAIL__USE_TLS=true
[email protected]
AUTHENTIK_EMAIL__PASSWORD=your_smtp_password
[email protected]

# PostgreSQL
PG_PASS=$(cat /opt/authentik/.pg_pass)
PG_USER=authentik
PG_DB=authentik

# Redis (no password needed for localhost-only Redis)
AUTHENTIK_REDIS__HOST=redis

# Set your external domain here
AUTHENTIK_HOST=https://auth.example.com
AUTHENTIK_HOST_BROWSER=https://auth.example.com
EOF

2. Write the Docker Compose file

cat > /opt/authentik/docker-compose.yml << 'EOF'
version: "3.9"

services:
  postgresql:
    image: postgres:15-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: ${PG_USER}
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_DB: ${PG_DB}
    volumes:
      - pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${PG_USER} -d ${PG_DB}"]
      interval: 10s
      timeout: 5s
      retries: 5

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

  server:
    image: ghcr.io/goauthentik/server:2024.12
    restart: unless-stopped
    command: server
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
      AUTHENTIK_HOST: ${AUTHENTIK_HOST}
    env_file:
      - .env
    ports:
      - "127.0.0.1:9000:9000"
    volumes:
      - media:/media
      - custom_templates:/templates
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy

  worker:
    image: ghcr.io/goauthentik/server:2024.12
    restart: unless-stopped
    command: worker
    user: root
    environment:
      AUTHENTIK_REDIS__HOST: redis
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER}
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY}
    env_file:
      - .env
    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

volumes:
  pg_data:
  redis_data:
  media:
  certs:
  custom_templates:
EOF

3. Configure Caddy

Add a block to your /etc/caddy/Caddyfile for the Authentik subdomain. Caddy will obtain a Let's Encrypt certificate automatically on first request.

auth.example.com {
    reverse_proxy 127.0.0.1:9000

    # Optional: increase timeouts for LDAP sync and large file uploads
    transport http {
        dial_timeout 10s
        response_header_timeout 300s
    }

    # Security headers
    header {
        Strict-Transport-Security "max-age=31536000; includeSubDomains"
        X-Content-Type-Options nosniff
        X-Frame-Options SAMEORIGIN
        Referrer-Policy strict-origin-when-cross-origin
    }
}
caddy reload --config /etc/caddy/Caddyfile
# Verify no syntax errors
caddy validate --config /etc/caddy/Caddyfile

4. Start the stack and complete initial setup

cd /opt/authentik
docker compose pull
docker compose up -d

# Watch logs until healthy
docker compose logs -f server worker

Once the server container logs show Listening on :9000, open https://auth.example.com/if/flow/initial-setup/ in your browser. Set the admin username and password. This flow only appears once; subsequent visits go to the normal login page.

Configuration and secrets handling

Never commit .env, .secret_key, or .pg_pass to version control. Set restrictive permissions immediately:

chmod 600 /opt/authentik/.env /opt/authentik/.secret_key /opt/authentik/.pg_pass
chown root:root /opt/authentik/.env

For teams using a secrets manager (Vault, Infisical, or AWS Secrets Manager), inject secrets as environment variables at container start time via env_file or Docker secrets mounts rather than storing them on disk. Rotate AUTHENTIK_SECRET_KEY by generating a new value, updating .env, and running docker compose up -d — active sessions will be invalidated, so schedule rotations during a maintenance window.

To integrate an application (for example Grafana), navigate to Applications → Providers → Create → OAuth2/OpenID Provider. Set the redirect URI to https://grafana.example.com/login/generic_oauth, copy the Client ID and Client Secret, and configure Grafana's grafana.ini with the Authentik discovery URL: https://auth.example.com/application/o/grafana/.well-known/openid-configuration.

Verification

# Confirm all containers are healthy
docker compose -f /opt/authentik/docker-compose.yml ps

# Check Authentik server responds
curl -s -o /dev/null -w "%{http_code}" https://auth.example.com/if/flow/default-authentication-flow/

# Check PostgreSQL reachability from the server container
docker compose -f /opt/authentik/docker-compose.yml exec server   python -c "import django; django.setup(); from django.db import connection; print(connection.ensure_connection())"

# Check Redis
docker compose -f /opt/authentik/docker-compose.yml exec redis redis-cli ping

Log in at https://auth.example.com/if/admin/ with your admin credentials. Confirm the Dashboard shows green status for the worker and outpost processes.

Common issues and fixes

Server crashes with django.db.utils.OperationalError: could not translate host name "postgresql": The server container started before PostgreSQL passed its healthcheck. Run docker compose restart server after the database is fully up, or increase the depends_on startup timeout.

Initial setup flow never appears: Authentik only shows it if no admin user exists. If the URL redirects to the login page, an admin was already created. Reset with docker compose exec server ak create_admin_group --password newpassword.

Caddy returns 502 Bad Gateway: The Authentik server container is not listening on port 9000, or the ports mapping uses a container-network address Caddy cannot reach. Confirm with curl http://127.0.0.1:9000/if/flow/default-authentication-flow/ from the host. If this fails, check docker compose logs server for startup errors.

Worker not processing tasks: The worker requires access to /var/run/docker.sock for outpost management. If running in a rootless Docker environment, either adjust the socket path or run the worker with user: root as shown in the Compose file. Check docker compose logs worker for permission errors.

SMTP emails not sending: Verify SMTP credentials with docker compose exec server ak test_email [email protected]. Common issues are wrong port (587 vs 465), missing TLS flags, or firewall blocking outbound SMTP from the host.

FAQ

What is the difference between Authentik and Authelia?

Both are open-source authentication proxies, but they serve different use cases. Authelia is primarily a forward-auth middleware for simple username/password + MFA flows in front of existing applications. Authentik is a full Identity Provider with a visual flow editor, OIDC and SAML 2.0 support, user provisioning via SCIM and LDAP outposts, and fine-grained policy bindings — it is significantly more capable but also more resource-intensive. Use Authelia when you need a lightweight gate in front of a few services; use Authentik when you need SSO, just-in-time provisioning, and enterprise-grade policy enforcement.

Can I use Authentik as an LDAP server for legacy applications?

Yes. Authentik ships a built-in LDAP outpost that exposes a virtual LDAP directory backed by Authentik users and groups. Deploy the outpost container and configure your legacy application (such as Nextcloud, Jenkins, or Grafana) to use LDAP with the outpost address. Password changes sync instantly since the outpost is just a protocol adapter over the live Authentik database — there is no separate LDAP sync job to schedule.

How do I set up TOTP (Google Authenticator) for all users?

In the Authentik admin console, go to Flows and Stages → Stages → Create → Authenticator TOTP Stage. Add it to your default authentication flow after the password stage using a binding with order 20. Users will be prompted to enroll their TOTP device on next login. For enforcement, create a policy that checks request.user.ak_groups.filter(name='no-mfa-required').exists() == False and bind it as a policy binding on the TOTP stage.

What happens to active sessions when I upgrade Authentik?

Sessions are stored in PostgreSQL, not in the container filesystem, so they survive container restarts and upgrades. The upgrade procedure is: update the image tag in docker-compose.yml, run docker compose pull, then docker compose up -d. The server and worker containers will restart with zero data loss. For major version upgrades, read the Authentik release notes — schema migrations run automatically on startup, but some major versions have documented one-time migration steps.

Can I enable SSO for applications that only support SAML, not OIDC?

Yes. Authentik has a native SAML 2.0 provider. In the admin console, create a new SAML provider, configure the ACS URL and entity ID from your application's SAML metadata, and download Authentik's signing certificate. Upload the certificate to your application as the IdP certificate. Authentik handles signing assertions with your certificate and optionally encrypts them with the service provider's key.

How do I back up and restore Authentik?

Back up two resources: the PostgreSQL database and the media Docker volume (custom certificates, enrollment icons). Take a consistent database dump with docker compose exec postgresql pg_dump -U authentik authentik > authentik_backup.sql. Back up the media volume with docker run --rm -v authentik_media:/data alpine tar czf - /data > media_backup.tar.gz. To restore, import the SQL dump into a fresh PostgreSQL container and extract the media archive before starting the Authentik containers.

Internal links

Talk to us

If you want this deployed with hardened access controls, monitoring standards, and production runbooks tailored to your environment, our team can help end-to-end.

Contact Us

Production Guide: Deploy Seafile with Docker Compose + Caddy + MariaDB on Ubuntu
Self-host a production-grade Dropbox alternative with HTTPS, persistent storage, and team file sync on your own server.