Skip to content

Miniluchi/Docker-At-Home

Repository files navigation

Docker At Home

Self-hosted Docker stack for home services, organised by functional profiles.

πŸ“ Architecture

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.

🌐 Public vs private (Tailscale)

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.

πŸš€ Quick start

  1. Configuration

    cp .env.example .env
    # Edit .env with your values
  2. Start everything

    docker compose --profile all up -d
  3. Start a specific profile

    # Infrastructure first (required)
    docker compose --profile infrastructure up -d
    
    # Then other profiles as needed
    docker compose --profile media up -d
  4. Start several profiles at once

    docker compose --profile infrastructure --profile media --profile tools up -d

πŸ› οΈ Management

# 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

βš™οΈ Services

πŸ—οΈ infrastructure

  • Traefik β€” Reverse proxy with automatic SSL (Let's Encrypt, DNS-01 via OVH)
  • Tailscale β€” Sidecar node dah-proxy sharing Traefik's netns; entry point of the private zone *.lan.${DOMAIN_BASE} (see docs/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 in dashboard)
  • CrowdSec β€” Intrusion detection engine; feeds the Traefik bouncer middleware, applied to the public routers only (also in security)

πŸ“Š dashboard

  • Homepage β€” Lightweight dashboard with customisable widgets, protected by Authentik SSO

🎬 media

  • 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)

🧰 devtools

  • SonarQube β€” Code quality / static analysis (dedicated PostgreSQL)
  • Authentik β€” see infrastructure

πŸ›‘οΈ security

  • CrowdSec β€” Behaviour-based intrusion detection; the Traefik bouncer middleware blocks flagged IPs on the public routers (jellyfin/seerr/auth/rss)

πŸ§ͺ tools

  • RSSHub β€” RSS feed generator for sites that don't provide one
  • FreshRSS β€” Self-hosted RSS aggregator (OIDC SSO via Authentik)

πŸ“‚ Media layout

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/movies and /data/tv

πŸ” SSO with Authentik

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 forwardauth middleware and Authentik's embedded outpost

Forward Auth (via Traefik β†’ 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> in config.xml) and rely entirely on Authentik for their web UI. If Authentik is down the UI is unreachable (502/503) but secured.

API routes outside Authentik

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=100 so the /api prefix 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).

Native OIDC

  • 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)

Access

  • URL: https://auth.${DOMAIN_BASE}
  • Bootstrap admin: set via AUTHENTIK_BOOTSTRAP_EMAIL and AUTHENTIK_BOOTSTRAP_PASSWORD in .env

Portainer OIDC β€” UI configuration

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

🧬 Infrastructure as Code (Terraform)

Two independent Terraform modules under terraform/ keep the stack reproducible across servers:

  • terraform/authentik/ β€” Authentik SSO (provider goauthentik/authentik)
  • terraform/arr/ β€” Radarr / Sonarr / Prowlarr configuration (providers devopsarr/{radarr,sonarr,prowlarr})

terraform/authentik/

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 apply

After apply, add your user to the relevant <service>-access groups β€” otherwise Forward Auth will deny access since the policy binding requires group membership.

terraform/arr/

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 apply

Adopting an existing setup: on a server where Radarr/Sonarr/Prowlarr are already configured, fill the *_import_id / indexer_import_ids values in terraform.tfvars so Terraform adopts the existing resources instead of creating duplicates. Leave them empty on a fresh server.

πŸ”’ Security notes

  • βœ… 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 /api routes 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 /api routes (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:

  1. Monitor Authentik (a healthcheck is configured)
  2. Back up the PostgreSQL databases regularly
  3. Alert on Authentik downtime
  4. Periodically test a full-stack startup

Disabling internal auth on Arr services

Internal auth is disabled by editing config.xml directly (not available via environment variables):

<AuthenticationMethod>External</AuthenticationMethod>

Procedure:

  1. Stop the containers: docker compose stop radarr sonarr prowlarr
  2. Edit the config.xml files in the Docker volumes
  3. Start them again: docker compose start radarr sonarr prowlarr

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages