← All Guides
advanced

Authelia on Docker: Add Two-Factor Auth to Every Service in Your Homelab

Set up Authelia as a forward-auth middleware in front of Nginx Proxy Manager to protect every homelab service with a single SSO portal and optional 2FA.

Budget Homelab ·
networkingnginx-proxy-managersecurity

Authelia is an authentication and authorization server. Combined with Nginx Proxy Manager, it puts a login portal in front of every service in your homelab — one set of credentials for everything, optional two-factor auth, and no more relying on individual apps’ authentication.

Without Authelia, accessing a service behind NPM requires only the service’s own login (or no login at all, for services that don’t have it). With Authelia, hitting mealie.homelab.lan redirects to auth.homelab.lan for login before the service loads.

This is a more complex setup than the other guides on this site. Read the whole thing before starting.

Prerequisites: Docker, Nginx Proxy Manager running, your own domain in Cloudflare, and Tailscale if you want this to work remotely. See the NPM guide and Tailscale guide first.

I run Authelia at 192.168.1.101:9091.

Overview

Authelia works as a “forward auth” endpoint. When a request hits NPM for a protected service, NPM asks Authelia: “is this person authorized?” Authelia checks for a valid session cookie. If yes, the request proceeds. If no, it redirects to the Authelia login page.

The components:

Create the directory and files

mkdir -p ~/docker/authelia
cd ~/docker/authelia

Authelia needs three files before it will start: configuration.yml, users_database.yml, and a .env file.

The configuration file

configuration.yml:

---
server:
  host: 0.0.0.0
  port: 9091

log:
  level: info

theme: dark

jwt_secret: change-this-to-a-long-random-string

default_redirection_url: https://auth.yourdomain.com

totp:
  issuer: yourdomain.com

authentication_backend:
  file:
    path: /config/users_database.yml
    password:
      algorithm: argon2id
      iterations: 1
      key_length: 32
      salt_length: 16
      memory: 1024
      parallelism: 8

access_control:
  default_policy: deny
  rules:
    - domain: auth.yourdomain.com
      policy: bypass
    - domain: "*.yourdomain.com"
      policy: one_factor

session:
  name: authelia_session
  secret: change-this-to-another-long-random-string
  expiration: 3600
  inactivity: 300
  domain: yourdomain.com
  redis:
    host: authelia-redis
    port: 6379

regulation:
  max_retries: 3
  find_time: 120
  ban_time: 300

storage:
  encryption_key: change-this-to-yet-another-long-random-string
  local:
    path: /config/db.sqlite3

notifier:
  filesystem:
    filename: /config/notification.txt

Replace all yourdomain.com instances with your actual domain.

The three secrets (jwt_secret, session.secret, storage.encryption_key) need to be long random strings. Generate them:

openssl rand -hex 32  # run three times, use each output for one secret

Access control rules: The config above requires single-factor auth (username/password) for all services. To require 2FA for specific services:

access_control:
  default_policy: deny
  rules:
    - domain: auth.yourdomain.com
      policy: bypass
    - domain: proxmox.yourdomain.com
      policy: two_factor   # Require TOTP for Proxmox
    - domain: "*.yourdomain.com"
      policy: one_factor   # Single factor for everything else

The users database

users_database.yml — create the file with a placeholder hash, then update it:

---
users:
  aaron:
    displayname: "Aaron Beebe"
    password: "$argon2id$v=19$m=1024,t=1,p=8$PLACEHOLDER"
    email: [email protected]
    groups:
      - admins

You need a real hashed password. Generate one using the Authelia Docker image:

docker run authelia/authelia:latest authelia hash-password yourpassword

Replace the PLACEHOLDER with the output hash. Keep the format exact — Authelia is picky about this.

The Docker Compose file

docker-compose.yml:

services:
  authelia:
    image: authelia/authelia:latest
    container_name: authelia
    restart: unless-stopped
    ports:
      - "9091:9091"
    volumes:
      - ./config:/config
    environment:
      TZ: America/New_York

  authelia-redis:
    image: redis:alpine
    container_name: authelia-redis
    restart: unless-stopped
    volumes:
      - ./redis:/data

Move your config files into place:

mv configuration.yml users_database.yml ./config/

Start it:

docker compose up -d
docker logs authelia  # Watch for startup errors

Common errors: misconfigured YAML (check indentation), wrong paths, placeholder secrets not replaced.

Set up the Authelia proxy host in NPM

Create a proxy host for Authelia itself:

Test it: navigate to https://auth.yourdomain.com. You should see the Authelia login page.

Log in with the username and password from users_database.yml. This confirms Authelia is working before you protect other services.

Add forward auth to protected services

In NPM, edit any proxy host you want to protect. Go to the Advanced tab and add:

location /authelia {
    internal;
    set $upstream_authelia http://your-server-ip:9091/api/verify;
    proxy_pass_request_body off;
    proxy_pass $upstream_authelia;
    proxy_set_header Content-Length "";

    proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Forwarded-Uri $request_uri;
    proxy_set_header X-Forwarded-Ssl on;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_cache_bypass $cookie_session;
    proxy_no_cache $cookie_session;
    proxy_buffers 4 32k;
    client_body_buffer_size 128k;
    proxy_buffer_size 128k;
}

location / {
    set $upstream_app your-service-ip;
    set $upstream_port your-service-port;
    set $upstream_proto http;
    proxy_pass $upstream_proto://$upstream_app:$upstream_port;

    auth_request /authelia;
    auth_request_set $target_url $scheme://$http_host$request_uri;
    auth_request_set $user $upstream_http_remote_user;
    auth_request_set $groups $upstream_http_remote_groups;
    proxy_set_header Remote-User $user;
    proxy_set_header Remote-Groups $groups;
    error_page 401 =302 https://auth.yourdomain.com/?rd=$target_url;

    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Replace your-service-ip and your-service-port with the actual backend values. Clear the existing Forward Hostname/IP and Forward Port fields — the advanced config handles routing now.

Setting up 2FA (TOTP)

Log into the Authelia portal at auth.yourdomain.com. Under your account settings, find One-Time Password and follow the setup flow. Authelia generates a QR code you scan with an authenticator app (Google Authenticator, Bitwarden Authenticator, etc.).

Once TOTP is set up, services with policy: two_factor in your access control rules will require both password and TOTP code.

Troubleshooting

Authelia won’t start: Check logs with docker logs authelia. Usually a YAML syntax error in configuration.yml or a file permission issue on /config.

Login loop (redirect keeps returning to auth page): The session cookie domain probably doesn’t match. Make sure session.domain in configuration.yml matches your actual domain exactly.

“Internal Server Error” on auth page: Redis connection issue. Check that authelia-redis is running: docker ps | grep redis.

Protected service accessible without login: The NPM advanced config isn’t correctly applied. Double-check the auth_request /authelia; line is inside the location / block and that you saved the NPM configuration.

Once Authelia is running cleanly, you have centralized auth for every service. Add new services by applying the NPM advanced config snippet — no Authelia configuration changes needed unless you want different policies for different services.