App setup

Now make them talk to each other.

Your stack is running, and every app is sitting there empty. This is the click-by-click part: where each one stores things, how they connect, and the handful of details that cause nearly every problem.

Before you start

The two rules

Almost every configuration problem in this stack comes back to one of these. Learn them now and most of what follows is obvious.

1. Apps find each other by container name, never localhost

Inside a container, localhost means that container. Sonarr looking at 127.0.0.1:8080 is looking inside itself. Use the service name from your Compose file: jellyfin, sonarr, radarr.

The exception that catches everyone: qBittorrent. Because it runs with network_mode: service:gluetun it has no network identity of its own — it lives inside Gluetun's network. So other apps must connect to gluetun, not qbittorrent. Same for Jackett when it is behind the VPN.
2. Every app must see the same file at the same path

If qBittorrent saves to /data/downloads/x.mkv, Sonarr must see that exact path. When they disagree, downloads finish and then never import. With the shared storage layout every app mounts one /data and this is free.

Connecting toHostPort
qBittorrent (behind VPN)gluetun8080
qBittorrent (no VPN)qbittorrent8080
Jackett (behind VPN)gluetun9117
Jackett (no VPN)jackett9117
Sonarrsonarr8989
Radarrradarr7878
Lidarrlidarr8686
Jellyfinjellyfin8096

What order to do this in

Each app needs something from the one before it — usually an API key. Going in this order means never doubling back.

  1. qBittorrent — set its password and save paths. Nothing else works until this does.
  2. Jackett — add indexers, collect the API key.
  3. Sonarr, Radarr, Lidarr — point each at qBittorrent and Jackett, set root folders.
  4. Bazarr — needs Sonarr's and Radarr's API keys.
  5. Jellyfin — create libraries over the folders the *arr apps now fill.
  6. Jellyseerr — needs Jellyfin, then Sonarr and Radarr.
  7. Uptime Kuma — monitor everything above, then set up alerts.
  8. Pocket ID — last, once the apps it protects are working.
Every *arr app hides its API key in the same place: Settings → General → API Key. You will copy these between apps constantly, so keep a scratch note open.
Downloads

qBittorrent

The download client. Configure this first — Sonarr and Radarr cannot be finished without it.

First runDo this firstFind the temporary password and change it

Recent versions no longer ship a fixed default password. One is generated on first start and written to the log:

docker compose logs qbittorrent | grep -i password

In Arcane, open the qBittorrent container and read its logs — the line is near the top.

  1. Open http://your-server:8080 and sign in as admin with that password.
  2. Go to Tools → Options → Web UI.
  3. Set a real username and password under Authentication.
  4. Tick Bypass authentication for clients on localhost only if you understand it. Leave it off otherwise.
Change the password even on a home network. qBittorrent's web interface can add torrents and write files anywhere it can reach.
Save pathsWhere downloads land, and why it matters

Under Tools → Options → Downloads:

Default Save Path
/data/downloads/complete
Keep incomplete torrents in
tick, then /data/downloads/incomplete
Copy .torrent files
off — the *arr apps manage this
Pre-allocate disk space
on, if you have room; avoids fragmentation

These are paths inside the container. With the shared layout, /data is your DATA_ROOT, so /data/downloads is really /srv/data/downloads on the host.

Keeping incomplete downloads in a separate folder is not cosmetic. Without it, Sonarr can see a half-finished file and try to import it, producing a corrupt episode that then needs deleting and re-downloading.
CategoriesSet up by the *arr appsKeeps TV, films and music apart

Categories put each app's downloads in their own subfolder and let each app see only its own work. You do not need to create them by hand — when you connect Sonarr, it creates tv-sonarr for you, Radarr creates radarr, Lidarr creates lidarr.

To check afterwards, look at the left sidebar in qBittorrent. If a category is missing, the connection from that app is not working.

Seeding and cleanup

Under Tools → Options → BitTorrent you can set a share ratio and what happens when it is reached. Pausing rather than deleting is the safe default — hardlinked files are safe to remove, but if you are not using hardlinks, deleting the torrent deletes the file your library points at.

API keys and permissionsFor the generator's deploy script

The generator can produce a deploy-to-arcane.sh that creates and starts your project through Arcane's API. It needs a key, and that key should be scoped.

Creating the key
  1. In Arcane, go to Settings → API Keys and create one.
  2. Grant only these three permissions:
    environments:read
    so the script can find which environment to deploy into
    projects:create
    to create the project
    projects:deploy
    to start it
  3. Copy the key. It is shown once.
Do not use an unrestricted key. Arcane holds the Docker socket, so a key with full access is equivalent to root on that machine. These three permissions are all the script uses, and a key cannot exceed the permissions of the user who created it.
If the names differ on your instance

Permissions follow a resource:action pattern, and the exact set depends on your Arcane version. Ask your own instance rather than guessing:

bash deploy-to-arcane.sh --permissions

That lists every permission your Arcane offers. If a call is refused, the script names the specific permission that was missing rather than failing with a bare error.

Running it
bash deploy-to-arcane.sh

It prompts for the URL and the key. The key is typed with the input hidden and held only in that shell process — never exported, never written to disk, and never passed as an argument, since arguments are visible to anyone running ps.

Environment variables were the wrong tool here. An exported key lands in your shell history, in /proc, and in the environment of every child process. Typing it at a prompt avoids all three.

Jackett

Turns many different tracker APIs into one format the *arr apps understand. It has no accounts and no library — it is plumbing.

Adding indexersAnd collecting what Sonarr needs
  1. Open http://your-server:9117.
  2. Press Add Indexer, search, and add the ones you use. Public indexers need nothing; private ones need your account details.
  3. Press Test on each. A failure here will also fail in Sonarr, so fix it now.
  4. For each working indexer, press Copy Torznab Feed and keep the URL.
  5. Copy the API Key from the top right of the page. It is the same for every indexer.
The Torznab URL Sonarr wants looks like http://gluetun:9117/api/v2.0/indexers/<indexer-id>/results/torznab/ when Jackett is behind the VPN. Jackett's copy button gives you a localhost version — swap the host for gluetun (or jackett if you are not using a VPN).
You are responsible for which indexers you use and whether accessing them is lawful where you live.
Automation

Sonarr

TV automation, and the template for Radarr and Lidarr — they are the same app with different libraries. Do this one carefully and the other two take two minutes each.

1 · Root folderStart hereWhere finished episodes go

Settings → Media Management, scroll to Root Folders, press Add Root Folder:

Path
/data/media/tv (shared layout) or /tv (split layout)

On the same page, turn on Rename Episodes. Sonarr will then name files the way Jellyfin expects, which fixes most artwork problems before they happen.

If the folder does not appear in the browser, the volume is not mounted. Check what Sonarr can actually see — in Arcane open the container console and run ls /data.
2 · Download clientConnecting to qBittorrent

Settings → Download Clients → + → qBittorrent:

Name
qBittorrent
Host
gluetunnot qbittorrent, and never localhost
Port
8080
Username / Password
what you set in qBittorrent
Category
tv-sonarr
Use SSL
off

Press Test. A green tick means the connection works. Then Save.

If Test fails, it is almost always the host. Prove the name resolves before changing anything else — in Arcane's Sonarr console, run ping -c2 gluetun. If that works, the problem is the port or the password instead.
3 · IndexersPointing at Jackett

Settings → Indexers → + → Torznab → Custom. Do this once per indexer you added in Jackett:

Name
whatever the indexer is called
URL
the Torznab feed, with the host changed to gluetun
API Key
Jackett's API key
Categories
leave default unless you know otherwise

Press Test, then Save.

4 · Quality profileDeciding what "good enough" means

Settings → Profiles. The defaults work, but two adjustments save trouble:

  • Cap the quality. A 1080p profile is the sensible default for most hardware. 4K files are enormous and will force Jellyfin to transcode on almost any client.
  • Set a size limit. Under the profile, a maximum size per episode stops a single release filling your disk.
5 · Your API keyNeeded by Bazarr and Jellyseerr

Settings → General → Security → API Key. Copy it — Bazarr and Jellyseerr both need it.

If downloads finish but never import

Sonarr and qBittorrent disagree about paths. Compare what each one sees:

docker compose exec qbittorrent ls /data/downloads
docker compose exec sonarr ls /data/downloads

Different listings mean different mounts. The proper fix is matching volumes; the patch is Settings → Download Clients → Remote Path Mappings, telling Sonarr that the client's path is really its own.

Radarr

Identical to Sonarr, with three values changed.

Root folder
/data/media/movies
Download category
radarr
Rename setting
Rename Movies, on

Everything else — download client host gluetun port 8080, Torznab indexers from Jackett, API key under Settings → General — works exactly as described for Sonarr.

Quality matters more here. A 4K remux film can be 60 GB and will transcode badly on modest hardware. Unless you have a reason, cap at 1080p.

Lidarr

Same again for music, with the usual caveat that music metadata is messier than film and TV.

Root folder
/data/media/music
Download category
lidarr
Metadata profile
Standard, unless you want every single and remix

Download client and indexers are configured exactly as for Sonarr. Expect more manual matching — Lidarr relies on MusicBrainz, so if an artist is wrong there it will be wrong here.

If you are running Kima, it can drive Lidarr from its own interface. Kima needs Lidarr's URL (http://lidarr:8686) and API key — see Kima below.

Bazarr

Fetches subtitles for the libraries Sonarr and Radarr already manage. It needs their API keys, so do this after both are working.

Connecting to Sonarr and RadarrTwo forms, same shape

Settings → Sonarr, toggle it on:

Address
sonarr
Port
8989
API Key
from Sonarr's Settings → General
SSL
off

Then Settings → Radarr with radarr and port 7878. Press Test on each, then save and restart Bazarr when it asks.

Bazarr must see the same paths as Sonarr and Radarr. If it uses different mounts it will look for your files in the wrong place and quietly find nothing. Its volumes should point at the same media folders.
Languages and providersWhat to fetch, and from where
  1. Settings → Languages — create a profile, add the languages you want, and set it as the default for series and movies.
  2. Settings → Providers — add subtitle sources. Most need a free account; create one and enter the details.
  3. Settings → Subtitles — turn on Use embedded subtitles so Bazarr does not fetch what your files already contain.
Out of sync subtitles usually mean the subtitle was matched to a different release of the same episode. Bazarr can shift timing, but fetching one matching your file's release group is the real fix.
Watching

Jellyfin

Where everything above ends up. Libraries are the main thing to get right, and the mistake is always the same one.

LibrariesThe important bitUse container paths, and split by type

Dashboard → Libraries → Add Media Library:

Content type
Movies — make a separate library for Shows
Display name
Films
Folder
/media/movies — the path inside the container

Repeat with content type Shows and folder /media/tv, and Music for /media/music.

One library per content type. Pointing a single library at a folder containing both films and TV produces wrong metadata for everything in it, and it is tedious to unpick afterwards.

If a library comes up empty

The mount is wrong, not Jellyfin. Check what it can actually see:

docker compose exec jellyfin ls /media

Nothing listed means the volume path in your Compose file does not match your real folder.

UsersGiving the household accounts

Dashboard → Users → +. For each person:

  • Set which libraries they can see under Library Access.
  • Turn off Allow media playback that requires transcoding for casual users if your server is CPU-limited — the difference between one device quietly overloading the machine and it politely refusing.
  • Leave administrative permissions off unless they need them.
Keep one local admin account with a password even if you later add single sign-on. If an SSO plugin breaks after an update, that account is how you get back in.
TranscodingOnly if you need it

Dashboard → Playback → Transcoding. On native Linux with an Intel chip, set hardware acceleration to Intel QuickSync and enable the codecs your hardware supports. This requires the device mapping in your Compose file:

devices:
  - /dev/dri:/dev/dri
Not available on Docker Desktop. On Windows and macOS the virtual machine cannot reach the GPU, so transcoding is CPU-only — realistically one stream. Storing files your devices can play directly is the practical answer there.

Jellyseerr

The request front end. It signs people in against Jellyfin and hands approved requests to Sonarr and Radarr, so it needs all three configured first.

Setup wizardRuns onceJellyfin, then the *arr apps
  1. Open http://your-server:5055. Choose Sign in with Jellyfin.
  2. Jellyfin URL: http://jellyfin:8096. Sign in with your Jellyfin admin account — this becomes the Jellyseerr owner.
  3. It offers to sync your Jellyfin libraries. Tick the ones people should be able to request from, and run the scan.
  4. Next it asks for Radarr and Sonarr. Add both.

Adding Radarr

Hostname
radarr
Port
7878
API Key
from Radarr's Settings → General
Quality Profile
the profile you set up in Radarr
Root Folder
/data/media/movies
Default server
on

Sonarr is the same with sonarr, port 8989, and root folder /data/media/tv. Sonarr also asks for a Language Profile — pick your default.

Test each connection before saving. If Jellyseerr cannot reach Radarr, requests will be accepted and then silently do nothing, which is a confusing thing to debug later.
Users and quotasLetting the household in without losing your disk
  1. Settings → UsersImport Users from Jellyfin. Everyone with a Jellyfin account appears.
  2. Set Global Movie Request Limit and Global Series Request Limit — something like 5 per week is sane.
  3. Decide on auto-approval. Leaving it off means requests wait for you; turning it on for trusted users saves you being a bottleneck.
Quotas are worth setting before you share the link. An enthusiastic new user can request an entire franchise in an afternoon and fill a disk overnight.

Immich

Independent of the media chain. The main setup is the mobile app, since phone backup is the point.

First run and mobile backupAdmin account, then your phone
  1. Open http://your-server:2283 and create the admin account. The first account is the administrator.
  2. Administration → Settings → Storage Template — turn it on if you want files organised by date on disk rather than by upload id. Decide this before uploading much; changing it later means moving everything.
  3. Install the Immich app on your phone, and enter the server URL. On the same network that is http://your-server:2283; from outside you will need remote access first.
  4. In the app, Backup → choose which albums to back up → enable background backup.
Back up the database, not just the photos. The files alone are not your library — albums, faces and metadata live in Postgres. Losing it means keeping every image but losing all the organisation.
First import is slow, and that is normal. Thumbnails and face recognition for a large library take hours. Watch Administration → Jobs to see progress rather than assuming it has hung.

Kima

Music streaming over the library Lidarr manages, with its own apps via the Subsonic API.

Setup and Lidarr linkLibrary, then automation
  1. Open http://your-server:3030 and create your account.
  2. Point it at your music. Inside the container that is /music, which maps to your media folder's music directory.
  3. Let it scan. Audio analysis is CPU-heavy and takes a while on a large library.
  4. To connect Lidarr, give Kima the URL http://lidarr:8686 and Lidarr's API key from Settings → General.

Native apps

Kima implements the OpenSubsonic API, so apps like Symfonium, Amperfy and DSub can connect to it directly. Point them at your Kima URL with your Kima username and password.

Kima's built-in Soulseek downloader is not behind the VPN. Unlike qBittorrent it runs on the normal network, because it also has to serve your library. If you enter Soulseek credentials, that peer-to-peer traffic uses your ordinary connection. Leaving the integration unconfigured avoids the question entirely.

Archiving apps

ArchiveBoxSaving web pages

Open http://your-server:8000 and sign in with admin and the generated password from your .env.

Add URLs through Add, or import bookmarks and RSS feeds. Each snapshot keeps several formats at once — HTML, PDF, screenshot and more — so storage grows faster than people expect.

Public access is off by default in the generated config (PUBLIC_INDEX, PUBLIC_SNAPSHOTS, PUBLIC_ADD_VIEW). Turn those on only deliberately.
Tube ArchivistArchiving YouTube channels

Open http://your-server:8001 and sign in with admin and the TA_PASSWORD from your .env.

  1. Settings → Application — set your download quality and format.
  2. Subscriptions — add channel URLs.
  3. Downloads — run a scan, then start the download queue.
If it will not start, it is usually Elasticsearch — either memory limits or vm.max_map_count being too low on the host. On ARM machines its Elasticsearch image has limited support, which is the one module likely to disappoint on a Raspberry Pi.
Monitoring

Uptime Kuma

Watches everything else and tells you when it stops. Worth setting up early — it is the app that tells you when the others need attention.

HTTP monitorsStart hereThe most useful check, and it needs no permissions

Open http://your-server:3001, create your account, then Add New Monitor:

Monitor Type
HTTP(s)
Friendly Name
Jellyfin
URL
http://jellyfin:8096 — the container name, not localhost
Heartbeat Interval
60 seconds is plenty
Retries
2 or 3, so a brief blip does not page you

Repeat for each service you care about:

ServiceURL
Jellyfinhttp://jellyfin:8096
Jellyseerrhttp://jellyseerr:5055
Sonarrhttp://sonarr:8989
Radarrhttp://radarr:7878
Immichhttp://immich-server:2283
qBittorrenthttp://gluetun:8080
Why the container name? Inside Docker, localhost means Uptime Kuma itself. Using container names also means you are testing the internal path rather than your router, so a monitor failing tells you something real.
Docker container monitorsNeeds the socket mounted

An HTTP monitor tells you the app is answering. A container monitor tells you the container is running. They fail in different ways, so both are useful.

First check your Compose file has the socket mounted — in the generated stack this line is commented out by default:

volumes:
  - /var/run/docker.sock:/var/run/docker.sock:ro

Then in Uptime Kuma, Profile → Settings → Docker Hosts → Add Docker Host:

Friendly Name
Local
Connection Type
Socket
Docker Daemon
/var/run/docker.sock

Save, then Add New Monitor with type Docker Container, container name jellyfin, and the Docker host you just made.

The socket is a powerful thing to hand out, even read-only. If you would rather not, HTTP monitors answer the more useful question anyway — whether the app actually works.
Status pageOptional, and safe to share

Status Pages → New Status Page. Add the monitors you want visible, and Kuma gives you a clean public page.

This is the one part of Uptime Kuma that is reasonable to expose publicly — it shows only up or down, not your admin interface. If your household keeps asking whether the server is down, this answers it for them.

Discord alerts

The quickest notification to set up, and the one most people want.

  1. In Discord, open the channel you want alerts in. You need Manage Webhooks permission, so your own server is easiest.
  2. Edit Channel → Integrations → Webhooks → New Webhook.
  3. Give it a name — Server alerts — and optionally an avatar. Press Copy Webhook URL.
  4. In Uptime Kuma, edit any monitor and press Setup Notification.
Notification Type
Discord
Friendly Name
Discord
Discord Webhook URL
paste it
Bot Display Name
optional
Default enabled
tick, so new monitors use it automatically
Apply on all existing monitors
tick, to cover what you already made

Press Test. A message should land in the channel within a second or two. If it does not, the webhook URL is wrong — nothing else in the chain produces that result.

A webhook URL is a password. Anyone who has it can post to your channel. Keep it out of screenshots, and out of git.

Telegram alerts

A little more setup than Discord, but it reaches your phone properly and does not need a Discord account.

  1. In Telegram, search for @BotFather and start a chat.
  2. Send /newbot. It asks for a display name, then a username ending in bot.
  3. BotFather replies with a token — a long string like 123456789:AAG.... That token is a password; treat it like one.
  4. Now start a chat with your new bot and send it any message. A bot cannot message you first, so without this step nothing will arrive.
  5. In Uptime Kuma, Setup Notification → Telegram, paste the bot token, then press Auto Get beside the Chat ID field. It reads the message you just sent and fills the ID in.
Notification Type
Telegram
Bot Token
from BotFather
Chat ID
press Auto Get after messaging your bot
Send silently
optional — notifies without a sound
Default enabled
tick

Press Test. If Auto Get finds nothing, you have not messaged the bot yet — that is the step everyone misses.

For a group instead of a direct message, add the bot to the group, send a message there, then press Auto Get. Group chat IDs are negative numbers, which is normal.
Connection

Cloudflare Tunnel

Reaches your services from anywhere without opening a single port. An outbound connection from your server to Cloudflare does the work, which also means it works behind CGNAT where port forwarding is impossible.

Read this before you put Jellyfin on it

The advice you will find everywhere is that streaming media through a tunnel violates Section 2.8 of Cloudflare's terms. That section was removed in 2023 — so that specific claim is out of date.

What replaced it is narrower but still relevant: the content restriction moved into Cloudflare's CDN-specific terms, and their own announcement says customers may serve video and large files through the CDN so long as that content is hosted by a Cloudflare service such as Stream, Images or R2. Video hosted on your own server is not.

So the honest position is that it is neither clearly prohibited nor clearly allowed. Plenty of people run Jellyfin through a tunnel without incident; others report throttling or a blocked subdomain. If your access matters, read Cloudflare's current terms yourself rather than trusting any guide, including this one.

Two things that are not about the terms at all.
  • Cloudflare terminates TLS. Traffic is encrypted to Cloudflare, decrypted there, then re-encrypted to your server. They can see it. That is how the protection works, but it is not end-to-end.
  • The free plan caps requests at 100 MB. This quietly breaks larger Immich photo and video uploads through the tunnel — the app will simply fail on big files.
What it is genuinely good at

Lightweight, text-based interfaces: Jellyseerr for requests, a status page, dashboards. Small payloads, no bandwidth concerns, no terms ambiguity. For Jellyfin and Immich, Tailscale avoids every one of the issues above — nothing is public at all.

Creating the tunnelStart hereAll of this happens on Cloudflare's side

You need a domain whose DNS is managed by Cloudflare. The free plan is enough.

  1. Go to the Cloudflare Zero Trust dashboard.
  2. Networks → Tunnels → Create a tunnel, and choose Cloudflared as the type.
  3. Name it — home-server is fine — and save.
  4. Cloudflare shows you install commands for various platforms. You do not need them. You only want the token — the long string beginning eyJ inside the command it displays.
  5. Paste that token into the generator's Connection step, or into CLOUDFLARE_TUNNEL_TOKEN in your .env.
That token is a credential. Anyone holding it can run a tunnel into your Cloudflare account. Keep it out of screenshots and out of git — the generated .gitignore already excludes .env.
Starting itNo ports, no certificates, no router changes
docker compose up -d cloudflared
docker compose logs -f cloudflared

You are looking for Registered tunnel connection, usually four of them to different Cloudflare locations. The tunnel then shows as Healthy in the dashboard.

Nothing was opened on your router, and no certificate was issued on your side — Cloudflare handles HTTPS at their edge.

If it will not connect, the token is almost always the cause — truncated on copy, or from a deleted tunnel. The log says so plainly.
Routing servicesDone in the dashboard, not in your files

This is the part that surprises people: your Compose file contains no routing at all. Which hostname reaches which service is configured entirely in Cloudflare.

In the tunnel's Public Hostname tab, add an entry per service:

Subdomain
requests
Domain
your domain
Service type
HTTP
URL
jellyseerr:5055

Use the container name, not localhost. cloudflared runs in a container on the same Docker network, so localhost would mean cloudflared itself — the same rule as everywhere else in this stack.

ServiceTunnel URLSensible?
Jellyseerrjellyseerr:5055Good fit
Uptime Kuma statusuptime-kuma:3001Good fit
Jellyfinjellyfin:8096See the terms note above
Immichimmich-server:2283100 MB upload cap
Arcane, qBittorrent, *arr appsNever

DNS records are created for you automatically — there is nothing to add by hand.

Adding a login in frontCloudflare Access, optional

A tunnel publishes a service to the whole internet. Anything without solid authentication of its own should have a login in front of it.

Under Access → Applications, add a self-hosted application for the hostname, then a policy — allow specific email addresses, or a whole domain. Cloudflare then requires a one-time code before the request ever reaches your server.

This is the piece that makes a tunnel reasonable for anything more sensitive than a status page. It also means a service with weak built-in auth is not directly exposed.
It still does not make the administrative tools safe to publish. Arcane holds the Docker socket; qBittorrent can write files anywhere. Keep those on Tailscale or your LAN regardless.
Single sign-on

Pocket ID

One passkey login across the apps that support it. This section is long because the honest answer is more complicated than "set up SSO and you are done" — the coverage is real but partial, and it is better to know that before you start.

What this actually gets you
AppSupportReality
ImmichNativeWorks fully, mobile app included
ArcaneNativeSupported directly
JellyfinPluginWeb browser or Quick Connect only
Sonarr, Radarr, LidarrNoneTheir own single login
Bazarr, qBittorrentNoneTheir own login
Uptime KumaNoneIts own accounts
ArchiveBox, Tube ArchivistNoneTheir own accounts

Realistically that is one login for Immich, Arcane and Jellyfin in a browser. Genuinely useful if those are the apps other people touch. Not stack-wide SSO.

Before anything else: Pocket ID needs HTTPS on a real domain. Passkeys are only allowed on a secure origin — this is a browser rule, not a setting, and no configuration works around it. Finish remote access first so https://id.example.com resolves with a valid certificate.
First runStart hereAdmin account and your first passkey
  1. Visit https://id.example.com. On first run it presents a setup wizard.
  2. Create the admin account and register a passkey when prompted — your phone, laptop, or a hardware key.
  3. Register a second passkey on a different device immediately.
This is the single most important step on this page. One passkey on one device means losing that device locks you out of every connected app at once. A second passkey — a laptop, or a hardware key in a drawer — costs a minute now.

If you are locked out anyway

Pocket ID's CLI can issue a one-time login link:

docker compose exec pocket-id \
  /app/pocket-id one-time-access-token admin

That prints a URL valid once, for a short window. Open it and register a new passkey.

Adding an OIDC clientThe same five steps for every app

Every app you connect needs a client entry in Pocket ID. The process never changes:

  1. Administration → OIDC Clients → Add OIDC Client.
  2. Name it after the app.
  3. Enter that app's callback URL — each app documents its own, and the ones you need are below.
  4. Save. Pocket ID shows a Client ID and a Client Secret. The secret is shown once — copy it now.
  5. Paste both into the app, along with the issuer URL.

Most apps only need the issuer and will discover everything else themselves:

https://id.example.com/.well-known/openid-configuration
Keep the client secret somewhere safe — your password manager, not a note file in the stack folder. If you lose it you can regenerate it in Pocket ID, but you then have to update the app too.
ImmichBest supportedFull SSO including the mobile app

The one where SSO is most worth having, since Immich is the app family members use daily.

In Pocket ID

Create a client named Immich with these callback URLs — all three:

https://photos.example.com/auth/login
https://photos.example.com/user-settings
app.immich:///oauth-callback   # the mobile app

In Immich

Administration → Settings → OAuth:

Issuer URL
https://id.example.com/.well-known/openid-configuration
Client ID
from Pocket ID
Client Secret
from Pocket ID
Scope
openid email profile
Button Text
Sign in with Pocket ID
Auto Register
on — creates accounts on first login
Do not disable password login until you have tested it. Immich can hide the password form entirely. Do that only once OIDC login has worked in a private browser window — otherwise a misconfiguration locks you out of your own photo library.
The mobile callback matters. Without app.immich:///oauth-callback registered, SSO works in the browser and fails on phones, which is a confusing way to find out.
ArcaneNative OIDC support

Arcane supports OIDC directly. Create a client in Pocket ID with Arcane's callback URL, then enter the issuer URL, client ID and secret in Arcane's authentication settings.

Keep a local admin account on Arcane too. Arcane holds the Docker socket — if SSO breaks and it is your only way in, you have lost the tool you would use to fix it.
Arcane should not be public at all, SSO or not. Reach it over Tailscale or your LAN. Single sign-on is convenience here, not a reason to expose it.
JellyfinRead before startingCommunity plugin, with a real limitation

Jellyfin has no native OpenID Connect support. There is a community SSO plugin, and it works — but it carries a limitation that matters enormously for a media server.

The plugin only authenticates through the web interface or Quick Connect. Native apps — Swiftfin, Jellyfin for Android and Android TV, Roku, Kodi — cannot use the SSO redirect flow directly. In practice, anyone watching on a TV still needs a normal Jellyfin username and password.

Native OIDC is being built upstream, but the discussion points at Jellyfin 13.0 at the earliest, and it will not reach current servers. Until then this is the situation.

Which plugin

The original 9p4/jellyfin-plugin-sso was archived in May 2026 and is unmaintained. The actively maintained fork keeps the same plugin ID, so it is an in-place upgrade if you already had the old one — and unlike the original it has a proper admin configuration UI rather than API-only setup.

Repository URL
https://raw.githubusercontent.com/K0lin/jellyfin-plugin-sso/manifest-release/manifest.json
Requires
Jellyfin 10.8+, 10.11.11 or newer recommended
Other forks exist, and at least one is explicitly pre-alpha and not for production use. Check what you are installing before adding any third-party repository to Jellyfin — a plugin repository can install code on your server.

Installing

  1. Administration → Plugins → Repositories → Add, and paste the repository URL above.
  2. Administration → Plugins → Catalog, find SSO Authentication, install it.
  3. Restart Jellyfin.
  4. In Pocket ID, create a client whose callback URL follows the plugin's pattern. The provider name at the end is yours to choose, and must match on both sides:
    https://jellyfin.example.com/sso/OID/redirect/pocketid
  5. In Plugins → SSO Authentication → Settings, add an OIDC provider with your issuer URL, client ID and secret. Enable it, and set folder access.
  6. Restart Jellyfin again — configuration changes do not take effect until you do.

Adding the login button

The button is not added automatically. Edit config/web/config.json:

{
  "customLinks": [
    { "name": "Sign in with SSO",
      "url": "/sso/OID/start/pocketid",
      "icon": "lock" }
  ]
}
Test in a private browser window. Your existing session will mask problems and make a broken setup look like it works.
Everything elseWhat to do about apps with no OIDC

Sonarr, Radarr, Lidarr, Bazarr, qBittorrent, Uptime Kuma, ArchiveBox and Tube Archivist have no OpenID Connect support. You have three honest options.

Keep them private — recommended

Do not publish them. Reach them over Tailscale or your LAN, and let their own login be a second layer rather than the only one. For administrative tools you touch occasionally, this is the right engineering answer for a home setup.

Forward authentication

A proxy component checks authentication before passing a request on, putting a login in front of apps that cannot do it themselves.

Pocket ID cannot do this on its own. It is an identity provider, not a gateway — it cannot sit in front of Sonarr and demand a login. That needs an additional component such as oauth2-proxy, or a proxy with an auth module. More moving parts, and worth being deliberate about.

Just use their own logins

A strong unique password on each, in a password manager. Unglamorous, and completely fine.

Users and groupsAdding people, and removing them properly

Adding someone

  1. In Pocket ID, Users → Add User.
  2. Send them a one-time link so they register their own passkey on their own device. You never handle a credential for them.
  3. Create groups if you want them — media-users and media-admins is plenty for a household. Apps that read group claims can map them to their own permissions.

Removing someone

Because accounts live in several systems, offboarding is a checklist rather than one button:

  • Delete or disable them in Pocket ID — stops SSO logins immediately.
  • Delete their Jellyfin user separately.
  • Remove them from Jellyseerr so pending requests do not linger.
  • Decide what happens to their Immich library before deleting that account, since it removes their photos.
  • Revoke active Jellyfin sessions, or a signed-in device keeps playing.
Deleting the Pocket ID account is not enough on its own. Anyone with a Jellyfin password and a TV app never touches Pocket ID, and will carry on watching indefinitely. This surprises people.
Still stuck?

Bring the log line. The troubleshooting page starts with the four commands that identify most problems, and has searchable answers for the specific ones — including the connection failures this page is designed to prevent.

Troubleshooting   Module reference   Remote access