Do these in order
Each step depends on the one before it. Skipping ahead is the main reason people get stuck — particularly with passkeys, which simply refuse to work without HTTPS.
https:// with a real domain name, or on localhost. You cannot run Pocket ID at http://192.168.1.50:1411 and register a passkey — the browser blocks it, and no setting changes that. Sort out HTTPS first.Reaching your server
Your server sits on your home network. The internet can't see it, which is a feature. There are three ways to change that, and they differ enormously in how much they expose.
Choosing an approach
Tailscale
A private network between your own devices. Nothing is published to the internet; your phone simply behaves as though it's at home.
- Set up in minutes
- Works around CGNAT
- No ports opened
- Only for people you invite
Cloudflare Tunnel
A public address with no open ports. An outbound connection from your server does the work, so it also beats CGNAT.
- Real public URL
- Router untouched
- Cloudflare terminates TLS
- Media streaming is unsettled
Reverse proxy
Your own server answering on ports 80 and 443, forwarded through your router.
- Full control, no middleman
- Needs a static-ish IP or DDNS
- Genuinely public — hardening matters
- Required for Pocket ID
If it's just you and your own devices, use Tailscale and skip the rest of this page. If you want family members to watch Jellyfin on a TV without installing anything, you need a public address — a reverse proxy or Cloudflare Tunnel. Pocket ID needs a public HTTPS address too, so single sign-on implies one of the latter two.
Tailscale
The least you can do while still getting remote access, and for many people it's genuinely enough.
curl -fsSL https://tailscale.com/install.sh | sh sudo tailscale up
Install the app on your phone and laptop, sign into the same account, and your server appears at a stable address like http://server:8096. No ports opened, nothing public.
Reverse proxy with Caddy
A reverse proxy sits in front of everything, terminates HTTPS, and routes by hostname. Caddy is the friendliest option because it obtains and renews certificates without being asked.
-
Point a domain at your home
Buy a domain, then create records pointing at your public address. If your address changes — most home connections — use a dynamic DNS updater, or Cloudflare's API with a small updater container.
# One record per service, or a wildcard jellyfin.example.com → your.public.ip id.example.com → your.public.ip -
Forward two ports
In your router, forward 80 and 443 to your server. Those two only — never the application ports themselves.
Port 80 is needed for certificate issuance, not for serving your site. Caddy redirects it to HTTPS automatically once certificates exist. -
Add Caddy to your stack
caddy: image: caddy:latest container_name: caddy ports: - "80:80" - "443:443" volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - ${CONFIG_ROOT}/caddy/data:/data - ${CONFIG_ROOT}/caddy/config:/config restart: unless-stoppedThe
/datavolume holds your certificates. Losing it means re-issuing them, and issuers apply rate limits — so keep it. -
Write the Caddyfile
One block per service. Caddy handles certificates for each name automatically.
id.example.com { reverse_proxy pocket-id:1411 } jellyfin.example.com { reverse_proxy jellyfin:8096 } photos.example.com { reverse_proxy immich-server:2283 }Services are addressed by container name, because Caddy is on the same Docker network — not
localhost, which inside Caddy's container means Caddy.Testing without burning your rate limit. Certificate authorities limit failed attempts. While experimenting, addacme_ca https://acme-staging-v02.api.letsencrypt.org/directoryat the top of the Caddyfile. Certificates won't be trusted by browsers, but you can confirm routing works, then remove the line. -
Bring it up and check
docker compose up -d caddy docker compose logs -f caddy
Watch for certificate issuance. Errors here are almost always DNS not resolving yet, or port 80 not actually reaching the server.
Reloading after a change
Adding a service means adding a block and reloading. A reload swaps the configuration with no dropped connections — it is not a restart:
# check the syntax first docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile # apply it with no downtime docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile
The Caddyfile is mounted live, so editing it on the host or through Arcane is enough — only the reload is needed.
What to never expose
Publishing a hostname makes it reachable by everyone, including automated scanners that will find it within hours. Some things in this stack must not be among them.
| Service | Public? | Why |
|---|---|---|
| Jellyfin | Reasonable | Designed for it, has real user accounts |
| Jellyseerr | Reasonable | Built for household requests |
| Immich | Reasonable | Needs it for phone backup away from home |
| Pocket ID | Required | Can't function otherwise |
| Arcane | Never | Holds the Docker socket — root on your machine |
| qBittorrent | Never | Can write files anywhere it can reach |
| Sonarr / Radarr / Lidarr | Never | Weak auth, can execute scripts on import |
| Jackett | Never | No meaningful authentication |
| Uptime Kuma | Status page only | Publish the status page, not the admin interface |
Reach the "never" items through Tailscale instead. You can run both: a public proxy for the things family use, and a private network for everything administrative.
Pocket ID
Pocket ID is a small, OpenID Connect certified identity provider built entirely around passkeys. One login for every app that supports it, no passwords stored anywhere, and a single Go binary with a SQLite database rather than the sprawl of Keycloak.
A key pair held by your phone, laptop or a physical security key, unlocked by your fingerprint, face or device PIN. The private half never leaves the device and is never sent anywhere. There's no password to reuse, leak or guess — which removes most of the ways accounts get broken into.
https://id.example.com resolving and trusted before anything below will work.Installing it
-
Generate an encryption key
openssl rand -base64 32
Put it in your
.env. Pocket ID uses it to encrypt sensitive values at rest.POCKETID_ENCRYPTION_KEY=your-generated-key POCKETID_URL=https://id.example.com -
Add the service
pocket-id: image: ghcr.io/pocket-id/pocket-id:v2 container_name: pocket-id ports: - "1411:1411" volumes: - ${CONFIG_ROOT}/pocket-id:/app/data environment: - APP_URL=${POCKETID_URL} - ENCRYPTION_KEY=${POCKETID_ENCRYPTION_KEY} - TRUST_PROXY=true - PUID=${PUID} - PGID=${PGID} restart: unless-stoppedAPP_URLmust exactly match the address you'll visit, includinghttps://. Passkeys are bound to that origin, so a mismatch means they silently stop working.TRUST_PROXY=truetells it to honour the forwarded headers from Caddy. Without it, it sees every request as coming from the proxy.The default database is SQLite, which is fine for a household. Pocket ID also supports Postgres viaDB_PROVIDERif you'd rather consolidate databases. -
Add it to the Caddyfile and start
id.example.com { reverse_proxy pocket-id:1411 }docker compose up -d pocket-id caddy
Visit
https://id.example.com. You should get a valid certificate and a login page. If the certificate isn't trusted, fix that before continuing — passkeys will not register.
Creating your first passkey
There's no admin password in an environment variable, because there are no passwords at all. On first run a setup wizard creates the initial admin account, and you register a passkey immediately afterwards.
If you need to get back in later without a working passkey, Pocket ID's CLI issues a one-time access 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 application
Every app you connect needs a client entry. The process is the same each time.
-
Create the client
In Pocket ID, go to Administration → OIDC Clients → Add OIDC Client. Give it the app's name and set the callback URL, which each application specifies in its own documentation.
-
Copy the credentials
You'll get a Client ID and a Client Secret. The secret is shown once. Store it in your
.envrather than pasting it into a Compose file. -
Point the app at Pocket ID
Most apps only need the issuer URL and will discover the rest themselves:
https://id.example.com/.well-known/openid-configuration
Connecting your apps
This is where expectations need managing. Single sign-on across a media stack is not the solved problem it appears to be — most of these applications have no OIDC support at all, and the most important one has an awkward caveat.
What actually supports it
| App | OIDC | Reality |
|---|---|---|
| Immich | Native | Works properly, including the mobile app |
| Arcane | Native | Supported directly |
| Jellyfin | Plugin | Community plugin, web browser only — see below |
| Jellyseerr | Varies | Signs in through Jellyfin; check your version's options |
| Sonarr / Radarr / Lidarr | None | Basic forms login only |
| Bazarr | None | Basic login only |
| qBittorrent | None | Its own single account |
| Uptime Kuma | None | Its own accounts |
| ArchiveBox | None | Django accounts, LDAP possible |
| Tube Archivist | None | Its own accounts |
Realistically, Pocket ID gives you one login for Immich, Arcane and Jellyfin in a browser. That's a genuine improvement if those are the apps other people touch. It does not give you one login for the whole stack, and anyone claiming otherwise hasn't tried it on a television.
Immich
The best-supported of the three, and the one where SSO is most worth having, since it's the app family members use daily on their phones.
In Pocket ID, create a client with these callback URLs:
https://photos.example.com/auth/login
https://photos.example.com/user-settings
app.immich:///oauth-callback # for the mobile app
Then in Immich, under 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 |
| Auto register | on, to create accounts on first login |
Jellyfin — read this before starting
Jellyfin has no native OpenID Connect support. There's a community SSO plugin that works well, but it carries a limitation that matters enormously for a media server.
Native OIDC has been proposed upstream and is being worked on, but it isn't in a released version at the time of writing, and the discussion points at a future major release. Until then, this is the situation.
Given that, decide honestly whether it's worth it:
If you're proceeding
The original plugin was archived in May 2026. Use the maintained fork, which keeps the same plugin ID (so it upgrades in place) and adds a proper admin UI. Add its repository under Administration → Plugins → Repositories, install SSO Authentication from the catalogue, and restart Jellyfin.
https://raw.githubusercontent.com/K0lin/jellyfin-plugin-sso/manifest-release/manifest.json
In Pocket ID, create a client whose callback URL follows the plugin's pattern — the provider name at the end is whatever you choose to call it, and must match on both sides:
https://jellyfin.example.com/sso/OID/redirect/pocketid
Configure the plugin with your issuer URL, client ID and secret, enable it, tick folder access, and restart Jellyfin — configuration changes don't take effect until you do.
The login button isn't added automatically. Add a custom link in Jellyfin's config/web/config.json:
{
"customLinks": [
{ "name": "Sign in with SSO",
"url": "/sso/OID/start/pocketid",
"icon": "lock" }
]
}
Everything else
For apps with no OIDC support, you have three honest options.
Keep them private
Don't publish them at all. Reach Sonarr, Radarr, qBittorrent and Arcane through Tailscale. Their own login is then a second layer, not the only one.
Forward auth
A proxy component checks authentication before passing the request on, putting a login in front of apps that don't support one.
Just use their logins
Set a strong unique password on each. Unglamorous, but these are admin tools you touch rarely, and a password manager makes it a non-issue.
User management
Accounts live in several places at once, and knowing which system owns what saves a lot of confusion later.
| System | Owns |
|---|---|
| Pocket ID | Who a person is, their passkeys, and group membership |
| Jellyfin | What they can watch, and playback permissions |
| Jellyseerr | What they may request, and how much |
| Immich | Their photo library and sharing |
Pocket ID authenticates. Each app authorises. Deleting someone from Pocket ID stops them logging in through it, but doesn't remove their Jellyfin account or their data — those need handling separately.
Groups
Create groups in Pocket ID and assign people to them. Applications that read group claims can map them to their own permissions.
Two groups is usually plenty for a household. Elaborate role hierarchies are for organisations, and they mostly create work for you.
Setting up the household
-
Create the person in Pocket ID
Add them under Users, then send them a one-time link so they can register their own passkey on their own device. You never handle a credential for them.
-
Give them a Jellyfin account
Because TV apps can't use SSO, create a normal Jellyfin user too. Set library access, and turn off downloading and management unless they need it.
Untick "Allow media playback that requires transcoding" for casual users if your server is CPU-limited. It's the difference between one person's device quietly overloading your machine and it politely refusing. -
Add them to Jellyseerr
Import users from Jellyfin, then set request quotas — something like five movies a week prevents enthusiastic new users filling your disk in an afternoon. Leave auto-approve off until you know how they use it.
-
Immich, if they want it
With auto-registration on, they just sign in and get their own library. Photos are private per user unless explicitly shared, so this needs no configuration from you.
-
Write down what you did
Keep a short note of which accounts exist where. In a year, when someone leaves or an app misbehaves, you'll want it — and it takes two minutes now.
Removing someone
Because accounts are spread across systems, offboarding is a checklist rather than one button.
- Delete or disable them in Pocket ID — stops SSO logins immediately
- Delete their Jellyfin user — their TV apps keep working until you do
- Remove them from Jellyseerr, so pending requests don't linger
- Decide what happens to their Immich library before deleting the account, since that removes their photos
- Revoke active sessions in Jellyfin, or a logged-in device continues to play
Security checklist
- Only ports 80 and 443 forwarded — never application ports
- Arcane, qBittorrent, Sonarr, Radarr and Jackett are not publicly reachable
- At least two passkeys registered, on separate devices
- One local admin account kept on Jellyfin as a fallback
- Every remaining app has a unique password, stored in a password manager
- Certificates renewing automatically — check once after 60 days
- Caddy's
/datavolume included in your backups .envis mode 600 and excluded from git- You've tested logging in from outside your network, in a private window
- You know how to get back in if Pocket ID is unavailable