Installing Authelia with Docker Compose: assembling the Authelia, Redis, and database stack, creating a minimal configuration, managing secrets through the .env file, then running and verifying the Authelia portal on port 9091.

After episode 2, where we understood the architecture — the Authelia server, Redis for sessions, a database for storage, and the proxy as the door — in this episode we start taking action: installing Authelia. We'll use Docker Compose as the primary method because it packages all the components (Authelia, Redis, database) into one declarative file — exactly the pattern most commonly used in the homelab and production world. By the end of this episode you'll have a running stack, a minimal configuration, secrets managed through .env, and an Authelia portal accessible in the browser.
Authelia is flexible. The main options:
Create the lab folder and the standard file structure — we'll use this convention throughout the series: config/configuration.yml will be mounted into the container, and .env holds the secrets referenced by docker-compose.yml:
mkdir -p ~/authelia-lab/config
cd ~/authelia-lab
touch docker-compose.yml .env config/configuration.ymlThis is our core file. Note that the secret values use placeholders filled from the .env file:
services:
authelia:
image: authelia/authelia:latest
container_name: authelia
restart: unless-stopped
environment:
AUTHELIA_JWT_SECRET: ${AUTHELIA_JWT_SECRET}
AUTHELIA_SESSION_SECRET: ${AUTHELIA_SESSION_SECRET}
AUTHELIA_STORAGE_ENCRYPTION_KEY: ${AUTHELIA_STORAGE_ENCRYPTION_KEY}
AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: ${AUTHELIA_RESET_PASSWORD_JWT_SECRET}
AUTHELIA_SESSION_REDIS_PASSWORD: ${REDIS_PASSWORD}
volumes:
- ./config:/config
ports:
- "9091:9091"
depends_on:
- redis
networks:
- authelia_net
redis:
image: redis:7-alpine
container_name: authelia-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD}
volumes:
- redis_data:/data
networks:
- authelia_net
volumes:
redis_data:
networks:
authelia_net:
driver: bridgeKey points from this file:
authelia/authelia image mounts ./config — the configuration lives on the host, the container only reads it.9091 is mapped to the host; this port will be used by the portal and the verification endpoint.AUTHELIA_ prefix — a configuration override mechanism we'll discuss in episode 4.depends_on ensures Redis starts first.Never put secrets directly in docker-compose.yml — keep them in .env, make sure it's in .gitignore, and never commit it. Authelia requires secrets of sufficient length (at least 16 bytes for JWT/session secrets, 20 bytes for the storage encryption key), so generate random values with OpenSSL:
AUTHELIA_JWT_SECRET=generate-16-byte-random
AUTHELIA_SESSION_SECRET=generate-16-byte-random
AUTHELIA_STORAGE_ENCRYPTION_KEY=generate-16-byte-random
AUTHELIA_RESET_PASSWORD_JWT_SECRET=generate-16-byte-random
REDIS_PASSWORD=generate-32-char-passwordopenssl rand -hex 32Warning
Authelia refuses to start if a required secret is missing or too short — that's a good built-in protection, not a bug. Always use long random secrets, store them somewhere safe, and never put them in a committed file.
Authelia can't start without configuration.yml. The following minimal version is enough for this episode — no access control and no users yet (we'll build those in episodes 4 and 5), but the portal will be alive:
host: 0.0.0.0
port: 9091
theme: auto
jwt_secret: not-used-with-env
default_redirection_url: https://auth.example.com
access_control:
default_policy: deny
rules: []
session:
name: authelia_session
domain: example.com
expiration: 1h
inactivity: 5m
remember_me_duration: 1M
redis:
host: redis
port: 6379
password: ${REDIS_PASSWORD}
storage:
local:
path: /config/db.sqlite3
notifier:
filesystem:
filename: /config/notifications.txt
authentication_backend:
file:
path: /config/users_database.ymlA few important notes:
host: 0.0.0.0 so the container serves from outside itself; port: 9091 must be consistent with the port mapping in Compose.session.redis.password uses the same placeholder from .env — this value is referenced directly by the configuration, not by Compose.default_policy: deny — all access is denied until there's an explicit rule (we build those in episode 6).authentication_backend.file points to users_database.yml, which we haven't created yet — the file must exist and be valid, or Authelia refuses to start. Create a minimal users: {}.Important
You'll see jwt_secret written as a placeholder in the config, even though its value is injected through the AUTHELIA_JWT_SECRET environment variable. Environment variables always override config file values — this is Authelia's official pattern for keeping secrets out of versioned files. Episode 4 covers this mechanism in full.
docker compose up -d
docker compose ps
docker compose logs authelia
curl -fsS http://localhost:9091/api/health
curl -fsS http://localhost:9091/api/stateOpen http://localhost:9091 in your browser — you'll see the Authelia portal page. With no users and no rules yet, the portal will look "empty" — that's expected; we'll add real users in episode 5 and access rules in episode 6. Clean logs with no errors are a sign of a healthy stack.
127.0.0.1:9092:9091) or stop the conflicting process.session.redis.password with Redis's --requirepass, and make sure indentation is consistent.In episode 3 you've installed Authelia with Docker Compose: assembled the Authelia + Redis stack, managed secrets through the .env file, created a minimal configuration.yml, and verified that the Authelia portal is alive on port 9091 with a responsive health endpoint.
Key takeaways:
.env, not in committed files — and are injected via environment variables prefixed with AUTHELIA_.default_policy: deny is the right security foundation from the start.In the next episode, episode 4, we'll dissect the configuration file structure thoroughly: from theme, jwt_secret, and default_redirection_url to access_control, session, regulation, storage, notifier, and authentication_backend — plus validation with authelia validate-config and secret management via environment variables. See you in episode 4!