On this page (9)
  1. Side-by-side measured table
  2. Features compared (researched)
  3. When to pick Authelia
  4. When to pick authentik
  5. How much RAM each needs
  6. Migration notes
  7. Compose files
  8. What we did not test
  9. Related

Pick Authelia when RAM is tight: it is one container, it idled at 60 MiB, and under 60 logins it peaked at 626 MiB (highest run 657 MiB) in our lab, medians of three runs. By our sizing rule that is a 1 GB plan, but only 38 MiB under the line, so we would use 2 GB if the box runs anything else. Pick authentik if you need SAML, an LDAP server or an admin web interface, and give it 2 GB: its three containers (server, worker, PostgreSQL) idled at 942 MiB (843 to 1,063) and peaked at 1,092 MiB. The two did not do the same work per login (1 request against 9), so the latency gap is not a head-to-head. The feature table is researched, not tested. The lab had one user, no TLS and no reverse proxy.

Measured readings

Test box: authentik 2026.8.4

Measured in our lab on : 3 full runs, each from a fresh stack. Each figure is the median, with the range over the runs under it. The numbers come from the raw result file.

Idle RAM
942 MiBrange 843 MiB–1.0 GiB · 3 runs all containers, median of 3 runs; each run is the mean over 30 s after the stack settled
Startup peak RAM
978 MiBrange 859 MiB–1.0 GiB · 3 runs highest total between healthy and settled
Peak RAM
1.1 GiBrange 1012 MiB–1.1 GiB · 3 runs under load, median of the run peaks
Settled after
163 srange 158–164 · 3 runs after the first healthy response
Idle CPU
3.4 %range 0.6–3.5 · 3 runs of one core, lab host
Startup
104 ssame when rounded in all 3 runs to first healthy response
App version
2026.8.4
Tested
, 3 runs (compose file verified the same day)
How the figures are made
Each run starts a fresh stack and goes through setup, settling, the idle window and the load step. We publish the median over the runs and the lowest and highest value, because the lab host and some apps vary from run to run. Runs, median and range
Lab host
AMD EPYC 7763 64-Core Processor, 2 cores, 7.8 GiB RAM, x86_64.Ubuntu 24.04.5 LTS, Docker 28.0.4.
Disk for the images
About 2.4 GB with the containerd image store, the default on fresh installs of Docker Engine 29 and later (our estimate: unpacked plus compressed size, plus 10%; not measured on a containerd host). About 1.6 GB with the older overlay2 store (the unpacked column, as measured on this lab host). Plan for the larger figure. How we size disk
How idle was measured
After the stack was healthy and set up, we sampled total RAM until it stayed within max(2% of its mean, 10 MiB) over 60 s (at least 120 s, at most 600 s), then measured idle over the next 30 s. Rule in full
Peak RAM measured
Under load. One user (the admin account), over plain HTTP on the Compose network, from one Python script that plays the part of the reverse proxy (the proxy itself is not in the stack). First 60 password logins, 4 at a time, each from a fresh client with no cookies: the default authentication flow through the flow executor API (identification, password, login; about 6 requests), then one single sign-on handshake with the embedded outpost (3 requests: the outpost's start, the authorize call, the outpost's callback) so that the session can be used behind a forward-auth proxy. Then 3,000 authorisation checks, 8 at a time, spread evenly over the 60 sessions (50 each): GET /outpost.goauthentik.io/auth/traefik with the X-Forwarded-Method, -Proto, -Host and -URI headers and the proxy session cookie, which must answer 200. The Authelia app runs the same script with the same counts (60 logins, 4 at a time; 3,000 checks, 8 at a time). No browser, no MFA, no OAuth/OIDC clients, no LDAP or other sources, no policies beyond the defaults, one user. Just before the measured window (not counted) a pre-flight checks that a wrong password is refused, a right one logs in, an authorised check returns 200 with the user name and a check without a session is redirected.
Per-container figures for authentik: median over 3 runs, with the range
ContainerIdle RAMPeak RAM (under load)Image, compressed (download)Image, unpacked (disk, overlay2 store)
postgresql77 MiBrange 77–78 · 3 runs114 MiBrange 109–115 · 3 runs116 MB294 MB
server563 MiBrange 465–682 · 3 runs676 MiBrange 606–710 · 3 runs383 MB1.3 GB
worker301 MiBrange 301–304 · 3 runs319 MiBrange 315–323 · 3 runs383 MB (same image as server)1.3 GB (same image as server)
Total942 MiBrange 843 MiB–1.0 GiB · 3 runs1.1 GiBrange 1012 MiB–1.1 GiB · 3 runs499 MB1.6 GB
Every run (3)
Total RAM, startup and CPU model of each run of authentik
RunIdle RAMPeak RAMStartup peakStartupLab host CPU
1 (closest to median idle)942 MiB1.1 GiB978 MiB104 sAMD EPYC 7763 64-Core Processor
2843 MiB1012 MiB859 MiB104 sAMD EPYC 7763 64-Core Processor
31.0 GiB1.1 GiB1.0 GiB104 sAMD EPYC 7763 64-Core Processor
Total RAM after the stack became healthy (10 s averages, run 1 of 3)
Total RAM across all containers in 10 s averages, from the moment the stack was healthy until the end of the idle window (200 s). Run 1 of 3, the one closest to the median idle. Highest 10 s average 975 MiB at 80 s, last 942 MiB at 200 s. Dashed line: the stack counted as settled at 163 s.

0 s to 200 s after healthy; the line spans 0 to 975 MiB. Dashed line: settled.

Timeline as a table
Total RAM, 10 s average ending at each time, every 30 s after healthy
Seconds after healthyTotal RAM
30809 MiB
60942 MiB
90942 MiB
120942 MiB
150942 MiB
180942 MiB
200942 MiB

RAM is MiB (220 bytes); image sizes are MB (106 bytes). The unpacked column is what our lab host's Docker reported; see Disk for the images for other setups. Per-container peaks happen at different moments, so they do not add up to the total peak. Medians of separate columns need not add up either.

Lab notes (9)
  • Redis: not used and not in the stack. authentik 2025.10 removed it ("authentik no longer uses Redis at all", https://docs.goauthentik.io/releases/2025.10/); the official 2026.8 compose file (https://docs.goauthentik.io/compose.yml) has PostgreSQL, server and worker only. Caching, sessions, tasks and websocket messaging now run on PostgreSQL, which is why PostgreSQL is part of the footprint.
  • Compose file: the official 2026.8 file with the image tag pinned to 2026.8.4 (the file's own default), PostgreSQL pinned to 16.15-alpine (the file says 16-alpine, the same image today), the ./data, ./certs and ./custom-templates folders turned into named volumes, and the first-start values in .env. The worker keeps the Docker socket mount of the official file.
  • First start without a browser: AUTHENTIK_BOOTSTRAP_PASSWORD, AUTHENTIK_BOOTSTRAP_TOKEN and AUTHENTIK_BOOTSTRAP_EMAIL in .env (https://docs.goauthentik.io/install-config/automated-install/); the published .env has the password, token and secret key lines commented out.
  • The embedded outpost (the forward-auth endpoint) runs INSIDE the server container, so its memory is part of the server's number. A separate proxy outpost container would be one more container.
  • Password hashing: authentik's own settings set no PASSWORD_HASHERS (authentik/root/settings.py at version/2026.8.4), so Django's default applies: PBKDF2 with SHA-256 and, in Django 5.2 (5.2.17 in this image, per its uv.lock), 1,000,000 iterations. The authentik docs do not state the parameters. The second setup step reads the algorithm and iteration count of the admin user's stored hash from the database (never the salt or the hash).
  • Same load as the Authelia app: 60 password logins, 4 at a time, then 3,000 authorisation checks, 8 at a time, spread over the 60 sessions (50 each); the counts are in the test.yaml of both apps and a test fails if they differ. A pre-flight of a few requests (one wrong password, one login, two checks) runs just before the measured window and is not counted.
  • NOT equivalent, on purpose and recorded: (1) one authentik login is the default authentication flow through the flow executor API, about 6 HTTP requests, plus a single sign-on handshake with the embedded outpost (3 requests) so that the session works behind a forward-auth proxy: 9 requests per login against Authelia's 1. (2) authentik's check goes to /outpost.goauthentik.io/auth/traefik (the embedded outpost inside the server container, https://docs.goauthentik.io/add-secure-apps/providers/proxy/server_traefik/) and needs the proxy session cookie the handshake sets; Authelia's check uses the one session cookie. (3) The two apps need very different time for the same counts, so the load window of this app is minutes and Authelia's is seconds. The memory peak is the highest simultaneous sample either way.
  • Why the forward-auth endpoint and not only the flow API for the checks: the outpost endpoint is what a reverse proxy (Traefik, Caddy, nginx) calls for every request to a protected site, as Authelia's /api/authz/forward-auth is. Both were checked to work headless (plain HTTP with X-Forwarded-* headers, no browser).
  • Startup: the server answers its readiness endpoint only after it has applied the database migrations on the first start (about 3 minutes on a 4-vCPU box), and the worker applies the blueprints (default flows, outposts) after that; the setup step waits for them. The stack is busy for several minutes after healthy, which the settle rule waits for.

Measured readings

Test box: Authelia 4.39.28

Measured in our lab on : 3 full runs, each from a fresh stack. Each figure is the median, with the range over the runs under it. The numbers come from the raw result file.

Idle RAM
60 MiBrange 58–60 · 3 runs all containers, median of 3 runs; each run is the mean over 30 s after the stack settled
Startup peak RAM
60 MiBrange 58–63 · 3 runs highest total between healthy and settled
Peak RAM
626 MiBrange 498–657 · 3 runs under load, median of the run peaks
Settled after
123 ssame when rounded in all 3 runs after the first healthy response
Idle CPU
0 %same when rounded in all 3 runs of one core, lab host
Startup
1.2 ssame when rounded in all 3 runs to first healthy response
App version
4.39.28
Tested
, 3 runs (compose file verified the same day)
How the figures are made
Each run starts a fresh stack and goes through setup, settling, the idle window and the load step. We publish the median over the runs and the lowest and highest value, because the lab host and some apps vary from run to run. Runs, median and range
Lab host
AMD EPYC 7763 64-Core Processor, 2 cores, 7.8 GiB RAM, x86_64.Ubuntu 24.04.5 LTS, Docker 28.0.4.
Disk for the images
About 119 MB with the containerd image store, the default on fresh installs of Docker Engine 29 and later (our estimate: unpacked plus compressed size, plus 10%; not measured on a containerd host). About 80 MB with the older overlay2 store (the unpacked column, as measured on this lab host). Plan for the larger figure. How we size disk
How idle was measured
After the stack was healthy and set up, we sampled total RAM until it stayed within max(2% of its mean, 10 MiB) over 60 s (at least 120 s, at most 600 s), then measured idle over the next 30 s. Rule in full
Peak RAM measured
Under load. One user, over plain HTTP on the Compose network, from one Python script that plays the part of the reverse proxy (the proxy itself is not in the stack). First 60 password logins, 4 at a time, each from a fresh client with no cookies: POST /api/firstfactor (one request each, one Argon2id verification each). Then 3,000 authorisation checks, 8 at a time, spread evenly over the 60 sessions (50 each): GET /api/authz/forward-auth with the X-Forwarded-Method, -Proto, -Host and -URI headers and the session cookie, which must answer 200. The authentik app runs the same script with the same counts (60 logins, 4 at a time; 3,000 checks, 8 at a time). No browser, no second factor, no OpenID Connect clients, no LDAP, one user, one protected host. Just before the measured window (not counted) a pre-flight checks that a wrong password is refused, the right one logs in, an authorised check returns 200 with the user name and a check without a session is refused.
Per-container figures for Authelia: median over 3 runs, with the range
ContainerIdle RAMPeak RAM (under load)Image, compressed (download)Image, unpacked (disk, overlay2 store)
authelia60 MiBrange 58–60 · 3 runs626 MiBrange 498–657 · 3 runs28 MB80 MB
Every run (3)
Total RAM, startup and CPU model of each run of Authelia
RunIdle RAMPeak RAMStartup peakStartupLab host CPU
158 MiB498 MiB58 MiB1.2 sAMD EPYC 7763 64-Core Processor
2 (closest to median idle)60 MiB626 MiB60 MiB1.2 sAMD EPYC 7763 64-Core Processor
360 MiB657 MiB63 MiB1.2 sAMD EPYC 7763 64-Core Processor
Total RAM after the stack became healthy (10 s averages, run 2 of 3)
Total RAM across all containers in 10 s averages, from the moment the stack was healthy until the end of the idle window (160 s). Run 2 of 3, the one closest to the median idle. Highest 10 s average 60 MiB at 40 s, last 60 MiB at 160 s. Dashed line: the stack counted as settled at 123 s.

0 s to 160 s after healthy; the line spans 0 to 60 MiB. Dashed line: settled.

Timeline as a table
Total RAM, 10 s average ending at each time, every 30 s after healthy
Seconds after healthyTotal RAM
3060 MiB
6060 MiB
9060 MiB
12060 MiB
15060 MiB
16060 MiB

RAM is MiB (220 bytes); image sizes are MB (106 bytes). The unpacked column is what our lab host's Docker reported; see Disk for the images for other setups. Per-container peaks happen at different moments, so they do not add up to the total peak. Medians of separate columns need not add up either.

Lab notes (8)
  • Redis: not used and not in the stack. The session provider is Authelia's default in-memory one. The Authelia docs (https://www.authelia.com/configuration/session/redis/) recommend Redis for high availability and production; they do not say it is required for one instance. Sessions are therefore lost when the container restarts (users log in again).
  • Minimal configuration for one small instance: users in a YAML file, SQLite storage, the file notifier (password-reset codes are written to /config/notification.txt instead of e-mailed). The docs say the file backend and SQLite prevent running more than one instance.
  • Authelia must be served over HTTPS, 'not optional even for testing' (https://www.authelia.com/integration/prologue/get-started/). The lab has no TLS and no proxy: the script sends X-Forwarded-Proto: https. The domain is example.com (portal auth.example.com, protected site app.example.com); a real deployment needs a real domain and a reverse proxy that does TLS.
  • Authelia checks the clock against an NTP server at start and logs a warning if it cannot reach one (it did not matter for the lab; the check can take a few seconds on a host without outbound UDP/123).
  • Password hashing: Argon2id with Authelia's defaults (variant argon2id, iterations 3, memory 65536 KiB = 64 MiB, parallelism 4, key length 32, salt length 16; https://www.authelia.com/configuration/first-factor/file/, also what `authelia crypto hash generate argon2` uses by default in 4.39.28). The lab user's stored hash was made with those defaults and the setup step reads the parameters back from the stored hash. Every login runs one hash, so concurrent logins multiply the 64 MiB. Authelia's own guidance (https://www.authelia.com/reference/guides/passwords/) lists 'low memory' (t=3, m=65536, p=4) and a 'recommended' profile (t=1, m=2097152 KiB = 2 GiB, p=4) and aims at about 500 ms per hash.
  • Same load as the authentik app: 60 password logins, 4 at a time, then 3,000 authorisation checks, 8 at a time, spread over the 60 sessions (50 each); the counts are in the test.yaml of both apps and a test fails if they differ. A pre-flight of a few requests (one wrong password, one login, two checks) runs just before the measured window and is not counted. It is there and not in the setup step because a password login makes Authelia hold on to its Argon2 memory: with two logins in the setup step the idle figure was 160 MiB, without them 62 MiB (two 1-run development runs).
  • NOT equivalent, on purpose and recorded: one Authelia login is one request (POST /api/firstfactor) and its session cookie is valid for the forward-auth check directly; one authentik login is about 6 requests plus a 3-request single sign-on handshake with the embedded outpost. The two apps need very different time for the same counts, so Authelia's load window is seconds (the peak rests on a handful of 2-second samples; the memory stays at the peak after the hashes finish) and authentik's is minutes.
  • The three secrets (JWT secret, session secret, storage encryption key) are lab-only values in .env; the published .env has them and the password hash commented out.

Side-by-side measured table

Source for every row: Tested, from authentik.json and authelia.json (three runs each, Oct 20, 2026), with the arithmetic in lab/notes/sso-2026-10-20.md and the script that produced it. Each figure is the median of three runs, lowest to highest in brackets. RAM is Docker’s working set (usage minus inactive file cache), sampled every 2 to 3 seconds, so a spike shorter than that is missed. CPU is in percent of one core on the lab host, so 200 means both cores busy.

Both apps ran in their own GitHub Actions job (2 vCPU, 7.8 GiB RAM, Ubuntu 24.04.5, Docker 28.0.4) on an AMD EPYC 7763 in all six runs, so the runner type and CPU model are the same. Each run was a fresh stack: start, wait for health, setup, wait for memory to settle, a 30-second idle window, a short pre-flight of a few requests (not counted), then the load. The load was one Python script playing the reverse proxy over plain HTTP: 60 password logins of one user, 4 at a time, then 3,000 authorization checks (the call a proxy makes for every request), 8 at a time, spread over the 60 sessions. All 60 logins and all 3,000 checks succeeded in all six runs, with no failed request, restart or out-of-memory kill.

Row authentik 2026.8.4 Authelia 4.39.28
Containers 3 (server, worker, PostgreSQL 16.15); no Redis 1 (SQLite, users in a file, sessions in memory)
Idle RAM, stack, MiB 942.0 (843.1 to 1,063.1) 60.1 (57.8 to 60.5)
Idle RAM by container, MiB server 563.4 (464.9 to 681.8), worker 301.2 (301.1 to 304.1), PostgreSQL 77.3 (76.9 to 77.5) 60.1 (57.8 to 60.5)
Load peak RAM, stack, MiB 1,092.0 (1,011.7 to 1,110.3) 626.3 (497.8 to 657.0)
Load peak by container (each its own highest sample), MiB server 676.1 (606.2 to 709.9), worker 318.9 (315.0 to 322.5), PostgreSQL 114.0 (108.6 to 115.3) 626.3 (497.8 to 657.0)
Startup peak, healthy to settled, MiB 977.5 (859.4 to 1,063.5) 60.3 (58.0 to 62.6)
RAM reading used for sizing (highest run), MiB 1,110.3 657.0
Docker stats samples in the load window 74 to 75 over about 145 s 5 over about 7 s
Idle CPU, % of one core 3.43 (0.57 to 3.55) 0.02 (0.02 to 0.02)
Images, distinct, compressed / unpacked, MB 498.6 / 1,604.0 (server and worker share one image: 382.5 / 1,310.0; PostgreSQL 116.1 / 294.0) 28.3 / 79.7
Healthy after docker compose up -d, s 103.6 (103.5 to 104.2) 1.2 (1.2 to 1.2)
Settled after healthy, s 163.3 (158.2 to 164.3) 122.7 (122.7 to 122.7)
HTTP requests per login 9 1
Logins per second, 4 at a time 0.58 (0.58 to 0.58) 9.75 (9.66 to 9.79)
Login latency p50, ms 6,829 (6,788 to 6,894) 359 (357 to 361)
Login latency p95, ms 7,714 (7,501 to 7,925) 799 (739 to 840)
Checks per second, 8 at a time 72.6 (70.8 to 72.6) 3,290 (3,276 to 3,302)
Check latency p50, ms 95.4 (94.5 to 98.8) 2.05 (2.05 to 2.06)
Check latency p95, ms 180.8 (179.6 to 181.4) 5.13 (5.12 to 5.21)
Load phase length, logins then checks, s 103.4 then 41.4 6.2 then 0.9

How to read it.

  • RAM is the headline, and it carries across hosts. Idle authentik used 15.7 times Authelia’s idle RAM (942.0 against 60.1 MiB), and its load peak was 1.7 times Authelia’s (1,092.0 against 626.3 MiB). The server container alone (563.4 MiB) is about 9.4 times Authelia’s whole stack. Latency and CPU are valid for the lab host only.
  • Authelia’s idle figure is for an instance nobody had logged in to. The lab logs in only after the idle window. In two single-run development tests on another host (not published, recorded in lab/apps/authelia/test.yaml), two logins before the idle window raised Authelia’s idle from 62 to 160 MiB. Authelia’s password guide describes this: Go releases the hash memory, but “the operating system may not reclaim the memory until a later time”, which “may cause the appearance of more memory being in use than Authelia is actually actively using”. Plan for the load peak, not the idle figure, once people log in.
  • authentik’s idle range is wide and comes from the server. The server idled at 563.4, 464.9 and 681.8 MiB in the three runs, each flat inside its own idle window. We did not find why.
  • PostgreSQL’s 77 MiB is mostly not heap. The cgroup memory.stat at idle showed 26.8 MiB anonymous and 120.0 MiB of file-backed memory, which includes 37.5 MiB of shared memory. Of that file-backed part, 82.5 MiB was inactive page cache, which the 77 MiB working-set figure already leaves out. The server and worker were almost all anonymous (547.9 of 563.4 and 288.6 of 301.2 MiB). We did not run authentik under a memory limit.
  • Startup. authentik was healthy after 103.6 s. The lab’s notes attribute that to the database migrations of a first start (we did not time them separately), and its worker applies the default flows after that (the lab’s setup step took about 36 s more). Authelia was healthy after 1.2 s. Our settle rule waits at least 120 s after setup, and both stacks settled within about 2 s of it.

Authelia’s peak: what the data shows and what we infer

  • Measured: the peak is anonymous memory. At the sample that set each run’s peak, file cache was 0.3 to 0.4 MiB, under 0.1% of anonymous plus file memory (peak_memory_stat in the result file). The peak is heap, not page cache.
  • Measured: the window is short. The load lasted about 7 s and docker stats took 5 samples per run, so each peak is the highest of 5 readings and the three runs spread widely (497.8, 626.3 and 657.0 MiB). Treat 657.0 MiB as the reading to size for, not as a stable figure.
  • Measured: it did not always fall back. The last sample, about 2 s after the load ended, read 68.1, 588.9 and 656.8 MiB (idle before the load: 57.8, 60.1 and 60.5). We did not sample longer, so we do not know when memory comes back down.
  • Inferred, not proven: four concurrent Argon2 hashes plus Go runtime headroom. Each login hashes with 64 MiB of memory, and the script held 4 logins in flight, which is 256 MiB. Peak minus idle was 440, 566 and 597 MiB, 1.7 to 2.3 times that. The extra is plausibly memory that Go has freed or not yet collected and the operating system has not taken back, which is what Authelia’s password guide describes. The data does not separate the hashes from that effect, and the memory reading (taken 0.1 s after each docker stats sample) does not match the stats sample for sample.

authentik’s login time: what the data shows and what we infer

  • Measured: the stack used nearly all of both cores. During the 103 s of logins the three containers together averaged 191 to 192% of the 200% the host has (median of three runs, from docker stats samples, so approximate): PostgreSQL 103%, server 59%, worker 29%. The check phase was as busy: 185 to 186%, with the server at 91% and PostgreSQL at 84%. The load script ran on the same two vCPUs and is not in these figures.
  • Measured: about 3.3 CPU-seconds per login in authentik, about 0.16 in Authelia. That is CPU percent times the length of the login phase, divided by 60 logins. For authentik it came out at 3.29 to 3.33 in the three runs: PostgreSQL about 1.8, server about 1.0, worker about 0.5. PostgreSQL used most of it, 54%. For Authelia we have only 3 samples inside the 6 s login phase per run (157 to 167%), so 0.16 is a rough figure. Per check authentik used about 0.026 CPU-seconds in total (server 0.013, PostgreSQL 0.012).
  • Measured, but only arithmetic: latency matches throughput. The script keeps 4 logins in flight, so mean latency must be 4 divided by logins per second, and it was within 3% for both apps (authentik 4 / 0.58 = 6.9 s against a measured mean of 6.8 s). This follows from how the load works. It does not say what the time was spent on.
  • Inferred: the CPU set the pace, so the 6.8 s includes waiting for CPU. With 96% of the host’s CPU in use while 4 logins were in flight, more concurrency could not have raised the throughput, and each login queued behind the others for part of its time. How much is queueing we did not measure. A login alone would still need about 3.3 CPU-seconds, which on two cores is at least about 1.7 s if its work spread perfectly over both (also inferred: CPU per login may differ when the host is idle). We did not time a login alone.
  • Inferred: the hash is part of the cost, not all of it. PBKDF2 at 1,000,000 iterations burns CPU, but the server container’s whole share, which includes the hash if it runs there (we did not trace which process hashes), was about 1.0 CPU-second per login for 9 requests. The largest share went to PostgreSQL, which we did not profile. A candidate is that authentik moved caching, the embedded outpost’s session store and WebSocket messages into PostgreSQL in 2025.10, after moving tasks there in 2025.8, with roughly 50% more database connections (2025.10 release notes). We did not test that.

Features compared (researched)

We did not test this table. Each row is Researched from the project’s own documentation or source, read Oct 20, 2026. The lab used only the forward-auth path, with one user and a password.

Feature authentik Authelia Sources
OIDC provider Yes. The OAuth 2.0 provider “supports both OAuth 2.0 and OpenID Connect”; the pricing page lists “OpenID Certified” under Open Source Yes, labeled an “open beta feature” in its configuration docs; OpenID Certified for the Basic, Implicit, Hybrid, Form Post and Config OP profiles. It does not act as an OIDC relying party authentik OAuth2 provider, authentik pricing, Authelia OIDC provider, Authelia README
SAML SAML 2.0 provider Not available. A SAML 2.0 provider is on the roadmap under “planning”; its first stage, choosing a library, is marked in progress (page updated Sep 25, 2026) authentik SAML provider, Authelia roadmap
LDAP LDAP provider that lets other apps authenticate against authentik and search its users and groups; the docs list it among the providers that need an outpost, and an outpost is “a single deployment of an authentik component”. We ran only the embedded outpost, for the proxy provider Connects to an LDAP directory as a user backend (authentication_backend.ldap). We found no feature that serves LDAP to other apps authentik LDAP provider, authentik outposts, Authelia LDAP backend
Proxy / forward auth Proxy provider with three modes: proxy, forward auth for one application, forward auth for a whole domain. The embedded outpost runs inside the server container; a separate proxy outpost is optional Forward-auth, ext-authz and auth-request endpoints; works with nginx, Traefik, Caddy, Skipper, Envoy and HAProxy authentik proxy provider, authentik architecture, Authelia proxy authorization
WebAuthn / passkeys Yes: security keys, platform authenticators and passkeys, enrolled through a setup stage Yes: FIDO2 WebAuthn security keys, passkeys and passwordless login; platform authenticators arrived in v4.39.0 authentik WebAuthn stage, Authelia security keys, Authelia roadmap: WebAuthn
TOTP Yes Yes. It also lists Duo push authentik TOTP stage, Authelia one-time password
Admin interface or config file Admin web interface (users, applications, providers, flows, outposts) No admin interface. Settings are a YAML file and environment variables; users live in a file or an LDAP directory. An administrator dashboard is on the roadmap: design in progress, implementation not started, targeted at v4.40.0 authentik outposts, authentik users, Authelia admin dashboard roadmap
User self-service A user interface for end users. With a recovery flow set on the brand, users reset their own passwords; setup stages can let users enroll authenticators from their user settings A user portal to register several WebAuthn keys, view and revoke devices, and change a password; password reset with email identity verification authentik user operations, Authelia README, Authelia user dashboard roadmap
Licence MIT for most of the repository; authentik/enterprise/ is under a separate Enterprise licence that requires a paid subscription for production use; website/ is CC BY-SA 4.0 Apache License 2.0 authentik LICENSE, authentik enterprise LICENSE, Authelia LICENSE
Paid editions Open Source is free, with community support only. A paid Enterprise tier adds features such as privileged access management, Microsoft Entra ID and Google Workspace integration, and client certificate authentication; see the pricing page for current terms (we do not copy prices here). Enterprise uses the same container images and is switched on with a license key. OIDC, SAML, LDAP and proxy are listed under Open Source None on the pages we read; the README says it is offered “at zero cost” authentik pricing, authentik Enterprise, Authelia README
Language and runtime Python 3.14 with Django 5.2.17, plus Go (a go.mod in the repository); Docker images, Kubernetes via Helm Go; web interface in React 19.3.0; ships as a static binary, a .deb package and a container image, with a Helm chart (beta) for Kubernetes authentik pyproject.toml, authentik go.mod, Authelia go.mod, Authelia web/package.json
Documented host size “At least 2 CPU cores and 2 GB of RAM” None found on the pages we read authentik Docker Compose install

Not tested in our lab: every row above, apart from the hash algorithms and the forward-auth path, which the result files record. We did not start an OIDC client, a SAML service provider, an LDAP outpost, a second factor, a recovery flow or an Enterprise license, and we did not click through either interface.

When to pick Authelia

  • RAM is the limit. One container, 60.1 MiB idle and 657.0 MiB at the highest peak in any run, against 942.0 and 1,110.3 MiB for authentik. By our sizing rule that is the 1 GB tier, with 38 MiB to spare under the line. That is thin: see the sizing section before you choose 1 GB.
  • A proxy asks it a yes or no for each request. In the lab a check took 2.05 ms at the median with 8 in flight, 3,290 per second, against 95.4 ms and 72.6 per second for authentik, on the same CPU model. Both calls are the forward-auth check, but the session stores differ and the script may have been Authelia’s limit (see the callout above), so read it as a lab-host result, not a ranking.
  • You are happy to keep settings in files. There is no admin interface (researched), and sessions sit in memory by default; the docs “highly recommend” Redis for production (session provider docs, read Oct 20, 2026). We did not restart it to see what a restart does to sessions.
  • OIDC for your apps is enough, and you accept that Authelia labels it a beta (researched).

What it cost in our runs: the load peak was 10 times its idle (626.3 against 60.1 MiB) in a 7 s window we sampled 5 times. Features it lacks, per the docs (researched, not tested): SAML, an LDAP server and an administrator interface.

When to pick authentik

  • You need SAML, an LDAP server for other apps, or an admin web interface (researched, not tested). Authelia offers none of the three (docs read Oct 20, 2026).
  • You have 2 GB and 2 CPU cores to give it, with a proxy and little else on the box. By our sizing rule it needs 2 GB, and its own install page asks for 2 CPU cores and 2 GB of RAM (researched).
  • You want self-service flows such as recovery and enrollment built from stages (researched, not tested).
  • You can accept a slower login and check path on a small box. On 2 vCPU it logged in at 0.58 per second with 4 in flight and checked at 72.6 per second with 8 in flight, with the CPU saturated in both phases. A larger host would change that, and we did not test one.

What it cost in our runs: 942.0 MiB idle, 103.6 s to become healthy on a fresh install, 498.6 MB of images to download (1,604.0 MB unpacked), and a server container whose idle RAM ranged from 464.9 to 681.8 MiB between runs. Enterprise features need a paid license, but the protocols listed in the table are in the free edition (researched).

How much RAM each needs

By our sizing rule (how we size) the figure is the highest reading of any run (load peak or startup peak), times 1.5, rounded up to 1, 2, 4, 8, 16 or 32 GB. The extra half is the rule’s allowance for the operating system, Docker, file cache and spikes our sampling missed.

App Highest run reading, MiB Times 1.5, MiB In GiB Tier Under the tier line, MiB
Authelia 4.39.28 657.0 (load peak, run 3) 985.5 0.96 1 GB 38.5
authentik 2026.8.4 1,110.3 (load peak, run 3) 1,665.4 1.63 2 GB 382.6
  • Authelia: the rule gives 1 GB, with almost no room left, so we would only use 1 GB for a box that runs little else. The 657.0 MiB is the highest of 5 samples in a 7 s burst of 4 simultaneous logins. The three runs ranged from 497.8 to 657.0 MiB, and in two of the three, memory had not come back down 2 s after the load ended. After the rule’s allowance, 38 MiB is all that is left under 1 GB for a reverse proxy and anything else on the box. Two things could move the real need either way, and we tested neither. Authelia’s password guide says freed hash memory can stay counted until the system is under memory pressure, so part of the 657 MiB may be reclaimable. Fewer simultaneous logins plausibly mean a lower peak, since each hash takes 64 MiB. The Authelia pages we read (the file backend and the password guide) do not mention a Go memory limit; Go documents GOMEMLIMIT as a soft limit that “makes no guarantees” (Go GC guide), and we did not try it. Our opinion, tied to those numbers: pick 2 GB if the same box runs a proxy and other apps. We did not test a 1 GB host or a container memory limit.
  • authentik: 2 GB is the floor. Its idle median alone was 942.0 MiB and one run idled at 1,063.1 MiB, so 1 GB does not fit. The rule gives 2 GB, which is also what its install page asks for along with 2 CPU cores. Our 2-vCPU host was 96% busy during the login load, so check that a 2 GB plan has 2 vCPUs. With 383 MiB under the tier line, 2 GB suits authentik plus a proxy, not much more.
  • The peak depends on how many logins land together. Authelia’s peak came from 4 hashes in flight and we did not vary that number. More simultaneous logins plausibly mean a higher peak, which we did not test.

Migration notes

Not run in our lab: we did not move users or applications between the two. The lab did find different stored password hashes (Authelia: Argon2id; authentik: PBKDF2-SHA256 with 1,000,000 iterations), so do not assume a stored hash moves across unchanged; plan for password resets or re-enrollment. A cautious pattern, our advice and not a tested procedure: run the new one beside the old on a separate host name, move one application at a time, and keep the old one until every application works.

Compose files

The two files below are the ones the lab ran, on the pinned versions. Neither has TLS or a reverse proxy, and both need one in production (below). Each reads secrets from a .env file next to it, and Compose refuses to start without them. The lab’s secrets are public, so the published .env files have those lines commented out: uncomment each and set your own value, as the comments in the file explain.

What is lab-only. For Authelia: the domain example.com (portal auth.example.com, protected site app.example.com), the single user lab-user, the one_factor rule for app.example.com, the file notifier (password-reset codes are written to a file, not emailed), the in-memory session provider (no Redis) and SQLite. The compose file publishes port 9091 on 127.0.0.1 only. For authentik: the admin email in .env, and everything the lab’s setup step did through the API, which is not in the compose file: it created a forward-auth proxy provider and an application for https://app.example.com and attached them to the embedded outpost. The compose file alone starts an empty authentik. It publishes ports 9000 (HTTP) and 9443 (HTTPS) on all interfaces, so firewall them or bind them to localhost behind your proxy; the localhost change was not run in our lab. The worker mounts the Docker socket, as the official file does; authentik’s install guide says the mount is for deploying outposts automatically, carries security risks, and can be removed if you deploy outposts by hand.

The authentik file is the official 2026.8 file with three changes: PostgreSQL pinned to 16.15-alpine, the ./data, ./certs and ./custom-templates folders turned into named volumes, and first-start values for the admin account in .env (optional, and commented out in the published file), so no browser is needed on the first start. authentik has used no Redis since 2025.10, so there is no Redis service.

authentik 2026.8.4

compose.yamlVerified 2026-10-20 · authentik 2026.8.4Download compose.yaml for authentik
# Verified compose for authentik, tested 2026-10-20 by the SelfHostBench lab (3 runs; the figures are the median).
# Lab host: AMD EPYC 7763 64-Core Processor, 2 vCPU, 7.8 GiB RAM, x86_64, Docker 28.0.4.
# Floating image tags below were replaced by the version resolved at test time;
# the registry digest is in the comment on each image line.
# Measurements: authentik.json (schema 3).
# Companion env file: authentik.env (save it as .env next to this file).
# Secrets are not published: the lab's values are public. In authentik.env:
#   PG_PASS is commented out. Uncomment it and set your own value (the comment above it explains how).
#   AUTHENTIK_SECRET_KEY is commented out. Uncomment it and set your own value (the comment above it explains how).
#   AUTHENTIK_BOOTSTRAP_PASSWORD is commented out. Uncomment it and set your own value (the comment above it explains how).
#   AUTHENTIK_BOOTSTRAP_TOKEN is commented out. Uncomment it and set your own value (the comment above it explains how).

# authentik 2026.8.4 (identity provider and SSO): PostgreSQL, the server and the worker. No Redis.
# This is the official compose file for 2026.8 (https://docs.goauthentik.io/compose.yml, which pins 2026.8.4 as the default of
# AUTHENTIK_TAG; install guide https://docs.goauthentik.io/install-config/install/docker-compose/), with these differences, all marked below:
#   - PostgreSQL is pinned to 16.15-alpine (the official file says 16-alpine, which today is the same image);
#   - ./data, ./certs and ./custom-templates are named volumes here (the official file bind-mounts folders next to the compose file);
#   - the .env file has first-start values for the admin account (AUTHENTIK_BOOTSTRAP_*), so the first start needs no browser.
# Redis: authentik "no longer uses Redis at all" since 2025.10 (https://docs.goauthentik.io/releases/2025.10/), so there is no redis service.
# There is no TLS and no reverse proxy in this file. Put one in front before you expose it.
name: authentik

services:
  postgresql:
    env_file:
      - .env
    environment:
      POSTGRES_DB: ${PG_DB:-authentik}
      POSTGRES_PASSWORD: ${PG_PASS:?database password required}
      POSTGRES_USER: ${PG_USER:-authentik}
    healthcheck:
      interval: 30s
      retries: 5
      start_period: 20s
      test:
        - CMD-SHELL
        - pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}
      timeout: 5s
    # Official file: postgres:16-alpine (a floating tag). 16.15-alpine is the same image today (same digest), pinned so a re-test uses the same PostgreSQL.
    image: docker.io/library/postgres:16.15-alpine  # sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea  (version 16.15, from exec)
    restart: unless-stopped
    volumes:
      - database:/var/lib/postgresql/data

  server:
    command: server
    depends_on:
      postgresql:
        condition: service_healthy
    env_file:
      - .env
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
    image: ghcr.io/goauthentik/server:2026.8.4  # sha256:e7226ae116dac5aa0a5643803e735b010038f492c401e08e16b3bf1ac54c090c  (version 2026.8.4, from exec)
    ports:
      - ${COMPOSE_PORT_HTTP:-9000}:9000
      - ${COMPOSE_PORT_HTTPS:-9443}:9443
    restart: unless-stopped
    shm_size: 512mb
    volumes:
      # Official file: ./data:/data and ./custom-templates:/templates (folders next to the compose file).
      - data:/data
      - custom-templates:/templates

  worker:
    command: worker
    depends_on:
      postgresql:
        condition: service_healthy
    env_file:
      - .env
    environment:
      AUTHENTIK_POSTGRESQL__HOST: postgresql
      AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
      AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
      AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
      AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
    image: ghcr.io/goauthentik/server:2026.8.4  # sha256:e7226ae116dac5aa0a5643803e735b010038f492c401e08e16b3bf1ac54c090c  (version 2026.8.4, from exec)
    restart: unless-stopped
    shm_size: 512mb
    user: root
    volumes:
      # Lets the worker manage Docker outposts. The install guide says to remove this mount (or put a Docker socket proxy
      # in front of it) if you do not want authentik to control Docker.
      - /var/run/docker.sock:/var/run/docker.sock
      # Official file: ./data, ./certs and ./custom-templates.
      - data:/data
      - certs:/certs
      - custom-templates:/templates

volumes:
  database:
    driver: local
  data:
  certs:
  custom-templates:
.envVerified 2026-10-20 · authentik 2026.8.4Download .env for authentik
# PostgreSQL password. Choose your own; the stack does not start without it (the compose file says: database password required).
# Generate one with:  openssl rand -base64 36 | tr -d '\n'   (PostgreSQL does not support passwords longer than 99 characters)
# PG_PASS=

# authentik secret key. Choose your own; the stack does not start without it. Do not change it after the first start: it signs sessions and encrypts stored values.
# A placeholder would be a known key, so the lab's value is not published and this line is commented out.
# Generate one with:  openssl rand -base64 60 | tr -d '\n'
# AUTHENTIK_SECRET_KEY=

# Optional: the password of the default admin user akadmin, read on the FIRST start only (https://docs.goauthentik.io/install-config/automated-install/).
# Leave the three AUTHENTIK_BOOTSTRAP_* lines out and authentik asks for the password in the browser instead, at http://<host>:9000/if/flow/initial-setup/.
# The lab's value is public, so this line is commented out. Choose your own if you want a non-interactive start.
# AUTHENTIK_BOOTSTRAP_PASSWORD=
# Optional: an API token for akadmin, created on the first start only. The value is the token itself, so keep it secret. Choose your own long random string.
# AUTHENTIK_BOOTSTRAP_TOKEN=
AUTHENTIK_BOOTSTRAP_EMAIL=akadmin@selfhostbench.test

Authelia 4.39.28

compose.yamlVerified 2026-10-20 · Authelia 4.39.28Download compose.yaml for Authelia
# Verified compose for Authelia, tested 2026-10-20 by the SelfHostBench lab (3 runs; the figures are the median).
# Lab host: AMD EPYC 7763 64-Core Processor, 2 vCPU, 7.8 GiB RAM, x86_64, Docker 28.0.4.
# Floating image tags below were replaced by the version resolved at test time;
# the registry digest is in the comment on each image line.
# Measurements: authelia.json (schema 3).
# Companion env file: authelia.env (save it as .env next to this file).
# Secrets are not published: the lab's values are public. In authelia.env:
#   AUTHELIA_USER_PASSWORD_HASH is commented out. Uncomment it and set your own value (the comment above it explains how).
#   AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET is commented out. Uncomment it and set your own value (the comment above it explains how).
#   AUTHELIA_SESSION_SECRET is commented out. Uncomment it and set your own value (the comment above it explains how).
#   AUTHELIA_STORAGE_ENCRYPTION_KEY is commented out. Uncomment it and set your own value (the comment above it explains how).

# Authelia 4.39.28 (authentication and authorization server for a reverse proxy): ONE container, SQLite, no Redis.
# The minimal setup for a small self-hoster, from the Authelia docs:
#   - users in a YAML file (authentication_backend.file),
#   - SQLite storage (storage.local),
#   - the file notifier (notifier.filesystem: password-reset codes are written to a file instead of e-mailed),
#   - the default in-memory session provider (session.redis is not configured).
# Docs: https://www.authelia.com/configuration/first-factor/file/ , https://www.authelia.com/configuration/storage/sqlite/ ,
#       https://www.authelia.com/configuration/notifications/file/ , https://www.authelia.com/configuration/session/introduction/
# The configuration and the users file are written inline under `configs:` (Compose 2.23.1 or newer), so there is nothing to create next to this file.
# Authelia has to be served over HTTPS ("This is not optional even for testing"). It does not do TLS here: put a reverse proxy that does in front of it
# (Caddy, Traefik, nginx), and point the proxy's forward-auth at http://authelia:9091/api/authz/forward-auth. No proxy is in this file.
# Replace example.com below with your own domain. Authelia and every protected site must share it: a session cookie is set for ONE domain.
name: authelia

services:
  authelia:
    image: authelia/authelia:4.39.28  # sha256:bd97cff4fcbf715b5ff1f9ae286afbe6033afce385302520b0368122d43a6f54  (version 4.39.28, from exec)
    restart: unless-stopped
    environment:
      # Secrets come from the .env file (Authelia reads AUTHELIA_<KEY> variables). The docs' own example uses Docker secrets and *_FILE variables instead.
      AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET: "${AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET:?set it in .env}"
      AUTHELIA_SESSION_SECRET: "${AUTHELIA_SESSION_SECRET:?set it in .env}"
      AUTHELIA_STORAGE_ENCRYPTION_KEY: "${AUTHELIA_STORAGE_ENCRYPTION_KEY:?set it in .env}"
    volumes:
      # SQLite database (db.sqlite3) and the notification file. Back this up.
      - authelia-config:/config
    configs:
      - source: authelia_configuration
        target: /config/configuration.yml
      - source: authelia_users
        target: /config/users_database.yml
    ports:
      # Localhost only. The reverse proxy on the host sends traffic here.
      - "127.0.0.1:9091:9091"
    # The image defines its own healthcheck (wget on /api/health every 30 s).

configs:
  authelia_configuration:
    content: |
      # The secrets (jwt_secret, session secret, storage encryption key) are not in this file: Authelia reads them from the
      # AUTHELIA_* environment variables above.
      server:
        address: 'tcp://:9091/'
        endpoints:
          authz:
            # The endpoint your reverse proxy calls for every request. Caddy and Traefik use forward-auth; nginx uses auth-request.
            forward-auth:
              implementation: 'ForwardAuth'
      log:
        level: 'info'
      authentication_backend:
        file:
          path: '/config/users_database.yml'
          # Password hashing is the default: Argon2id, 3 iterations, 64 MiB of memory, parallelism 4 (every login uses 64 MiB briefly).
      access_control:
        default_policy: 'deny'
        rules:
          - domain: 'app.example.com'
            policy: 'one_factor'
      session:
        cookies:
          - name: 'authelia_session'
            domain: 'example.com'
            authelia_url: 'https://auth.example.com'
            default_redirection_url: 'https://app.example.com'
      storage:
        local:
          path: '/config/db.sqlite3'
      notifier:
        filesystem:
          filename: '/config/notification.txt'
  authelia_users:
    content: |
      users:
        lab-user:
          disabled: false
          displayname: 'Lab User'
          # Argon2id hash from .env (a new one: docker run --rm -it authelia/authelia:4.39.28 authelia crypto hash generate argon2).
          password: "${AUTHELIA_USER_PASSWORD_HASH:?set it in .env}"
          email: 'lab-user@example.com'
          groups: []

volumes:
  authelia-config:
.envVerified 2026-10-20 · Authelia 4.39.28Download .env for Authelia
# Argon2id hash of the user's password. Authelia never stores the password. A placeholder is not a valid hash and the stack would not start, so this line is commented out and the compose file stops until you set it.
# Make your own (it asks for the password twice):  docker run --rm -it authelia/authelia:4.39.28 authelia crypto hash generate argon2
# Copy the part after 'Digest: ' and keep the single quotes: the hash contains $ signs, which Compose would otherwise read as variables.
# AUTHELIA_USER_PASSWORD_HASH=

# Three random secrets; the stack does not start without them. The lab's values are public, and Authelia would accept a placeholder as a real (known) secret,
# so these lines are commented out. Generate each with:  docker run --rm authelia/authelia:4.39.28 authelia crypto rand --length 64 --charset alphanumeric
# Signs the password-reset links.
# AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET=
# Encrypts the session data. It is only used when the session is stored in Redis, but Authelia refuses to start without it. Generate your own (see above).
# AUTHELIA_SESSION_SECRET=
# Encrypts the sensitive columns of the SQLite database (at least 20 characters). Do not lose it, and do not change it later without the CLI (authelia storage encryption change-key). Generate your own (see above).
# AUTHELIA_STORAGE_ENCRYPTION_KEY=

In production: a proxy with TLS

Not run in our lab. Both apps expect a reverse proxy in front. Point the proxy’s forward-auth at http://authelia:9091/api/authz/forward-auth for Authelia (the endpoint our script called) or at the embedded outpost’s /outpost.goauthentik.io/auth/traefik path for authentik. The vendors’ own pages, which we read but did not run, are Authelia with Caddy, Authelia with Traefik, authentik with Caddy and authentik with Traefik. The Compose side of putting Caddy in front of a stack, tested with Vaultwarden, is in the Vaultwarden Docker Compose guide.

What we did not test

  • TLS and any reverse proxy. The script played the proxy over HTTP. Both apps need a proxy with TLS in production (Authelia’s docs say HTTPS “is not optional even for testing”), and the proxy adds RAM this page did not measure.
  • OIDC and SAML clients, LDAP, RADIUS, second factors, mail, recovery flows. Each adds work; none ran.
  • More than one user, and concurrency other than 4 logins and 8 checks. A single login on an idle stack was not timed. Authelia’s sessions sit in memory by default and authentik’s data sits in PostgreSQL; we did not measure either with more users, sessions or a bigger database.
  • Memory limits and a 1 GB host.
  • Redis or Valkey for Authelia sessions, and a database server other than SQLite. The docs highly recommend Redis in production.
  • A separate authentik proxy outpost container. The forward-auth endpoint ran inside the server container, so its RAM is in the server’s figure.
  • The cost of a password hash alone. We did not time the hashes outside the apps.
  • Long runs. Each stack ran for a few minutes after settling. Memory growth over days is unknown.
  • Other CPUs and Arm. One x86-64 runner type, AMD EPYC 7763.
  • Upgrades, backups and restores of either stack.

Update log

  • First published. authentik 2026.8.4 and Authelia 4.39.28, three runs each on Oct 20, 2026 on the same GitHub Actions runner type and CPU model (AMD EPYC 7763), one user, plain HTTP, no reverse proxy. The feature table is researched from each project's documentation and source, read Oct 20, 2026. TLS, a proxy, OIDC clients, LDAP and second factors were not tested.