Jellyfin and Authentik OIDC with admin

Status: CURRENT — COMMUNITY OIDC / PRE-STABLE PLUGIN
Last reviewed: 31 August 2026
Applies to: Jellyfin + Authentik using the community SSO/OIDC plugin

Important: Authentik’s current official Jellyfin integration guide now recommends LDAP, not OIDC. This article is specifically for people who still want OIDC/SSO through the community Jellyfin SSO plugin.

⚠️ Keep a break-glass Jellyfin admin account. A bad role mapping can remove administrator permissions from the account you normally use. Before changing SSO/admin-role settings, create or keep a second local Jellyfin administrator that does not depend on OIDC.

A quick 2026 update

When I originally wrote this article, the common OIDC option was the 9p4/jellyfin-plugin-sso plugin. That repository was archived by its author on 12 May 2026.

There is now a community revival maintained under Flowfin/jellyfin-plugin-sso. It supports Authentik over OpenID Connect and retains role-based mappings including administrator roles. It also keeps the same plugin GUID as the original 9p4 plugin, so it is designed to install over an existing installation while keeping the configuration.

At the time of this review the Flowfin project is still pre-stable/release-candidate software, so read its current installation notes before using it on a production Jellyfin server.

If you just want the Authentik-supported path with fewer moving parts, the current Authentik Jellyfin guide uses Jellyfin’s LDAP Authentication plugin instead.

The admin-role problem

With the OIDC SSO plugin, Jellyfin can use a role/group claim from Authentik to decide whether an SSO user should have Jellyfin administrator permissions.

The important settings are:

  • Role Claim: usually groups when using Authentik’s group claim.
  • Admin Roles: the exact Authentik group name that should grant Jellyfin administrator access.

For example, I originally used Authentik’s default administrator group:

authentik Admins


You can also create a dedicated group specifically for Jellyfin administrators, which is cleaner than tying Jellyfin permissions to Authentik’s own superuser/admin group.

In Authentik go to:

Admin Interface → Directory → Groups → Create


Create something like:

jellyfin-admins


Add only the users who should have Jellyfin administrator access to that group, then configure the Jellyfin SSO plugin with:

Role Claim: groups
Admin Roles: jellyfin-admins


The group name needs to match the value Authentik actually sends in the groups claim.

Jellyfin SSO plugin Admin Roles and Role Claim configuration

I also resized this screenshot from the original article because WordPress was rendering the full-size image way too large and making it look stretched.

Why admin access can disappear

The SSO plugin uses the role/group information supplied by the identity provider to determine administrator status. If your configured Admin Roles do not match anything in the incoming role claim, the SSO login may no longer receive Jellyfin administrator permissions.

This is why I strongly recommend keeping a separate local admin account while testing SSO. Don’t make your only Jellyfin administrator depend on an external identity provider and a role mapping you’re still configuring.

Fixing an existing locked-out admin

Credit to Spyros from the comments for this recovery method.

If you’ve already configured OIDC and your normal Jellyfin account has lost administrator access, the plugin configuration can be edited directly.

On my Docker setup the configuration was under:

/jellyfin/data/plugins/configurations/SSO-Auth.xml


Your actual host path may differ depending on where the Jellyfin /config volume is mounted. The important part is the Jellyfin plugin configuration file:

plugins/configurations/SSO-Auth.xml


Find the <AdminRoles> section and make sure it contains the exact Authentik group that should grant administrator access.

<AdminRoles>
  <string>authentik Admins</string>
</AdminRoles>


Or, if you’ve created a dedicated Jellyfin administrator group:

<AdminRoles>
  <string>jellyfin-admins</string>
</AdminRoles>


Save the file and restart Jellyfin.

After logging in again through Authentik, the account should regain Jellyfin administrator rights as long as the configured group is present in the incoming role claim.

If it still doesn’t work

Don’t just keep changing AdminRoles randomly. First confirm what Authentik is actually sending.

  • Confirm the Jellyfin plugin’s Role Claim is set to the claim containing your groups — commonly groups with Authentik.
  • Confirm the exact Authentik group name matches the value entered in Admin Roles, including spaces and capitalisation.
  • Confirm the user is actually a member of that Authentik group.
  • Check the Jellyfin logs during an SSO login for role/claim errors.
  • Keep your local break-glass admin available until the SSO mapping has been tested successfully.

Official LDAP vs community OIDC

As of August 2026 there are effectively two Authentik/Jellyfin paths:

  • LDAP: the method currently documented by Authentik. It uses Authentik’s LDAP provider/outpost plus Jellyfin’s LDAP Authentication plugin.
  • OIDC: the community SSO-plugin method covered by this article. The old 9p4 project is archived; the Flowfin revival is the current continuation but is not yet a stable release.

I still prefer OIDC conceptually for browser SSO, so I’m keeping this article rather than archiving it completely. Just be aware that it is now a community-plugin path, not Authentik’s current official Jellyfin integration.

References

And seriously — keep that second local admin until you’ve proved the OIDC role mapping works. Saving five minutes during setup isn’t worth locking yourself out of your own Jellyfin server 😅

Docker Compose for Entertainment Stack

Status: CURRENT — MAJOR 2026 UPDATE
Last reviewed: 31 August 2026
Applies to: Docker Compose, Bazarr, Jellyfin, Seerr, Plex, Prowlarr, Radarr, Sonarr and Tautulli

The original idea behind this stack is still good: put related containers on one Docker network so they can find each other by service name instead of hard-coding IP addresses. A few of the actual applications and Docker habits from the old stack are now cactus though, so this is the cleaned-up version.

My entertainment stack has always been built around one simple Docker feature:

http://prowlarr:9696


No memorising container IP addresses. No changing configs when Docker hands a container a different IP. If two containers are on the same user-defined Docker network, Docker’s internal DNS lets them talk to each other using the service/container name.

That part of the original article was spot on and is still how I’d do it today.

What’s changed since the old stack?

  • Overseerr is gone. The original project was archived in February 2026. Overseerr and Jellyseerr have effectively converged into Seerr, which is the current maintained project.
  • Watchtower is gone. The upstream project was archived in December 2025 and now explicitly says it is no longer maintained.
  • The top-level Compose version: "3.6" line is obsolete and ignored by modern Docker Compose.
  • I’ve changed application configs from anonymous/named Docker volumes to obvious bind mounts under /home/richay/docker/. Much easier to back up and inspect when something inevitably goes sideways at 1am.
  • Radarr/Sonarr/Bazarr now see one consistent /data tree instead of three completely different host paths.
  • Prowlarr and Seerr don’t need access to your entire media library, so they no longer get it. Less access, less bullshit.
  • Plex still uses host networking in this example because LinuxServer continues to document that as its standard setup, but its old ports: entry has been removed because Docker ignores published ports in host-network mode.

Overseerr → Seerr

The old stack used:

sctx/overseerr:latest


That repository was archived on 15 February 2026. The current maintained project is Seerr, which supports Plex, Jellyfin and Emby and still integrates with Sonarr and Radarr.

The current official Docker image is:

ghcr.io/seerr-team/seerr:latest


If you’re migrating an existing Overseerr/Jellyseerr database rather than starting fresh, use Seerr’s official migration guide. Don’t just point a random new image at the old database and hope Docker Jesus sorts it out 🙂

Watchtower has been removed

The old stack let Watchtower automatically pull and replace containers. The Watchtower maintainers archived the project on 17 December 2025 and now state that it is no longer maintained.

I don’t think unattended auto-updates are worth handing permanent Docker-socket access to an abandoned container.

Updating this stack manually is hardly an ordeal:

docker compose pull
docker compose up -d
docker image prune


LinuxServer currently recommends Diun if you want notifications that new images are available, rather than automatically updating everything while you’re asleep and waking up to seven broken services and a strong urge to throw the server into the pool.

Folder layout

I keep Compose files and persistent Docker data separate:

/home/richay/docker-compose/   # Compose files and small hand-managed config
/home/richay/docker/           # Persistent container data


A simple media tree might look like:

/mnt/media/
├── downloads/
└── media/
    ├── movies/
    ├── tv/
    └── music/


The important part is that Radarr/Sonarr and your download client see the same filesystem through the same /data container path. That allows hardlinks and atomic moves instead of copying a massive file from one mount to another and then deleting the original.

Create the config folders:

mkdir -p /home/richay/docker-compose/entertainment
mkdir -p /home/richay/docker/{bazarr,jellyfin,seerr,plex,prowlarr,radarr,sonarr,tautulli}
mkdir -p /mnt/media/{downloads,media/movies,media/tv,media/music}


Current entertainment Compose stack

This keeps the spirit of my original stack without carrying all the old baggage:

services:
  bazarr:
    image: lscr.io/linuxserver/bazarr:latest
    container_name: bazarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
    volumes:
      - /home/richay/docker/bazarr:/config
      - /mnt/media:/data
    ports:
      - "6767:6767"
    restart: unless-stopped
    networks:
      - entertainment

  jellyfin:
    image: lscr.io/linuxserver/jellyfin:latest
    container_name: jellyfin
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
    volumes:
      - /home/richay/docker/jellyfin:/config
      - /mnt/media/media:/data/media:ro
    ports:
      - "8096:8096"
      # Optional local discovery:
      # - "7359:7359/udp"
      # Optional DLNA/service discovery:
      # - "1900:1900/udp"
    # Intel/AMD hardware acceleration:
    # devices:
    #   - /dev/dri:/dev/dri
    restart: unless-stopped
    networks:
      - entertainment

  seerr:
    image: ghcr.io/seerr-team/seerr:latest
    container_name: seerr
    init: true
    environment:
      - LOG_LEVEL=info
      - TZ=Australia/Perth
      - PORT=5055
    volumes:
      - /home/richay/docker/seerr:/app/config
    ports:
      - "5055:5055"
    healthcheck:
      test: wget --no-verbose --tries=1 --spider http://localhost:5055/api/v1/settings/public || exit 1
      start_period: 20s
      timeout: 3s
      interval: 15s
      retries: 3
    restart: unless-stopped
    networks:
      - entertainment

  plex:
    image: lscr.io/linuxserver/plex:latest
    container_name: plex
    network_mode: host
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
      - VERSION=docker
      - PLEX_CLAIM=
    volumes:
      - /home/richay/docker/plex:/config
      - /mnt/media/media:/data/media:ro
    # Intel/AMD hardware acceleration:
    # devices:
    #   - /dev/dri:/dev/dri
    restart: unless-stopped

  prowlarr:
    image: lscr.io/linuxserver/prowlarr:latest
    container_name: prowlarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
    volumes:
      - /home/richay/docker/prowlarr:/config
    ports:
      - "9696:9696"
    restart: unless-stopped
    networks:
      - entertainment

  radarr:
    image: lscr.io/linuxserver/radarr:latest
    container_name: radarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
    volumes:
      - /home/richay/docker/radarr:/config
      - /mnt/media:/data
    ports:
      - "7878:7878"
    restart: unless-stopped
    networks:
      - entertainment

  sonarr:
    image: lscr.io/linuxserver/sonarr:latest
    container_name: sonarr
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
    volumes:
      - /home/richay/docker/sonarr:/config
      - /mnt/media:/data
    ports:
      - "8989:8989"
    restart: unless-stopped
    networks:
      - entertainment

  tautulli:
    image: lscr.io/linuxserver/tautulli:latest
    container_name: tautulli
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Australia/Perth
    volumes:
      - /home/richay/docker/tautulli:/config
    ports:
      - "8181:8181"
    restart: unless-stopped
    networks:
      - entertainment

networks:
  entertainment:
    name: entertainment


Why keep an explicit entertainment network?

Docker Compose automatically creates a default network and provides service-name DNS anyway, so technically the explicit network isn’t required if everything lives in this one Compose project.

I still like naming it entertainment because it gives me a stable network name that another stack can deliberately join later.

For example, Radarr can talk to Sonarr/Prowlarr-style services using names such as:

http://prowlarr:9696
http://radarr:7878
http://sonarr:8989
http://seerr:5055
http://jellyfin:8096


No container IPs required. Docker handles the internal DNS. Fucking lovely.

Plex is the odd bastard

Plex uses network_mode: host in the LinuxServer recommended configuration, so it does not join the entertainment bridge network in this example.

That also means this old combination was pointless:

network_mode: host
ports:
  - 32400:32400


Docker explicitly ignores published ports when host networking is enabled because Plex is already binding directly to the host’s network stack.

So Tautulli/Seerr should reach Plex using the Docker host’s LAN address, for example:

http://192.168.1.10:32400


Replace that with your actual Docker/Plex host address.

Jellyfin ports

The only Jellyfin port I expose by default in the stack is:

8096/tcp  # normal Jellyfin HTTP web/API


LinuxServer still documents 8920/tcp, 7359/udp and 1900/udp as optional ports. I don’t bother with Jellyfin’s own HTTPS port 8920 when Traefik is handling TLS in front of it.

7359/udp is useful for local client discovery and 1900/udp is used for service/DLNA discovery. Uncomment them only if you actually use those features.

Hardware transcoding

For Intel/AMD hardware acceleration, LinuxServer’s Jellyfin image still supports mapping the host’s DRI device:

devices:
  - /dev/dri:/dev/dri


The same mapping can be used for Plex when the Docker host has the GPU available.

If you’re running Docker inside a Proxmox LXC, get the GPU working in the LXC first. Then pass it to Docker. Debugging Proxmox → LXC → Docker → Jellyfin all at once is how hobbies turn into drinking problems 🙂

Where’s qBittorrent?

I deliberately left the download client out of this stack.

If qBittorrent needs to live behind a VPN, I prefer it in a dedicated VPN/download stack rather than mixing VPN networking into Plex, Jellyfin and everything else.

See my updated Docker Stack with ExpressVPN article for the VPN side.

Radarr and Sonarr can still use that qBittorrent instance as their download client — it does not need to be in the same Compose file.

Don’t mount everything into everything

My old stack gave Prowlarr and Overseerr access to media, storage and downloads even though they had no bloody reason to touch those files.

The new split is:

  • Radarr / Sonarr / Bazarr: access to the shared /data tree.
  • Plex / Jellyfin: read-only access to /data/media.
  • Prowlarr / Seerr / Tautulli: config only; no media filesystem access required.

It’s cleaner and reduces the blast radius if one container ever gets compromised.

Starting and checking the stack

cd /home/richay/docker-compose/entertainment
docker compose up -d
docker compose ps


Then confirm the internal Docker DNS works from one of the containers:

docker exec sonarr getent hosts prowlarr
docker exec sonarr getent hosts radarr


If those return container IPs, the service-name networking is doing exactly what we wanted.

Old stack — what I’d keep and what I’d bin

  • Keep: one shared Docker network and service-name communication.
  • Keep: LinuxServer images for the *arr apps, Jellyfin, Plex and Tautulli.
  • Keep: Perth timezone and sensible PUID/PGID ownership.
  • 🛠️ Change: Overseerr → Seerr.
  • 🛠️ Change: named volumes → obvious bind-mounted config folders.
  • 🛠️ Change: multiple inconsistent media paths → one /data tree.
  • 🗑️ Bin: obsolete Compose version: "3.6".
  • 🗑️ Bin: Watchtower.
  • 🗑️ Bin: pointless Plex ports: while using host networking.

References

The original stack wasn’t bad at all — the networking idea aged really well. It just accumulated a couple of dead projects, redundant mounts and Docker archaeology around it. A bit of a clean-out and she’s good again 🙂