How to Deploy Authentik with Caddy for Single Sign-On (SSO) and Multi-Factor Authentication Across Docker Services

IT Specialist configuring Authentik SSO and Caddy Reverse Proxy in a modern DevOps Operations Center
How to Deploy Authentik with Caddy for Single Sign-On (SSO) and Multi-Factor Authentication Across Docker Services 3

As self-hosted infrastructure expands, managing independent user credentials across dozens of containerized services—such as Grafana, Portainer, Proxmox, and internal microservices—rapidly introduces credential sprawl and severe security vulnerabilities. Many homelab and internal applications either lack native Multi-Factor Authentication (MFA), rely on outdated basic authentication, or provide no built-in authorization mechanisms at all.

Authentik is an open-source, enterprise-grade Identity Provider (IdP) that unifies authentication, single sign-on (SSO), and role-based access control. When paired with the modern, high-performance Caddy web server, you can implement robust Forward Authentication. With this pattern, Caddy intercepts incoming HTTP requests, validates user sessions against Authentik before forwarding traffic, and enforces hardware-backed MFA (FIDO2/WebAuthn or TOTP) across all self-hosted web applications—even those with zero native auth capabilities.

In this technical implementation guide, we will deploy a production-ready Authentik stack with PostgreSQL, Redis, and Caddy using Docker Compose, configure Caddy’s forward_auth directive, and secure internal dashboards behind unified identity governance.

Architecture: Forward Authentication Flow

Unlike traditional OpenID Connect (OIDC) or SAML workflows where each application must support federated protocols natively, Forward Auth acts at the reverse proxy layer. Caddy queries Authentik on every request to determine whether the client holds a valid cryptographic session cookie.

+-----------------------------------------------------------------------------+
|                               Client Browser                                |
|                                      |                                      |
|                                      | 1. HTTPS Request (e.g. app.lab.lan)  |
|                                      v                                      |
|  +-----------------------------------------------------------------------+  |
|  | Caddy Reverse Proxy (Ports 80 / 443)                                  |  |
|  | Handles TLS Certificates & Routing                                     |  |
|  +-----------------------------------+-----------------------------------+  |
|                                      |                                      |
|                  2. forward_auth     |                                      |
|                  (Checks Session)    v 4. Authorized Traffic                |
|  +---------------------------------------+   +---------------------------+  |
|  | Authentik Embedded Outpost            |   | Protected Backend App     |  |
|  | (Port 9000 /outpost.goauthentik.io)   |   | (Portainer, Grafana, etc) |  |
|  +-------------------+-------------------+   +---------------------------+  |
|                      |                                                      |
|       Session Lookup | Valid / Invalid Status                               |
|                      v                                                      |
|  +---------------------------------------+                                  |
|  | Authentik Server & Worker             |                                  |
|  | + PostgreSQL (Database)               |                                  |
|  | + Redis (Session & Task Cache)        |                                  |
|  +---------------------------------------+                                  |
+-----------------------------------------------------------------------------+

Step-by-Step Traffic Lifecycle

  • 1. Request Arrival: The client initiates an HTTPS request to an internal application (e.g., https://monitor.example.com).
  • 2. Forward Auth Interception: Caddy pauses the request and submits a sub-request containing the client’s headers and cookies to Authentik’s outpost endpoint (/outpost.goauthentik.io/auth/caddy).
  • 3. Challenge or Validation: If the client lacks an active session, Authentik returns an HTTP 302 Redirect to the Authentik login portal, prompting the user for username, password, and WebAuthn/TOTP. Upon successful challenge completion, a signed session cookie is written.
  • 4. Upstream Proxying: If the session is valid, Authentik returns an HTTP 200 OK along with injected identity headers (e.g., X-authentik-username and X-authentik-email). Caddy then forwards the original request to the backend container.

Step 1: Directory Setup & Cryptographic Secrets

Create an isolated directory structure for Authentik state, database storage, and Caddy configuration files:

sudo mkdir -p /opt/authentik/{database,redis,media,templates,certs,caddy-data,caddy-config}
cd /opt/authentik

Generate high-entropy cryptographic keys for Authentik’s internal encryption and PostgreSQL access. We will store these variables in a secure .env file:

cat <<EOF > .env
# Authentik Core Settings
AUTHENTIK_SECRET_KEY=$(openssl rand -base64 36)
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_DISABLE_STARTUP_ANALYTICS=true

# PostgreSQL Credentials
PG_PASS=$(openssl rand -base64 24)
PG_USER=authentik
PG_DB=authentik

# Domain Configuration
AUTHENTIK_DOMAIN=auth.example.com
BASE_DOMAIN=example.com
EOF

sudo chmod 600 .env

Step 2: Production Docker Compose Configuration

Create the docker-compose.yml file. This definition bundles PostgreSQL 16, Redis 7 (alpine), the Authentik Server (API and web frontend), the Authentik Worker (background tasks and directory synchronization), Caddy, and a demonstration backend service (Whoami) to validate Forward Auth enforcement.

services:
  postgresql:
    image: docker.io/library/postgres:16-alpine
    container_name: authentik-postgres
    restart: unless-stopped
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      interval: 10s
      timeout: 5s
      retries: 5
    volumes:
      - /opt/authentik/database:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: ${PG_PASS}
      POSTGRES_USER: ${PG_USER}
      POSTGRES_DB: ${PG_DB}
    networks:
      - authentik-internal

  redis:
    image: docker.io/library/redis:7-alpine
    container_name: authentik-redis
    restart: unless-stopped
    command: --save 60 1 --loglevel warning
    healthcheck:
      test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
      interval: 10s
      timeout: 3s
      retries: 5
    volumes:
      - /opt/authentik/redis:/data
    networks:
      - authentik-internal

  server:
    image: ghcr.io/goauthentik/server:2024.8.3
    container_name: authentik-server
    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_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING__ENABLED}
    volumes:
      - /opt/authentik/media:/media
      - /opt/authentik/templates:/templates
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - authentik-internal
      - web-proxy

  worker:
    image: ghcr.io/goauthentik/server:2024.8.3
    container_name: authentik-worker
    restart: unless-stopped
    command: worker
    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}
    volumes:
      - /opt/authentik/media:/media
      - /opt/authentik/templates:/templates
      - /var/run/docker.sock:/var/run/docker.sock
    depends_on:
      postgresql:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - authentik-internal

  caddy:
    image: docker.io/library/caddy:2.8-alpine
    container_name: caddy-proxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - /opt/authentik/Caddyfile:/etc/caddy/Caddyfile:ro
      - /opt/authentik/caddy-data:/data
      - /opt/authentik/caddy-config:/config
    depends_on:
      - server
    networks:
      - web-proxy

  # Test application to verify Forward Auth
  demo-app:
    image: traefik/whoami:latest
    container_name: demo-whoami
    restart: unless-stopped
    networks:
      - web-proxy

networks:
  authentik-internal:
    internal: true
  web-proxy:
    driver: bridge

Step 3: Caddyfile Configuration for Forward Authentication

Create the /opt/authentik/Caddyfile. This configuration handles automatic TLS issuance via Let’s Encrypt, passes Authentik’s portal directly, and protects demo.example.com using Caddy’s native forward_auth directive:

{
    email admin@example.com
    admin off
}

# 1. Authentik Web Portal & Outpost Endpoints
auth.example.com {
    reverse_proxy server:9000 {
        header_up Host {host}
        header_up X-Real-IP {remote_host}
        header_up X-Forwarded-For {remote_host}
        header_up X-Forwarded-Proto {scheme}
    }
}

# 2. Protected Application (demo.example.com)
demo.example.com {
    # Forward all requests to Authentik Outpost for authorization
    forward_auth server:9000 {
        uri /outpost.goauthentik.io/auth/caddy
        copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Name
    }

    # Pass traffic to internal upstream once authorized
    reverse_proxy demo-app:80
}

Understanding Caddy’s forward_auth Directives

  • uri /outpost.goauthentik.io/auth/caddy: Specifies the internal sub-request destination where Authentik inspects cookies and authorization tokens.
  • copy_headers: Forwards the authenticated user’s metadata downstream to the protected application. For applications like Grafana, these headers allow automatic user provisioning and role assignment.
  • Automatic Let’s Encrypt: Caddy automatically requests and renews valid zero-configuration ACME certificates for both auth.example.com and demo.example.com.

Step 4: Launching and Initializing Authentik

Start the stack in detached mode:

docker compose up -d

Track the database migrations and server initialization:

docker compose logs -f server worker

Once the logs show Starting authentik server..., navigate to the initial setup URL in your browser:

https://auth.example.com/if/flow/initial-setup/

Set a secure password for the primary administrator account (akadmin). Once authenticated, you will enter the comprehensive Authentik Admin Management Interface.

Step 5: Configuring Applications and Proxy Providers

To enforce Forward Authentication on demo.example.com, we must create a corresponding Provider and Application in Authentik:

1. Create the Proxy Provider

  • Navigate to Applications > Providers > Create.
  • Select Proxy Provider and click Next.
  • Configure the provider fields:
    • Name: Demo App Provider
    • Authorization flow: default-provider-authorization-implicit-consent
    • Forward auth (single application): Checked
    • External host: https://demo.example.com
  • Click Finish.

2. Create the Application

  • Navigate to Applications > Applications > Create.
  • Configure the application parameters:
    • Name: Demo Whoami
    • Slug: demo-whoami
    • Provider: Select Demo App Provider
  • Click Create.

3. Bind Provider to the Embedded Outpost

Authentik manages Forward Auth endpoints via Outposts. By default, Authentik runs an authentik Embedded Outpost inside the server container:

  • Navigate to Applications > Outposts.
  • Edit the authentik Embedded Outpost.
  • Under Applications, ensure both authentik and Demo Whoami are selected.
  • Click Update.

Step 6: Enforcing Hardware MFA (FIDO2 / WebAuthn & TOTP)

One of Authentik’s strongest capabilities is mandating multi-factor authentication before any access to proxy-protected services is granted:

  • Open Flows and Stages > Flows > default-authentication-flow.
  • Under Stage Bindings, verify that default-authentication-mfa-validation is bound following password validation.
  • In the User Settings menu (top right user profile > User Settings), users can enroll hardware security keys (YubiKey / FIDO2) or authenticator apps (TOTP).
  • Any access attempt to https://demo.example.com without an active, MFA-verified session will immediately bounce to the challenge stage.

Troubleshooting Common Forward Auth Issues

Issue 1: Infinite Redirect Loop Between Caddy and Authentik

Symptom: Attempting to access https://demo.example.com results in rapid URL flickering between the app and Authentik, eventually terminating with an HTTP 414 (Request-URI Too Long) or browser redirect limit error.

Root Cause: The session cookie domain is misconfigured, preventing the browser from presenting the authenticated session cookie to the application’s subdomain.

Resolution: In Authentik Admin, open Applications > Providers > edit your Proxy Provider > expand Advanced protocol settings. Ensure the Cookie domain is set to your root parent domain (e.g., example.com with no leading dot or port), allowing cookie inheritance across all subdomains.

Issue 2: Authentik Database Migration Lockout on First Boot

Symptom: The Authentik server container continually restarts or logs django.db.utils.OperationalError: could not connect to server: Connection refused.

Root Cause: Authentik attempted to apply database schemas before the PostgreSQL container completed initialization.

Resolution: Ensure the docker-compose.yml file utilizes formal Docker healthchecks on the postgresql service, and that depends_on in the server service specifies condition: service_healthy.

Issue 3: Upstream Headers Missing or Stripped

Symptom: The application opens, but user authentication variables (e.g., X-Authentik-Username) are absent in the application environment.

Root Cause: The Caddy forward_auth block omitted the copy_headers directive, or the upstream application ignores untrusted incoming proxy headers.

Resolution: Explicitly declare the headers to mirror in your Caddyfile:

    forward_auth server:9000 {
        uri /outpost.goauthentik.io/auth/caddy
        copy_headers X-Authentik-Username X-Authentik-Email X-Authentik-Name
    }

Conclusion: Centralized Zero Trust for Homelabs

Combining Caddy’s lightweight reverse proxy with Authentik’s forward authentication brings enterprise-grade Zero Trust architecture to self-hosted environments. You no longer need to rely on the disparate, unvetted authentication mechanisms of individual container projects. Every HTTP request is strictly authenticated, logged, and shielded with hardware-backed Multi-Factor Authentication before it ever reaches your application backends.

For applications that support standard OpenID Connect (such as Nextcloud, Proxmox, and Grafana), you can easily transition from simple Forward Auth to native OIDC providers inside Authentik, enabling seamless single-click login with granular user claims and group synchronization.