Tech blog
Installing Authentik and Putting My Homelab Services Behind It

I finally got around to doing something I should have done a while ago: putting a proper front door in front of the self-hosted services I actually care about.
For a long time, my setup worked, but it was messy. Some apps were public, some were only reachable internally, some lived behind random reverse proxy rules, and a few had their own local logins that I didn't really want to keep managing forever. It wasn't terrible, but it also wasn't the kind of setup that gives you confidence when you look at it six months later.
So I rebuilt that part of the stack around Authentik.
The goal was simple:
- one place to handle authentication
- fewer publicly exposed services
- cleaner access for internal dashboards and admin panels
- the option to keep selected paths public when I actually need them to be
- something I could keep expanding without reinventing the wheel every weekend

Cloudflare Tunnel handles the public edge, Authentik handles login and policy, and the services themselves stay internal.
Why I picked Authentik
I wanted something that felt at home in a self-hosted environment rather than something that constantly nudged me back towards a managed platform.
What sold me was that Authentik can be installed pretty cleanly with Docker Compose, supports proxy providers out of the box, and has an embedded outpost that removes a lot of the usual friction when you're fronting internal web apps. Once that clicked, the rest of the design got a lot simpler.
I also liked that I didn't need to throw every application directly onto the internet just to make it usable. Cloudflare Tunnel gave me the external entry point, while Authentik became the thing sitting in front of the apps I actually wanted to protect.
Nothing exotic here. I kept it straightforward:
- Authentik running in Docker Compose
- PostgreSQL and Redis as part of the stack
- Cloudflare Tunnel publishing the external hostnames
- Internal services sitting on private addresses
- Proxy providers in Authentik for the apps I wanted to protect

Installing Authentik
I went with the Docker Compose route because it is fast, easy to reason about, and perfectly fine for a small production-style homelab setup.
The rough flow was:
- Download the current compose.yml
- Generate the PostgreSQL password and Authentik secret key
- Bring the stack up
- Run the browser-based initial setup flow
- Create the first admin account and start wiring apps into it
wget https://docs.goauthentik.io/compose.yml
echo "PG_PASS=$(openssl rand -base64 36 | tr -d '\n')" >> .env
echo "AUTHENTIK_SECRET_KEY=$(openssl rand -base64 60 | tr -d '\n')" >> .env
docker compose pull
docker compose up -dOnce the containers were healthy, I opened the initial setup flow and finished the first-run configuration from the browser.
What I liked: the install didn't ask me to invent a whole platform around it first. It was just a clean Docker Compose deployment, then the real work started in the Authentik UI.
I didn't want to forward a pile of ports or leave random web apps hanging out on my WAN IP. Cloudflare Tunnel was the obvious fit for that.
The key idea is simple: instead of exposing the origin directly, cloudflared creates an outbound-only connection to Cloudflare and publishes hostnames from there. That means the apps can stay on internal addresses.
For this setup, both of these public hostnames point to the Authentik server, not directly to the app:
- auth.example.com
- monitor.example.com
That part matters.
When using Authentik's embedded outpost, the request comes into the Authentik server and gets routed by hostname to the correct proxy provider. So rather than aiming monitor.example.com straight at something like http://192.168.69.182:3001, I aimed it at Authentik and let Authentik forward the request to the internal service after authentication.
tunnel: <TUNNEL-UUID>
credentials-file: /home/marius/.cloudflared/<TUNNEL-UUID>.json
ingress:
- hostname: auth.example.com
service: http://192.168.69.10:9000
- hostname: monitor.example.com
service: http://192.168.69.10:9000
- hostname: dashboard.example.com
service: http://192.168.69.10:9000
- service: http_status:404Once I switched to that model, the whole setup made a lot more sense.
How I actually protected the apps
For each service, I created a Proxy Provider in Authentik and pointed it at the internal service URL.
A simplified example:
- External host: https://monitor.example.com
- Internal host: http://192.168.69.182:3001
That gives Authentik the public-facing hostname users hit, plus the internal upstream it should proxy to once the request is allowed.
The nice bit is that it scales well. Once the pattern is in place, adding more apps is mostly repetition instead of invention.
Uptime Kuma is a good example because it has two completely different use cases:
- I want the admin UI protected
- I might still want the status page visible publicly
This is exactly where Authentik's unauthenticated paths become useful.
For Kuma, I allowed only the status-related paths through without login and kept the rest of the app behind Authentik:
^/status/.*
^/assets/.*
^/api/push/.*
^/api/badge/.*
^/api/status-page/heartbeat/.*
^/icon.svg
^/upload/.*
The biggest mistake to avoid
The easiest way to get confused is to point the public hostname directly at the internal application and expect Authentik to somehow still be in the middle.
If you want Authentik to protect the application through a proxy provider, the public hostname needs to land on Authentik's side of the stack, not directly on the app.
That was the moment where things stopped feeling magical and started feeling logical.
What I like about the setup now
The best part is not that it looks fancy. It's that it is easier to reason about.
Now when I add a new service, I already know the questions:
- does it need to be public or private?
- does it need a public status page but a private admin panel?
- is it better behind a proxy provider or native OAuth / OpenID Connect?
- what hostname should it live on?
- what group should have access?
That is a much better place to be than "why does this one container have a direct public port mapping again?"
It wasn't entirely plug-and-play.
- mixing up internal host and external host
- accidentally leaving direct app access available while testing
- forgetting that the embedded outpost is still tied to the Authentik server ports
- needing to think differently about apps that have some paths that should stay public
- dealing with the usual "this works internally but not through the hostname" troubleshooting loop
Sooo yeah...
I like self-hosting a lot more when the setup feels calm.
Not flashy. Not overcomplicated. Just clear.
Cloudflare Tunnel gave me a safe public edge. Authentik gave me a proper identity layer. Together they cleaned up a part of my homelab that had been gradually turning into a pile of exceptions.
And once I had the first few services working through it, I knew I wasn't going back.
ΠΠ½Π³ΡΠ΅Π΄ΠΈΠ΅Π½ΡΡ Libidex
ΠΠ½Π³ΡΠ΅Π΄ΠΈΠ΅Π½ΡΡ, Π²Ρ ΠΎΠ΄ΡΡΠΈΠ΅ Π² ΡΠΎΡΡΠ°Π² Libidex,
ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»ΡΡΡ ΡΠΎΠ±ΠΎΠΉ ΡΡΠ°ΡΠ΅Π»ΡΠ½ΠΎ ΠΏΠΎΠ΄ΠΎΠ±ΡΠ°Π½Π½ΡΡ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°ΡΠΈΡ ΡΡΡΠ΅ΠΊΡΠΈΠ²Π½ΡΡ ΠΏΡΠΈΡΠΎΠ΄Π½ΡΡ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠΎΠ², Π½Π°ΠΏΡΠ°Π²Π»Π΅Π½Π½ΡΡ Π½Π° ΠΊΠΎΠΌΠΏΠ»Π΅ΠΊΡΠ½ΠΎΠ΅ Π²ΠΎΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΌΡΠΆΡΠΊΠΎΠΉ ΡΠΈΠ»Ρ ΠΈ ΡΠ²Π΅ΡΠ΅Π½Π½ΠΎΡΡΠΈ Π² ΡΠ΅Π±Π΅.
ΠΠ°ΠΆΠ΄ΡΠΉ ΡΠ»Π΅ΠΌΠ΅Π½Ρ β ΠΎΡ ΠΈΠ·Π²Π΅ΡΡΠ½ΠΎΠ³ΠΎ Π°ΡΡΠΎΠ΄ΠΈΠ·ΠΈΠ°ΠΊΠ° ΠΉΠΎΡ ΠΈΠΌΠ±Π΅
Π΄ΠΎ Π°Π΄Π°ΠΏΡΠΎΠ³Π΅Π½Π½ΠΎΠ³ΠΎ ΡΠ°ΡΡΠ΅Π½ΠΈΡ ΠΆΠ΅Π½ΡΡΠ΅Π½Ρ ΠΈ Π½Π΅ΠΎΠ±Ρ ΠΎΠ΄ΠΈΠΌΡΡ Π°ΠΌΠΈΠ½ΠΎΠΊΠΈΡΠ»ΠΎΡ β Π²ΡΠΏΠΎΠ»Π½ΡΠ΅Ρ ΡΠ²ΠΎΡ ΡΠ½ΠΈΠΊΠ°Π»ΡΠ½ΡΡ ΡΠΎΠ»Ρ Π² ΡΠ»ΡΡΡΠ΅Π½ΠΈΠΈ
ΠΊΡΠΎΠ²ΠΎΠΎΠ±ΡΠ°ΡΠ΅Π½ΠΈΡ, Π°ΠΊΡΠΈΠ²ΠΈΠ·Π°ΡΠΈΠΈ
ΡΠ΅ΡΡΠΎΡΡΠ΅ΡΠΎΠ½ΠΎΠ²ΠΎΠ³ΠΎ ΡΠΈΠ½ΡΠ΅Π·Π° ΠΈ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠ°Π½ΠΈΠΈ Π·Π΄ΠΎΡΠΎΠ²ΡΡ ΡΠ΅ΠΏΡΠΎΠ΄ΡΠΊΡΠΈΠ²Π½ΠΎΠΉ
ΡΠΈΡΡΠ΅ΠΌΡ Π² ΡΠ΅Π»ΠΎΠΌ. ΠΠ·Π°ΠΈΠΌΠΎΠ΄Π΅ΠΉΡΡΠ²ΠΈΠ΅ ΡΡΠΈΡ Π²Π΅ΡΠ΅ΡΡΠ² ΠΎΠ±Π΅ΡΠΏΠ΅ΡΠΈΠ²Π°Π΅Ρ Π½Π΅ ΡΠΎΠ»ΡΠΊΠΎ ΠΊΡΠ°ΡΠΊΠΎΡΡΠΎΡΠ½ΡΠ΅ ΡΠ»ΡΡΡΠ΅Π½ΠΈΡ,
Π½ΠΎ ΠΈ ΡΡΡΠΎΠΉΡΠΈΠ²ΠΎΠ΅ ΡΠ²Π΅Π»ΠΈΡΠ΅Π½ΠΈΠ΅ ΠΏΠΎΡΠ΅Π½ΡΠΈΠΈ ΠΈ ΡΠ΅ΠΊΡΡΠ°Π»ΡΠ½ΠΎΠ³ΠΎ ΠΆΠ΅Π»Π°Π½ΠΈΡ.
libidex ΠΊΠ°ΠΏΡΡΠ»Ρ.
ΠΠ½Π³ΡΠ΅Π΄ΠΈΠ΅Π½ΡΡ Libidex
ΠΠ½Π³ΡΠ΅Π΄ΠΈΠ΅Π½ΡΡ, Π²Ρ ΠΎΠ΄ΡΡΠΈΠ΅ Π² ΡΠΎΡΡΠ°Π² Libidex,
ΠΏΡΠ΅Π΄ΡΡΠ°Π²Π»ΡΡΡ ΡΠΎΠ±ΠΎΠΉ ΡΡΠ°ΡΠ΅Π»ΡΠ½ΠΎ ΠΎΡΠΎΠ±ΡΠ°Π½Π½ΡΡ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°ΡΠΈΡ ΠΌΠΎΡΠ½ΡΡ
Π½Π°ΡΡΡΠ°Π»ΡΠ½ΡΡ Π²Π΅ΡΠ΅ΡΡΠ², Π½Π°ΠΏΡΠ°Π²Π»Π΅Π½Π½ΡΡ Π½Π°
Π²ΠΎΡΡΡΠ°Π½ΠΎΠ²Π»Π΅Π½ΠΈΠ΅ ΠΌΡΠΆΡΠΊΠΎΠΉ ΡΠΈΠ»Ρ ΠΈ ΡΠ²Π΅ΡΠ΅Π½Π½ΠΎΡΡΠΈ.
ΠΠ°ΠΆΠ΄ΡΠΉ ΡΠ»Π΅ΠΌΠ΅Π½Ρ, Π½Π°ΡΠΈΠ½Π°Ρ Ρ ΠΏΠΎΠΏΡΠ»ΡΡΠ½ΠΎΠ³ΠΎ Π°ΡΡΠΎΠ΄ΠΈΠ·ΠΈΠ°ΠΊΠ° ΠΉΠΎΡ ΠΈΠΌΠ±Π΅ ΠΈ Π·Π°ΠΊΠ°Π½ΡΠΈΠ²Π°Ρ Π°Π΄Π°ΠΏΡΠΎΠ³Π΅Π½ΠΎΠΌ
ΠΆΠ΅Π½ΡΡΠ΅Π½Π΅ΠΌ ΠΈ Π½Π΅ΠΎΠ±Ρ ΠΎΠ΄ΠΈΠΌΡΠΌΠΈ Π°ΠΌΠΈΠ½ΠΎΠΊΠΈΡΠ»ΠΎΡΠ°ΠΌΠΈ, ΠΈΠ³ΡΠ°Π΅Ρ ΠΊΠ»ΡΡΠ΅Π²ΡΡ ΡΠΎΠ»Ρ Π² ΡΠ»ΡΡΡΠ΅Π½ΠΈΠΈ ΠΊΡΠΎΠ²ΠΎΠΎΠ±ΡΠ°ΡΠ΅Π½ΠΈΡ, ΡΡΠΈΠΌΡΠ»ΡΡΠΈΠΈ
Π²ΡΡΠ°Π±ΠΎΡΠΊΠΈ ΡΠ΅ΡΡΠΎΡΡΠ΅ΡΠΎΠ½Π° ΠΈ ΠΏΠΎΠ΄Π΄Π΅ΡΠΆΠ°Π½ΠΈΠΈ Π½ΠΎΡΠΌΠ°Π»ΡΠ½ΠΎΠ³ΠΎ ΡΡΠ½ΠΊΡΠΈΠΎΠ½ΠΈΡΠΎΠ²Π°Π½ΠΈΡ ΡΠ΅ΠΏΡΠΎΠ΄ΡΠΊΡΠΈΠ²Π½ΠΎΠΉ ΡΠΈΡΡΠ΅ΠΌΡ.
Π‘ΠΈΠ½Π΅ΡΠ³ΠΈΡ ΡΡΠΈΡ ΠΊΠΎΠΌΠΏΠΎΠ½Π΅Π½ΡΠΎΠ² ΠΎΠ±Π΅ΡΠΏΠ΅ΡΠΈΠ²Π°Π΅Ρ Π½Π΅ ΡΠΎΠ»ΡΠΊΠΎ ΠΊΡΠ°ΡΠΊΠΎΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΡΠ΅ ΠΏΠΎΠ²ΡΡΠ΅Π½ΠΈΡ, Π½ΠΎ ΠΈ Π΄ΠΎΠ»Π³ΠΎΠ²ΡΠ΅ΠΌΠ΅Π½Π½ΠΎΠ΅ ΡΠ»ΡΡΡΠ΅Π½ΠΈΠ΅ ΠΏΠΎΡΠ΅Π½ΡΠΈΠΈ ΠΈ ΡΠ΅ΠΊΡΡΠ°Π»ΡΠ½ΠΎΠ³ΠΎ
Π²Π»Π΅ΡΠ΅Π½ΠΈΡ. ΠΠΈΠ±ΠΈΠ΄Π΅ΠΊΡ ΠΏΡΠΎΡΡΠ°ΡΠΈΡ.
Guardium
ΠΠ°ΠΏΡΡΠ»Ρ Guardium ΡΠ°Π·ΡΠ°Π±ΠΎΡΠ°Π½Ρ Ρ ΠΈΡΠΏΠΎΠ»ΡΠ·ΠΎΠ²Π°Π½ΠΈΠ΅ΠΌ ΠΏΠ΅ΡΠ΅Π΄ΠΎΠ²ΡΡ Π±ΠΈΠΎΡΠ΅Ρ Π½ΠΎΠ»ΠΎΠ³ΠΈΠΉ.
Π ΠΈΡ ΡΠΎΡΡΠ°Π²Π΅ ΡΠΎΠ΄Π΅ΡΠΆΠΈΡΡΡ ΡΡΡΠ΅ΠΊΡΠΈΠ²Π½Π°Ρ ΠΊΠΎΠΌΠ±ΠΈΠ½Π°ΡΠΈΡ ΡΠΊΡΡΡΠ°ΠΊΡΠΎΠ² ΡΠ°ΡΡΠ΅Π½ΠΈΠΉ, Π²ΠΈΡΠ°ΠΌΠΈΠ½ΠΎΠ² ΠΈ Π°ΠΊΡΠΈΠ²Π½ΡΡ Π±ΠΈΠΎΡΠ»Π°Π²ΠΎΠ½ΠΎΠΈΠ΄ΠΎΠ², ΡΠ΅Π»ΡΡ ΠΊΠΎΡΠΎΡΠΎΠΉ ΡΠ²Π»ΡΠ΅ΡΡΡ ΠΏΠΎΠ»Π½ΠΎΠ΅ ΡΠ½ΠΈΡΡΠΎΠΆΠ΅Π½ΠΈΠ΅
ΠΏΠ°ΡΠ°Π·ΠΈΡΠΎΠ² Π½Π° Π²ΡΠ΅Ρ ΡΡΠ°ΠΏΠ°Ρ ΠΈΡ ΡΠ°Π·Π²ΠΈΡΠΈΡ β
Π½Π°ΡΠΈΠ½Π°Ρ Ρ ΡΠΈΡ ΠΈ Π·Π°ΠΊΠ°Π½ΡΠΈΠ²Π°Ρ Π²Π·ΡΠΎΡΠ»ΡΠΌΠΈ ΠΎΡΠΎΠ±ΡΠΌΠΈ.Guardium ΠΎΡΠ·ΡΠ²Ρ
It's not a breeze having all the apps work flawlessly through the panel, but once you set it up, it's bliss! :D
you're not wrong