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.
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:
- Authelia (the auth server)
- Redis (session storage)
- A configuration file with your user database and rules
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:
- Domain:
auth.yourdomain.com - Forward to:
your-server-ip:9091 - Force SSL: Yes
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.