Self-hosted Docker stack for home services, organised by functional profiles.
A single docker-compose.yml uses profiles to group services by category:
- infrastructure β Core services (Traefik, Tailscale sidecar, Portainer, Authentik, Homepage, CrowdSec)
- dashboard β Dashboards (Homepage)
- media β Media stack (Jellyfin, Seerr, Radarr, Sonarr, Prowlarr, qBittorrent + Gluetun VPN, Jellystat, YgΓ¨ge)
- devtools β Developer tools (SonarQube, Authentik)
- security β Security (CrowdSec)
- tools β Misc tools (RSSHub, FreshRSS)
- all β Every service
The PostgreSQL databases (Authentik, Jellystat, SonarQube) start automatically with their parent service and are not listed individually.
Only Jellyfin, Seerr, Authentik and FreshRSS are exposed on the Internet
(*.${DOMAIN_BASE}, CrowdSec in front). Everything else lives in the private zone
*.lan.${DOMAIN_BASE}, reachable only through the tailnet: a Tailscale sidecar shares
Traefik's network namespace, so the node's 100.x IP exposes Traefik :443 to authorized
devices (LAN and remote). TLS uses a Let's Encrypt DNS-01 wildcard (*.lan.${DOMAIN_BASE},
OVH provider), and Authentik Forward Auth stays on the private UIs as defense-in-depth.
Full design, DNS plan and rollback: docs/acces-prive-tailscale.md.
-
Configuration
cp .env.example .env # Edit .env with your values -
Start everything
docker compose --profile all up -d
-
Start a specific profile
# Infrastructure first (required) docker compose --profile infrastructure up -d # Then other profiles as needed docker compose --profile media up -d
-
Start several profiles at once
docker compose --profile infrastructure --profile media --profile tools up -d
# Stop everything
docker compose --profile all down
# Stop a specific profile
docker compose --profile media down
# Follow a service's logs
docker compose logs -f [service]
# Follow a profile's logs
docker compose --profile media logs -f
# Restart a service
docker compose restart [service]
# Restart a profile
docker compose --profile media restart- Traefik β Reverse proxy with automatic SSL (Let's Encrypt, DNS-01 via OVH)
- Tailscale β Sidecar node
dah-proxysharing Traefik's netns; entry point of the private zone*.lan.${DOMAIN_BASE}(seedocs/acces-prive-tailscale.md) - Portainer β Docker management UI (OIDC SSO via Authentik)
- Authentik β SSO / Identity Provider (OIDC, OAuth2, Forward Auth), with a dedicated PostgreSQL (also in
devtools) - Homepage β Main landing dashboard on
lan.${DOMAIN_BASE}, protected by Authentik Forward Auth (also indashboard) - CrowdSec β Intrusion detection engine; feeds the Traefik bouncer middleware, applied to the public routers only (also in
security)
- Homepage β Lightweight dashboard with customisable widgets, protected by Authentik SSO
- Jellyfin β Media streaming server (open-source Plex alternative)
- Jellyseerr β Media request interface for Jellyfin
- Radarr β Movie download/organisation automation
- Sonarr β TV show download/organisation automation
- Prowlarr β Indexer manager for Radarr/Sonarr
- qBittorrent β Torrent client with web UI; all traffic routed through the Gluetun VPN
- Gluetun β VPN client (WireGuard) tunnelling qBittorrent traffic
- gluetun-qbt-watchdog β Syncs Gluetun's forwarded port into qBittorrent and restarts qBittorrent when the VPN tunnel drops (brunoorsolon/gluetun-qbt-watchdog)
- Jellystat β Usage statistics dashboard for Jellyfin (dedicated PostgreSQL)
- YgΓ¨ge β YGGtorrent indexer proxy (consumed by Prowlarr)
- SonarQube β Code quality / static analysis (dedicated PostgreSQL)
- Authentik β see infrastructure
- CrowdSec β Behaviour-based intrusion detection; the Traefik bouncer middleware blocks flagged IPs on the public routers (jellyfin/seerr/auth/rss)
- RSSHub β RSS feed generator for sites that don't provide one
- FreshRSS β Self-hosted RSS aggregator (OIDC SSO via Authentik)
The media stack uses a unified layout under ${MEDIA_PATH}:
/srv/.../media/
βββ downloads/ # qBittorrent downloads
β βββ movies/ # Movies in progress
β βββ tv/ # TV in progress
βββ movies/ # Movie library (Jellyfin)
βββ tv/ # TV library (Jellyfin)
Recommended configuration:
- Radarr β root folder:
/data/movies - Sonarr β root folder:
/data/tv - qBittorrent β downloads:
/data/downloads - Jellyfin β libraries:
/data/moviesand/data/tv
Authentik is an open-source Identity Provider (IdP) providing single sign-on for the stack. It supports several protocols:
- OIDC (OpenID Connect) β for apps with native support
- OAuth2 β for modern apps
- SAML β for enterprise apps
- Forward Auth β for apps without native SSO, via Traefik's
forwardauthmiddleware and Authentik's embedded outpost
The following services are gated by Authentik Forward Auth on their private hostnames (*.lan.${DOMAIN_BASE}, tailnet-only). Each one has a dedicated <service>-access group that controls who may reach it (a user must belong to the group):
homepage, radarr, sonarr, prowlarr, qbittorrent, jellystat, glances
The Traefik middlewares all point at the embedded outpost:
http://authentik-server:9000/outpost.goauthentik.io/auth/traefik
Arr services: Radarr, Sonarr and Prowlarr have their internal auth disabled (
<AuthenticationMethod>External</AuthenticationMethod>inconfig.xml) and rely entirely on Authentik for their web UI. If Authentik is down the UI is unreachable (502/503) but secured.
For native API clients (Helmarr-style mobile tooling from the tailnet β internal sync like Prowlarr β Arr or Terraform uses container names and never goes through Traefik), the Forward Auth would block the request since it carries an API key, not an Authentik session. A dedicated higher-priority router therefore exposes the API path of each service bypassing Authentik, on the private hostname (the client device must be on the tailnet):
| Service | Router | Rule | Auth on the API |
|---|---|---|---|
| Radarr | radarr-api |
Host(radarr.lan.β¦) && PathPrefix(/api) |
X-Api-Key |
| Sonarr | sonarr-api |
Host(sonarr.lan.β¦) && PathPrefix(/api) |
X-Api-Key |
| Prowlarr | prowlarr-api |
Host(prowlarr.lan.β¦) && PathPrefix(/api) |
X-Api-Key |
| qBittorrent | qbittorrent-api |
Host(qbt.lan.β¦) && PathPrefix(/api/v2) |
qBittorrent WebUI auth (re-enabled) |
| Jellystat | jellystat-api |
Host(jellystat.lan.β¦) && PathPrefix(/api) |
x-api-token |
These routers use
priority=100so the/apiprefix wins over the catch-all UI router. The API stays reachable even when Authentik is down β each service authenticates the request itself (API key / token). For qBittorrent this requires its WebUI auth to be re-enabled (it is no longer fronted by Authentik on/api/v2).
- Portainer β OIDC (Authentik side managed by Terraform; the OAuth fields are entered manually in Portainer's UI, see below)
- FreshRSS β OIDC (fully managed by Terraform, credentials injected via env file)
- Jellyfin β OIDC via the SSO plugin (manual)
- URL:
https://auth.${DOMAIN_BASE} - Bootstrap admin: set via
AUTHENTIK_BOOTSTRAP_EMAILandAUTHENTIK_BOOTSTRAP_PASSWORDin.env
Portainer does not read OIDC settings from environment variables, so the OAuth section must be filled in its web UI. Terraform creates the Authentik provider/application and writes the endpoints to terraform/authentik/generated/portainer.env for reference.
In Portainer β Settings β Authentication β OAuth:
| Field | Value |
|---|---|
| Provider | Custom |
| Client ID / Secret | from Authentik (see generated/portainer.env) |
| Authorization URL | https://auth.${DOMAIN_BASE}/application/o/authorize/ |
| Access token URL | https://auth.${DOMAIN_BASE}/application/o/token/ |
| Resource URL | https://auth.${DOMAIN_BASE}/application/o/userinfo/ |
| Logout URL | https://auth.${DOMAIN_BASE}/application/o/portainer/end-session/ |
| Redirect URL | https://portainer.lan.${DOMAIN_BASE}/ |
| User identifier | preferred_username (or email) |
| Scopes | email openid profile |
Two independent Terraform modules under terraform/ keep the stack reproducible across servers:
terraform/authentik/β Authentik SSO (providergoauthentik/authentik)terraform/arr/β Radarr / Sonarr / Prowlarr configuration (providersdevopsarr/{radarr,sonarr,prowlarr})
The Authentik configuration is managed as code (provider goauthentik/authentik).
| File | Purpose |
|---|---|
providers.tf, variables.tf, terraform.tfvars |
Provider + connection (Authentik URL & API token) |
shared.tf |
Shared data sources (authorization/invalidation flows, default OIDC scopes) |
freshrss.tf, portainer.tf |
OIDC providers + applications (+ generated env file) |
proxy_forwardauth.tf |
The Forward Auth services: per service a proxy provider (forward_single, external_host on the private zone), an application, a <service>-access group, and a policy binding restricting access to that group |
outpost.tf |
Embedded outpost; all proxy providers are attached automatically |
The embedded outpost is a pre-existing singleton: outpost.tf adopts it automatically through an import block (requires Terraform β₯ 1.6), so no manual terraform import is needed on a fresh server.
Usage:
cd terraform/authentik
# set authentik_url and authentik_token in terraform.tfvars
terraform init
terraform plan
terraform applyAfter
apply, add your user to the relevant<service>-accessgroups β otherwise Forward Auth will deny access since the policy binding requires group membership.
This module codifies the Radarr / Sonarr / Prowlarr configuration (quality settings, indexers, download clients, notifications) so a fresh server lands on the same tuned setup. It talks to each service over its internal URL (container name on traefik_net) authenticated by API key, never the public Traefik URL. Requires Terraform β₯ 1.7 (conditional import blocks with for_each).
| File | Purpose |
|---|---|
providers.tf, variables.tf, terraform.tfvars |
Providers + connection (URLs, API keys, qBittorrent, Telegram, paths) |
locals.tf |
Shared TRaSH-style custom formats (FR language favoured: MULTi/TRUEFRENCH/FRENCH; VOSTFR/VFQ/unwanted blocked) with per-resolution scores (1080p / 2160p) |
radarr.tf, sonarr.tf |
Root folder, qBittorrent download client, custom formats, quality profiles, recycle bin |
prowlarr.tf |
qBittorrent download client, Radarr/Sonarr applications (category sync), Telegram notification |
indexers.tf |
Prowlarr Cardigann indexers (YgΓ©gΓ©, Generation-Free, Nostradamus, Torr9, C411) β created in Prowlarr then adopted by import |
sync_profile.tf |
Prowlarr sync profiles with a per-indexer minimum-seeders floor (YgΓ©gΓ©/Leak = 2) |
quality_definitions.tf |
TRaSH quality sizes (min_size = 0 so size never blocks; custom formats do the filtering) |
imports.tf |
Conditional imports β empty *_import_id β resource created (fresh server); set β existing Arr config adopted |
Usage:
cd terraform/arr
cp terraform.tfvars.example terraform.tfvars
# fill in API keys (config.xml -> <ApiKey>), qBittorrent + Telegram creds, paths
terraform init
terraform plan
terraform applyAdopting an existing setup: on a server where Radarr/Sonarr/Prowlarr are already configured, fill the
*_import_id/indexer_import_idsvalues interraform.tfvarsso Terraform adopts the existing resources instead of creating duplicates. Leave them empty on a fresh server.
- β Private services are reachable only through the tailnet (device identity = first auth layer)
- β Centralised authentication, unified users/groups, native 2FA/MFA, centralised auth logs
- β CrowdSec + Traefik bouncer block malicious IPs on the remaining public edge (jellyfin/seerr/auth/rss)
β οΈ Single point of failure (assumed): if Authentik is down, every Forward Auth / OIDC web UI becomes inaccessible, including the private ones (the/apiroutes stay up β see below). Keeping Forward Auth on top of Tailscale is a deliberate defense-in-depth choice.β οΈ Arr web UIs (AuthenticationMethod=External) have no protection of their own without Authentik β mitigated by the tailnet boundaryβ οΈ The/apiroutes (Radarr, Sonarr, Prowlarr, qBittorrent, Jellystat) bypass Authentik and are guarded by the tailnet + the service's own API key / token. Keep those keys secret and qBittorrent's WebUI auth enabled.
Recommendations:
- Monitor Authentik (a healthcheck is configured)
- Back up the PostgreSQL databases regularly
- Alert on Authentik downtime
- Periodically test a full-stack startup
Internal auth is disabled by editing config.xml directly (not available via environment variables):
<AuthenticationMethod>External</AuthenticationMethod>Procedure:
- Stop the containers:
docker compose stop radarr sonarr prowlarr - Edit the
config.xmlfiles in the Docker volumes - Start them again:
docker compose start radarr sonarr prowlarr