How to Deploy Authelia on a VPS
Updated Sep 2026
verified on Ubuntu 26.04 · Sep 2026Put Authelia in front of your self-hosted apps on a VPS. One small container adds a login portal and two-factor authentication through Caddy's forward_auth, with secrets kept in files and users in a hashed YAML database.
- A small VPS. Authelia itself idles at about 30 MB of RAM
- A fresh Ubuntu 24.04 or 26.04 server with root/sudo SSH access
- A domain you control, with subdomains for the portal and each app it protects (Authelia needs a real domain for its session cookie, not a bare IP)
- Docker Engine + Compose installed (see the base guide below)
What Authelia is
Authelia is an authentication and authorization server that sits beside your reverse proxy. When a request comes in for a protected app, the proxy first asks Authelia whether that visitor is allowed in. If the visitor has no valid session, Authelia sends them to its login portal. There they sign in with a password and a second factor (TOTP, WebAuthn or push). After that the request goes through to the app. The app itself never handles a login. This pattern is called forward auth.
Authelia is written in Go and licensed Apache-2.0. On our test box, measured idle, it used about 30 MB of RAM and about 500 MB of disk for the image, so it can share a VPS with the apps it protects. It is a gatekeeper, not a full identity platform: users live in a YAML file or an LDAP directory, not in a web admin UI. If you want a web UI for user management, or a full OIDC/SAML provider for many applications, compare Authelia vs Authentik and Pocket ID vs Authelia before you start.
Server sizing
The catalog lists Authelia's minimum at 256 MB RAM, and our measurement confirms it needs little. Size the box for the apps you are protecting, not for Authelia. A 1 GB instance can run Authelia, Caddy and a couple of small apps. Disk use is the image plus a small SQLite database.
Prepare the server
This guide assumes Docker Engine, the Compose plugin and a ufw firewall are
set up. If they aren't, start with
Docker & Compose on Ubuntu.
Only SSH and the reverse proxy are public. Authelia's port stays on the loopback interface:
sudo ufw allow OpenSSH
sudo ufw allow 80
sudo ufw allow 443
sudo ufw status verbose
DNS. Authelia's session cookie is scoped to a domain. The portal and every
protected app must share a parent domain, for example auth.example.com,
app.example.com and wiki.example.com under example.com. Create A
records for the portal and each app, all pointing at this server.
Paid link — we earn a commission if you shop through it.
Install Authelia
Create the project directory, with separate folders for configuration and secrets:
mkdir -p ~/authelia/config ~/authelia/secrets && cd ~/authelia
Secrets
Authelia needs three random secrets:
- the key that signs password-reset tokens;
- the session secret;
- the storage encryption key, which encrypts sensitive columns such as TOTP seeds in the database.
Authelia's documentation recommends loading them from files rather than putting them in the configuration. Generate each one once:
for s in jwt session storage; do
[ -f secrets/$s ] || openssl rand -hex 64 > secrets/$s
done
chmod 600 secrets/*
ls -l secrets
Back up secrets/storage with the database. Without it, the encrypted data
in the database can't be read.
The users database
With the file backend, users live in users_database.yml, and passwords are
stored as Argon2 hashes. The Authelia image can create the hash. This block
generates a random admin password, hashes it, writes the file, and prints the
password once. It only runs if the file doesn't exist yet:
if [ ! -f config/users_database.yml ]; then
ADMIN_PASS=$(openssl rand -base64 18)
HASH=$(docker run --rm authelia/authelia:4.39 authelia crypto hash generate argon2 --password "$ADMIN_PASS" | sed -n 's/^Digest: //p')
cat > config/users_database.yml <<EOF
users:
admin:
disabled: false
displayname: 'Admin'
password: '$HASH'
email: 'admin@example.com'
groups:
- 'admins'
EOF
echo "Authelia admin password (save it in your password manager now): $ADMIN_PASS"
fi
grep -c argon2id config/users_database.yml
To add users later, run
docker run --rm authelia/authelia:4.39 authelia crypto hash generate argon2
interactively, add another entry under users:, and restart the container
with docker compose restart authelia. Authelia doesn't pick up changes to the
file on its own unless you set watch: true under authentication_backend.file.
Configuration
Write the main configuration file. Replace example.com with your domain before
going live:
cat > config/configuration.yml <<'YAML'
server:
address: 'tcp://:9091'
log:
level: 'info'
totp:
issuer: 'example.com'
authentication_backend:
file:
path: '/config/users_database.yml'
access_control:
default_policy: 'deny'
rules:
- domain: '*.example.com'
policy: 'two_factor'
session:
cookies:
- name: 'authelia_session'
domain: 'example.com'
authelia_url: 'https://auth.example.com'
storage:
local:
path: '/config/db.sqlite3'
notifier:
filesystem:
filename: '/config/notification.txt'
YAML
The sections that matter most:
access_controlstarts fromdenyand then allows every subdomain withtwo_factor. Rules are checked in order, and the first match wins, so put specific rules, such asbypassfor a public health endpoint orone_factorfor a low-risk app, above the wildcard.session.cookiesis where the domain requirement comes from.domainis the parent domain the cookie covers.authelia_urlis where visitors without a session are sent.notifier: filesystemwrites emails, such as two-factor registration links, to a file instead of sending them. That's fine for a single admin. For more users, replace it with ansmtpblock.
Compose file
cat > docker-compose.yml <<'YAML'
services:
authelia:
image: authelia/authelia:4.39
container_name: authelia
restart: unless-stopped
volumes:
- ./config:/config
- ./secrets:/secrets:ro
environment:
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: /secrets/jwt
AUTHELIA_SESSION_SECRET_FILE: /secrets/session
AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: /secrets/storage
TZ: UTC
ports:
# Loopback only: Caddy talks to Authelia, the internet doesn't.
- "127.0.0.1:9091:9091"
YAML
Start it and wait for the health endpoint:
docker compose up -d
timeout 120 bash -c 'until curl -fsS http://127.0.0.1:9091/api/health; do sleep 3; done'; echo
docker compose logs --tail 20 authelia
{"status":"OK"} means the configuration loaded. Authelia checks its
configuration at startup and refuses to start on errors, so if the health check
never passes, the log says which key is wrong. A secret set both in a file and in
configuration.yml also stops it from starting.
HTTPS + domain via Caddy
Authelia doesn't serve TLS itself here. Caddy does, for the portal and for every protected app. Install Caddy as described in Automatic HTTPS with Caddy, then add one block for the portal and one per protected app:
auth.example.com {
reverse_proxy 127.0.0.1:9091
}
app.example.com {
forward_auth 127.0.0.1:9091 {
uri /api/authz/forward-auth
copy_headers Remote-User Remote-Groups Remote-Email Remote-Name
}
reverse_proxy 127.0.0.1:8081
}
Every request to app.example.com now goes through Authelia first. Replace
127.0.0.1:8081 with wherever your app listens. copy_headers passes the
signed-in user's name, groups and email on to the app. Apps that support
trusted-header SSO can use those headers to log the user in automatically.
If Caddy runs as a container, use service names (authelia:9091) on a
shared Docker network instead of 127.0.0.1.
First login and two-factor setup
Open https://app.example.com. You should be redirected to
https://auth.example.com. Sign in as admin with the password printed during
install. Because the policy is two_factor, Authelia asks you to register a
second factor. With the filesystem notifier, the one-time verification code is
written to a file instead of an email:
cat ~/authelia/config/notification.txt
Enter the code, then scan the TOTP QR code with an authenticator app or register a security key. After that you land on the protected app.
Securing it
- Keep
default_policy: deny. New subdomains stay closed until a rule allows them. - Brute-force protection is on by default. The
regulationsection bans a user after repeated failed logins. The defaults are reasonable; tunemax_retries,find_timeandban_timeif you need to. - Keep port 9091 on the loopback interface. The forward-auth endpoint
trusts the
X-Forwarded-*headers the proxy sends. If anyone else can reach Authelia directly, those headers can be forged. - Use SMTP before you add other users, so password resets and two-factor registrations reach them.
- For more than a handful of users, move them from the YAML file to
LLDAP or another LDAP directory. Authelia supports an
ldapauthentication backend.
Backups
Three things together make a complete backup: the configuration (including
users_database.yml), the SQLite database with registered devices and TOTP
seeds, and the secrets, especially the storage encryption key. Stop the
container for a consistent copy of the database. It restarts in seconds:
cd ~/authelia
docker compose stop
tar czf authelia-backup-$(date +%F).tar.gz config secrets docker-compose.yml
docker compose start
ls -lh authelia-backup-*.tar.gz
The archive contains secrets and password hashes. Encrypt it (for example with
gpg --symmetric) before you copy it off the server.
Upgrades
The compose file pins the 4.39 release line, so pull picks up patch
releases:
cd ~/authelia
docker compose pull
docker compose up -d
To move to a new minor version, back up first, read that release's notes,
then change the tag. Authelia sometimes renames or deprecates configuration keys
between minor versions. It logs a warning for deprecated keys and still starts,
so check docker compose logs authelia after every upgrade.
Troubleshooting
Authelia exits right after starting. Read docker compose logs authelia.
The configuration validator names the exact key. Common causes: a secret defined
both in a file and in configuration.yml, a session.cookies.domain that
doesn't contain the authelia_url host, or a secret file the container can't
read.
Redirect loop between the app and the portal. The cookie domain doesn't cover both hosts, or the site is being reached over plain HTTP. The session cookie is secure-only, so everything must go through Caddy's HTTPS sites.
Every request gets "403 Forbidden" after login. No access_control rule
matches that host, so default_policy: deny applies. Add a rule for the
domain.
Login works, but the app doesn't know who you are. The app has to read the
Remote-User header, and Caddy only passes it if it is listed in
copy_headers. Many apps need a setting that enables trusted-header or proxy
authentication.
Verification + next steps
You're done when:
https://auth.example.comloads with a valid certificate;- visiting a protected app while signed out sends you to the portal;
- signing in requires your second factor;
- the backup archive, with
secrets/storageinside it, is stored off the box.
From here, add a rule and a Caddy block for each app you want to protect. Use
one_factor only where a password alone is acceptable. If you later need apps
to sign in over OIDC rather than forward auth, Authelia also includes an OpenID
Connect provider. For a passkey-only alternative, see
Pocket ID, and for a heavier, all-in-one
identity server see Keycloak.