Authelia is an open source authentication server that works with your reverse proxy to add a login portal and two-factor authentication (TOTP codes or WebAuthn security keys) in front of any web application, without changing the application. In this tutorial you will run Authelia with Docker Compose on Ubuntu 24.04, serve its portal at https://auth.example.com, and configure Nginx so https://app.example.com is only reachable after a password and a second factor.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with a non-root
sudouser, for example a CubePath VPS. 1 GB of RAM is enough. - Two DNS A records pointing to the server:
auth.example.comfor the Authelia portal andapp.example.comfor the protected application. Both must be subdomains of the same domain, because Authelia's session cookie is set on that parent domain. Replaceexample.comwith your own domain throughout. - Ports 80 and 443 open to the internet.
- The application you want to protect, listening on
127.0.0.1:3000. Step 5 starts a test page if you do not have one yet.
Step 1 - Installing Docker, Nginx and Certbot
Install Docker Engine and the Compose plugin from the Ubuntu repositories, along with Nginx and Certbot:
sudo apt update
sudo apt install docker.io docker-compose-v2 nginx certbot python3-certbot-nginx
Open HTTP and HTTPS in UFW, making sure SSH stays allowed:
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Verify that Docker is running:
sudo systemctl is-active docker
active
Step 2 - Generating secrets
Authelia needs three random secrets: one to sign password reset links, one for the session, and one to encrypt its database (which stores the TOTP seeds). Keep them in files instead of the configuration so they never end up in a copied config or a screenshot:
sudo mkdir -p /opt/authelia/config /opt/authelia/secrets
for name in jwt_secret session_secret storage_encryption_key; do
openssl rand -hex 32 | sudo tee "/opt/authelia/secrets/${name}" > /dev/null
done
sudo chmod 600 /opt/authelia/secrets/*
Check that the three files exist and are only readable by root:
sudo ls -l /opt/authelia/secrets
-rw------- 1 root root 65 Sep 24 12:01 jwt_secret
-rw------- 1 root root 65 Sep 24 12:01 session_secret
-rw------- 1 root root 65 Sep 24 12:01 storage_encryption_key
WarningBack up
storage_encryption_key. If you lose it, Authelia cannot decrypt the stored TOTP and WebAuthn registrations and every user has to enroll again.
Step 3 - Configuring Authelia
Create the main configuration file:
sudo nano /opt/authelia/config/configuration.yml
server:
address: 'tcp://:9091'
log:
level: info
totp:
issuer: example.com
webauthn:
display_name: Example
authentication_backend:
file:
path: /config/users_database.yml
access_control:
default_policy: deny
rules:
- domain: app.example.com
subject:
- 'group:admins'
policy: two_factor
session:
expiration: 1h
inactivity: 15m
cookies:
- domain: example.com
authelia_url: https://auth.example.com
default_redirection_url: https://app.example.com
regulation:
max_retries: 3
find_time: 2m
ban_time: 10m
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notification.txt
What each section does:
access_controldenies everything by default and requires a second factor for members of theadminsgroup onapp.example.com. Rules are evaluated top to bottom and the first match wins.session.cookiessets the session cookie onexample.com, so one login covers every protected subdomain.authelia_urlis the portal address users are redirected to.regulationbans a user for 10 minutes after 3 failed logins within 2 minutes.notifier.filesystemwrites emails (such as the one-time codes used to register a second factor) to a file instead of sending them. It is fine for a first setup; switch tonotifier.smtpfor real users.
Next, create a user. Generate an Argon2 password hash; the command prompts for the password twice:
sudo docker run --rm -it authelia/authelia:4.39 authelia crypto hash generate argon2
Enter Password:
Confirm Password:
Digest: $argon2id$v=19$m=65536,t=3,p=4$Rk1Ln0mS3Cz9E8uJ1aVtZw$m0e9oG6cZ7d1Q2r5u8vX3yT4w1k2n5p8s0a3d6f9g1h
Copy the full Digest value into the users file:
sudo nano /opt/authelia/config/users_database.yml
users:
jsmith:
disabled: false
displayname: 'John Smith'
password: '$argon2id$v=19$m=65536,t=3,p=4$Rk1Ln0mS3Cz9E8uJ1aVtZw$m0e9oG6cZ7d1Q2r5u8vX3yT4w1k2n5p8s0a3d6f9g1h'
email: [email protected]
groups:
- admins
Keep the single quotes around the hash, because it contains $ characters.
Step 4 - Starting Authelia with Docker Compose
Create the Compose file. The secrets are passed through Authelia's _FILE environment variables, and port 9091 is bound to localhost so the service is only reachable through Nginx:
sudo nano /opt/authelia/compose.yaml
services:
authelia:
image: authelia/authelia:4.39
container_name: authelia
restart: unless-stopped
ports:
- "127.0.0.1:9091:9091"
volumes:
- ./config:/config
- ./secrets:/secrets:ro
environment:
TZ: UTC
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: /secrets/jwt_secret
AUTHELIA_SESSION_SECRET_FILE: /secrets/session_secret
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /secrets/storage_encryption_key
The image tag pins the 4.39 release line; check the Authelia releases for newer versions. Validate the configuration before starting, then start the container:
cd /opt/authelia
sudo docker compose run --rm authelia authelia validate-config --config /config/configuration.yml
sudo docker compose up -d
Configuration parsed and loaded successfully without errors.
Check that the service answers on its health endpoint:
curl -s http://127.0.0.1:9091/api/health; echo
{"status":"OK"}
If the container keeps restarting, sudo docker compose logs authelia names the configuration key that is wrong.
Step 5 - Starting a test application
If your real application already listens on 127.0.0.1:3000, skip this step. Otherwise open a second terminal and start a simple page to protect:
mkdir -p ~/testapp && echo "Hello from the protected app" > ~/testapp/index.html
python3 -m http.server 3000 --bind 127.0.0.1 --directory ~/testapp
Step 6 - Configuring Nginx
You need two server blocks: one for the Authelia portal and one for the protected application. Start with the portal:
sudo nano /etc/nginx/sites-available/auth.example.com
server {
listen 80;
listen [::]:80;
server_name auth.example.com;
location / {
proxy_pass http://127.0.0.1:9091;
proxy_set_header Host $host;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
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-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
}
}
For the application, Nginx sends a subrequest to Authelia's /api/authz/auth-request endpoint before every request. If the user is not logged in, Authelia answers 401 with a Location header pointing to the portal, and Nginx turns that into a redirect:
sudo nano /etc/nginx/sites-available/app.example.com
server {
listen 80;
listen [::]:80;
server_name app.example.com;
# Subrequest endpoint, not reachable from outside
location /internal/authelia/authz {
internal;
proxy_pass http://127.0.0.1:9091/api/authz/auth-request;
proxy_set_header X-Original-Method $request_method;
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_set_header Content-Length "";
proxy_set_header Connection "";
proxy_pass_request_body off;
proxy_http_version 1.1;
}
location / {
auth_request /internal/authelia/authz;
# Redirect unauthenticated users to the Authelia portal
auth_request_set $redirection_url $upstream_http_location;
error_page 401 =302 $redirection_url;
# Pass the user identity to the application
auth_request_set $user $upstream_http_remote_user;
auth_request_set $groups $upstream_http_remote_groups;
auth_request_set $name $upstream_http_remote_name;
auth_request_set $email $upstream_http_remote_email;
proxy_set_header Remote-User $user;
proxy_set_header Remote-Groups $groups;
proxy_set_header Remote-Name $name;
proxy_set_header Remote-Email $email;
proxy_pass http://127.0.0.1:3000;
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;
}
}
Enable both sites and check the syntax:
sudo ln -s /etc/nginx/sites-available/auth.example.com /etc/nginx/sites-enabled/
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Authelia only works over HTTPS. Request certificates for both names; Certbot adds the TLS configuration and an HTTP to HTTPS redirect to each server block:
sudo certbot --nginx -d auth.example.com -d app.example.com --redirect
Step 7 - Logging in and registering a second factor
First confirm that an anonymous request is redirected to the portal:
curl -sI https://app.example.com/ | grep -i '^location'
location: https://auth.example.com/?rd=https%3A%2F%2Fapp.example.com%2F&rm=GET
Open https://app.example.com/ in a browser. You land on the Authelia portal. Sign in as jsmith with the password you hashed in Step 3. Because the rule requires two_factor and no device is registered yet, Authelia asks you to register one.
Before it shows a QR code or accepts a security key, Authelia verifies your identity with a one-time code sent by email. With the filesystem notifier, read it from the server:
sudo cat /opt/authelia/config/notification.txt
Date: 2026-09-24 12:20:41 +0000 UTC
Recipient: { John Smith [email protected] }
Subject: Confirm your identity
...
ABCD1234
Enter the code in the portal, then either:
- TOTP: scan the QR code with an authenticator app (Aegis, Google Authenticator, 1Password and similar) and enter the six-digit code, or
- WebAuthn: register a hardware key such as a YubiKey, or a platform passkey, following the browser prompts.
After registration Authelia redirects you to https://app.example.com/, which now shows Hello from the protected app. Sign out at https://auth.example.com/logout and sign in again to confirm that the second factor is requested.
When you finish testing, stop the Python server with Ctrl+C and point proxy_pass in app.example.com to your real application.
Step 8 - Writing access control rules
Real deployments usually mix policies. The four policies are bypass (no login), one_factor (password only), two_factor (password and second factor) and deny. An example for several services:
access_control:
default_policy: deny
rules:
# Health checks and public files do not need a login
- domain: app.example.com
resources:
- '^/health$'
- '^/public/.*$'
policy: bypass
# Admin panels: admins only, with 2FA
- domain: admin.example.com
subject:
- 'group:admins'
policy: two_factor
# Internal tools: any user in the users or admins group, with 2FA
- domain:
- grafana.example.com
- wiki.example.com
subject:
- 'group:users'
- 'group:admins'
policy: two_factor
Each protected hostname needs its own Nginx server block with the same auth_request configuration as app.example.com. After editing the rules, validate and restart:
cd /opt/authelia
sudo docker compose run --rm authelia authelia validate-config --config /config/configuration.yml
sudo docker compose restart authelia
Troubleshooting
Redirect loop between the app and the portal. The session cookie is not reaching the browser for the app's domain. Check that session.cookies[].domain is the parent domain (example.com, not auth.example.com), that both sites use HTTPS, and that authelia_url matches the portal address exactly.
Nginx returns 500 on the protected site. The subrequest cannot reach Authelia. Check curl http://127.0.0.1:9091/api/health and /var/log/nginx/error.log.
Valid TOTP codes are rejected. TOTP depends on the clock. Run timedatectl and make sure System clock synchronized: yes is shown on the server, and that the phone's time is set automatically.
A user lost their TOTP device. Delete the registration so the user can enroll again at the next login:
sudo docker exec authelia authelia storage user totp delete jsmith --config /config/configuration.yml
The user is banned after failed attempts. The ban set by regulation expires after ban_time. Look for regulation messages with sudo docker compose logs authelia.
Conclusion
Authelia now protects app.example.com with a password and a second factor, and a single session covers every subdomain you add behind Nginx. Next, configure the SMTP notifier so users receive codes by email, replace the users file with the LDAP backend to reuse an existing directory, and add Redis for sessions if you run more than one Authelia instance.
